@adhisang/minecraft-modding-mcp 6.1.0 → 6.2.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 +37 -0
- package/README.md +4 -2
- package/dist/cache-registry.d.ts +1 -1
- package/dist/cache-registry.js +3 -0
- package/dist/entry-tools/analyze-mod-service.d.ts +12 -6
- package/dist/entry-tools/analyze-mod-service.js +37 -3
- package/dist/entry-tools/analyze-symbol-service.d.ts +2 -0
- package/dist/entry-tools/analyze-symbol-service.js +37 -2
- package/dist/entry-tools/inspect-minecraft/internal.d.ts +4 -0
- package/dist/entry-tools/inspect-minecraft/internal.js +28 -0
- package/dist/entry-tools/manage-cache-service.d.ts +4 -4
- package/dist/error-mapping.d.ts +13 -0
- package/dist/error-mapping.js +35 -2
- package/dist/errors.d.ts +2 -0
- package/dist/errors.js +2 -0
- package/dist/index.js +37 -17
- package/dist/mapping/internal-types.d.ts +7 -0
- package/dist/mapping/types.d.ts +18 -0
- package/dist/mapping-service.js +16 -3
- package/dist/minecraft-explorer-service.d.ts +4 -0
- package/dist/minecraft-explorer-service.js +156 -17
- package/dist/mod-analyzer.d.ts +7 -0
- package/dist/mod-analyzer.js +28 -7
- package/dist/nbt/typed-json.js +4 -1
- package/dist/source/artifact-resolver.d.ts +2 -0
- package/dist/source/artifact-resolver.js +24 -1
- package/dist/source/class-source/members-builder.d.ts +4 -0
- package/dist/source/class-source/members-builder.js +3 -1
- package/dist/source/class-source.d.ts +2 -1
- package/dist/source/class-source.js +154 -17
- package/dist/source/did-you-mean.d.ts +14 -0
- package/dist/source/did-you-mean.js +79 -0
- package/dist/source/file-access.js +159 -3
- package/dist/source/indexer.js +72 -2
- package/dist/source/lifecycle/runtime-check.js +15 -7
- package/dist/source/nested-jars.d.ts +59 -0
- package/dist/source/nested-jars.js +198 -0
- package/dist/source/workspace-target.js +5 -2
- package/dist/source-jar-reader.d.ts +16 -0
- package/dist/source-jar-reader.js +82 -0
- package/dist/source-service.d.ts +36 -0
- package/dist/source-service.js +49 -6
- package/dist/stage-emitter.js +24 -8
- package/dist/stdio-supervisor.d.ts +92 -9
- package/dist/stdio-supervisor.js +915 -103
- package/dist/tool-contract-manifest.js +1 -1
- package/dist/tool-guidance.js +3 -1
- package/dist/tool-schemas.d.ts +1337 -149
- package/dist/tool-schemas.js +38 -7
- package/dist/types.d.ts +23 -0
- package/dist/workspace-mapping-service.d.ts +1 -0
- package/dist/workspace-mapping-service.js +120 -8
- package/docs/tool-reference.md +19 -3
- package/package.json +1 -1
package/dist/source/indexer.js
CHANGED
|
@@ -7,6 +7,7 @@ import { decompileBinaryJar } from "../decompiler/vineflower.js";
|
|
|
7
7
|
import { ERROR_CODES, createError, isAppError } from "../errors.js";
|
|
8
8
|
import { log } from "../logger.js";
|
|
9
9
|
import { resolveMojangTinyFile } from "../mojang-tiny-mapping-service.js";
|
|
10
|
+
import { detectShellJarInventory } from "./nested-jars.js";
|
|
10
11
|
import { iterateJavaEntriesAsUtf8 } from "../source-jar-reader.js";
|
|
11
12
|
import { extractSymbolsFromSource } from "../symbols/symbol-extractor.js";
|
|
12
13
|
import { remapJar } from "../tiny-remapper-service.js";
|
|
@@ -44,7 +45,11 @@ export async function indexArtifact(svc, input) {
|
|
|
44
45
|
const artifact = svc.getArtifact(artifactId);
|
|
45
46
|
const force = input.force ?? false;
|
|
46
47
|
const meta = svc.indexMetaRepo.get(artifact.artifactId);
|
|
47
|
-
|
|
48
|
+
// Shell jars legitimately index zero files; without force they count as
|
|
49
|
+
// current instead of re-running shell detection on every reindex call.
|
|
50
|
+
const hasFiles = meta
|
|
51
|
+
? meta.filesCount > 0 || artifact.qualityFlags.includes("shell-jar")
|
|
52
|
+
: false;
|
|
48
53
|
const expectedSignature = artifact.artifactSignature ?? fallbackArtifactSignature(artifact.artifactId);
|
|
49
54
|
const reason = resolveIndexRebuildReason({
|
|
50
55
|
force,
|
|
@@ -180,8 +185,56 @@ export async function buildRebuiltArtifactData(svc, resolved) {
|
|
|
180
185
|
let files = [];
|
|
181
186
|
if (resolved.sourceJarPath) {
|
|
182
187
|
files = await loadFromSourceJar(svc, resolved.sourceJarPath);
|
|
188
|
+
// Loom split-source pairs (common/clientOnly) publish the version across
|
|
189
|
+
// two sources jars; index the companion half too so neither side's
|
|
190
|
+
// classes go missing. The primary jar wins on duplicate paths. Note the
|
|
191
|
+
// artifact signature derives from the primary jar only: a regenerated
|
|
192
|
+
// companion lands at a new hash-addressed path, so persisted provenance
|
|
193
|
+
// can point at a deleted companion — that must degrade to a primary-only
|
|
194
|
+
// index, never fail the primary rebuild.
|
|
195
|
+
for (const companion of resolved.provenance?.companionSourceJars ?? []) {
|
|
196
|
+
let extra;
|
|
197
|
+
try {
|
|
198
|
+
extra = await loadFromSourceJar(svc, companion);
|
|
199
|
+
}
|
|
200
|
+
catch (companionError) {
|
|
201
|
+
log("warn", "index.companion_source_skipped", {
|
|
202
|
+
artifactId: resolved.artifactId,
|
|
203
|
+
companion,
|
|
204
|
+
reason: companionError instanceof Error ? companionError.message : String(companionError)
|
|
205
|
+
});
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
const seenPaths = new Set(files.map((file) => file.filePath));
|
|
209
|
+
files.push(...extra.filter((file) => !seenPaths.has(file.filePath)));
|
|
210
|
+
}
|
|
183
211
|
}
|
|
184
212
|
else if (resolved.binaryJarPath) {
|
|
213
|
+
// Jar-in-Jar shells (near-zero own classes, all content in nested jars)
|
|
214
|
+
// would decompile to zero Java files and dead-end in
|
|
215
|
+
// ERR_DECOMPILER_FAILED. Detect them before remap/decompile: the artifact
|
|
216
|
+
// is created with an empty file index, the nested-jar inventory persisted
|
|
217
|
+
// in provenance, and class-family lookups redirect into the nested jars.
|
|
218
|
+
const shellInventory = await detectShellJarInventory(resolved.binaryJarPath);
|
|
219
|
+
if (shellInventory) {
|
|
220
|
+
const qualityFlags = resolved.qualityFlags ?? [];
|
|
221
|
+
resolved.qualityFlags = qualityFlags.includes("shell-jar")
|
|
222
|
+
? qualityFlags
|
|
223
|
+
: [...qualityFlags, "shell-jar"];
|
|
224
|
+
if (resolved.provenance) {
|
|
225
|
+
// Mutate in place: resolveArtifact holds a reference to this object
|
|
226
|
+
// for its response, mirroring the binaryJarPath swap below.
|
|
227
|
+
resolved.provenance.nestedJars = shellInventory;
|
|
228
|
+
}
|
|
229
|
+
resolved.isDecompiled = false;
|
|
230
|
+
return {
|
|
231
|
+
files: [],
|
|
232
|
+
symbols: [],
|
|
233
|
+
totalContentBytes: 0,
|
|
234
|
+
indexedAt: new Date().toISOString(),
|
|
235
|
+
indexDurationMs: Date.now() - indexStartedAt
|
|
236
|
+
};
|
|
237
|
+
}
|
|
185
238
|
const decompileInputJarPath = await maybeRemapBinaryForMojang(svc, resolved);
|
|
186
239
|
// When the binary jar was remapped from obfuscated to mojang, swap the resolved
|
|
187
240
|
// artifact's binaryJarPath to the remapped jar so downstream bytecode consumers
|
|
@@ -298,7 +351,11 @@ export async function ingestIfNeeded(svc, resolved) {
|
|
|
298
351
|
// Derive hasFiles from meta instead of a separate listFiles probe: when meta is
|
|
299
352
|
// absent the reason is "missing_meta" regardless of hasFiles, and when present
|
|
300
353
|
// meta.filesCount is the authoritative count written alongside the file rows.
|
|
301
|
-
|
|
354
|
+
// Shell jars legitimately index zero files (their content lives in nested
|
|
355
|
+
// jars), so their empty index counts as current instead of forcing a
|
|
356
|
+
// re-detection rebuild on every warm resolve.
|
|
357
|
+
const existingIsShell = existing?.qualityFlags.includes("shell-jar") ?? false;
|
|
358
|
+
const hasFiles = meta ? meta.filesCount > 0 || existingIsShell : false;
|
|
302
359
|
const reason = resolveIndexRebuildReason({
|
|
303
360
|
force: false,
|
|
304
361
|
expectedSignature: resolved.artifactSignature,
|
|
@@ -306,6 +363,19 @@ export async function ingestIfNeeded(svc, resolved) {
|
|
|
306
363
|
meta
|
|
307
364
|
});
|
|
308
365
|
if (existing && reason === "already_current") {
|
|
366
|
+
if (existingIsShell) {
|
|
367
|
+
// Reconcile shell state onto the freshly-resolved object so warm-cache
|
|
368
|
+
// responses carry the same flag and inventory as the first resolve.
|
|
369
|
+
const qualityFlags = resolved.qualityFlags ?? [];
|
|
370
|
+
if (!qualityFlags.includes("shell-jar")) {
|
|
371
|
+
resolved.qualityFlags = [...qualityFlags, "shell-jar"];
|
|
372
|
+
}
|
|
373
|
+
const persistedInventory = existing.provenance?.nestedJars;
|
|
374
|
+
if (resolved.provenance && persistedInventory && !resolved.provenance.nestedJars) {
|
|
375
|
+
resolved.provenance.nestedJars = persistedInventory;
|
|
376
|
+
}
|
|
377
|
+
resolved.isDecompiled = false;
|
|
378
|
+
}
|
|
309
379
|
// Mojang binary-remap reconciliation on the warm cache hit path:
|
|
310
380
|
// resolveSourceTargetInternal always returns the original binary jar
|
|
311
381
|
// (resolver does not know about prior remap output), so without this
|
|
@@ -73,13 +73,16 @@ export async function checkSymbolExistsInUnobfuscatedRuntime(svc, input, fallbac
|
|
|
73
73
|
]
|
|
74
74
|
};
|
|
75
75
|
}
|
|
76
|
-
const warnings = [
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
76
|
+
const warnings = [...fallbackBase.warnings, ...signature.warnings];
|
|
77
|
+
// Runtime validation is reported as the structured
|
|
78
|
+
// mappingContext.runtimeValidated flag instead of a per-response sentence.
|
|
79
|
+
const runtimeValidatedContext = {
|
|
80
|
+
...fallbackBase.mappingContext,
|
|
81
|
+
runtimeValidated: true
|
|
82
|
+
};
|
|
81
83
|
const buildResolved = (resolvedSymbol) => ({
|
|
82
84
|
...fallbackBase,
|
|
85
|
+
mappingContext: runtimeValidatedContext,
|
|
83
86
|
querySymbol,
|
|
84
87
|
resolved: true,
|
|
85
88
|
status: "resolved",
|
|
@@ -96,6 +99,7 @@ export async function checkSymbolExistsInUnobfuscatedRuntime(svc, input, fallbac
|
|
|
96
99
|
});
|
|
97
100
|
const buildUnresolved = (status) => ({
|
|
98
101
|
...fallbackBase,
|
|
102
|
+
mappingContext: runtimeValidatedContext,
|
|
99
103
|
querySymbol,
|
|
100
104
|
resolved: false,
|
|
101
105
|
status,
|
|
@@ -142,8 +146,12 @@ export async function checkSymbolExistsInUnobfuscatedRuntime(svc, input, fallbac
|
|
|
142
146
|
}
|
|
143
147
|
const descriptor = input.descriptor?.trim();
|
|
144
148
|
const matched = methodCandidates.filter((method) => method.jvmDescriptor === descriptor);
|
|
145
|
-
|
|
146
|
-
|
|
149
|
+
// An exact descriptor pins a single overload. Multiple matches only arise when an
|
|
150
|
+
// inherited method is overridden (same name + descriptor, different owner) and
|
|
151
|
+
// includeInherited surfaces both copies — that is the same logical method, not an
|
|
152
|
+
// ambiguity. Only zero matches is not_found. (Mirrors the name-only fix above.)
|
|
153
|
+
if (matched.length === 0) {
|
|
154
|
+
return buildUnresolved("not_found");
|
|
147
155
|
}
|
|
148
156
|
return buildResolved({
|
|
149
157
|
kind: "method",
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A jar with at most this many own `.class` entries can qualify as a
|
|
3
|
+
* Jar-in-Jar shell. Real Fabric API umbrella jars carry zero to a handful of
|
|
4
|
+
* marker classes while every API class lives in META-INF/jars.
|
|
5
|
+
*/
|
|
6
|
+
export declare const SHELL_JAR_MAX_OUTER_CLASSES = 8;
|
|
7
|
+
export declare const NESTED_JAR_CACHE_DIRNAME = "nested-jars";
|
|
8
|
+
export interface NestedJarMatch {
|
|
9
|
+
entryName: string;
|
|
10
|
+
extractedPath: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Shell detection with both required signals: near-zero own classes AND
|
|
14
|
+
* bundled nested jars (META-INF/jars scan or fabric.mod.json "jars"
|
|
15
|
+
* declarations that really exist in the archive). Returns the inventory for
|
|
16
|
+
* shells and undefined for every regular jar.
|
|
17
|
+
*/
|
|
18
|
+
export declare function detectShellJarInventory(jarPath: string): Promise<string[] | undefined>;
|
|
19
|
+
/**
|
|
20
|
+
* Content-addressed on-disk location of an extracted nested jar. The digest
|
|
21
|
+
* covers the outer jar path, its signature, and the entry name, so the same
|
|
22
|
+
* shell always maps to the same extracted file and re-resolution reuses it.
|
|
23
|
+
*/
|
|
24
|
+
export declare function nestedJarCachePath(cacheDir: string, outerJarPath: string, outerSignature: string, entryName: string): string;
|
|
25
|
+
/**
|
|
26
|
+
* Extracts one nested jar to the content-addressed cache (no-op when already
|
|
27
|
+
* present). Entry-name safety is enforced by readJarEntryAsBuffer, and the
|
|
28
|
+
* on-disk name is the digest — never derived from the entry name — so a
|
|
29
|
+
* hostile entry name cannot escape the cache directory.
|
|
30
|
+
*/
|
|
31
|
+
export declare function extractNestedJar(cacheDir: string, outerJarPath: string, outerSignature: string, entryName: string): Promise<string>;
|
|
32
|
+
/**
|
|
33
|
+
* Finds every nested jar of a shell that contains the class. Zero matches
|
|
34
|
+
* means the class genuinely is not bundled; more than one means the caller
|
|
35
|
+
* must return candidates instead of picking silently.
|
|
36
|
+
*
|
|
37
|
+
* The lookup is deliberately single-level: it inspects each nested jar's own
|
|
38
|
+
* class list only. A nested jar that is itself a shell surfaces its content
|
|
39
|
+
* when resolved directly as its own artifact.
|
|
40
|
+
*/
|
|
41
|
+
export declare function findNestedJarsContainingClass(args: {
|
|
42
|
+
cacheDir: string;
|
|
43
|
+
outerJarPath: string;
|
|
44
|
+
outerSignature: string;
|
|
45
|
+
inventory: string[];
|
|
46
|
+
internalName: string;
|
|
47
|
+
}): Promise<NestedJarMatch[]>;
|
|
48
|
+
/**
|
|
49
|
+
* Resolves the single nested jar containing a class. Zero matches returns
|
|
50
|
+
* undefined (the caller keeps its not-found contract); several matches throw
|
|
51
|
+
* candidates instead of picking one silently.
|
|
52
|
+
*/
|
|
53
|
+
export declare function resolveUniqueNestedJarForClass(args: {
|
|
54
|
+
cacheDir: string;
|
|
55
|
+
outerJarPath: string;
|
|
56
|
+
outerArtifactId: string;
|
|
57
|
+
inventory: string[];
|
|
58
|
+
className: string;
|
|
59
|
+
}): Promise<NestedJarMatch | undefined>;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { access, mkdir, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { buildSuggestedCall } from "../build-suggested-call.js";
|
|
5
|
+
import { ERROR_CODES, createError } from "../errors.js";
|
|
6
|
+
import { collectNestedJars } from "../mod-analyzer.js";
|
|
7
|
+
import { listJarEntries, readJarEntryAsBuffer, readJarEntryAsUtf8 } from "../source-jar-reader.js";
|
|
8
|
+
/**
|
|
9
|
+
* A jar with at most this many own `.class` entries can qualify as a
|
|
10
|
+
* Jar-in-Jar shell. Real Fabric API umbrella jars carry zero to a handful of
|
|
11
|
+
* marker classes while every API class lives in META-INF/jars.
|
|
12
|
+
*/
|
|
13
|
+
export const SHELL_JAR_MAX_OUTER_CLASSES = 8;
|
|
14
|
+
export const NESTED_JAR_CACHE_DIRNAME = "nested-jars";
|
|
15
|
+
// In-process cache of class listings per extracted nested jar, so repeated
|
|
16
|
+
// class lookups against the same shell never re-open its nested jars. The
|
|
17
|
+
// shell's own inventory is persisted with the artifact record; this cache is
|
|
18
|
+
// only the per-nested-jar class membership.
|
|
19
|
+
const classSetCache = new Map();
|
|
20
|
+
const CLASS_SET_CACHE_MAX = 32;
|
|
21
|
+
function rememberClassSet(key, value) {
|
|
22
|
+
classSetCache.delete(key);
|
|
23
|
+
classSetCache.set(key, value);
|
|
24
|
+
while (classSetCache.size > CLASS_SET_CACHE_MAX) {
|
|
25
|
+
const oldest = classSetCache.keys().next().value;
|
|
26
|
+
if (oldest === undefined) {
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
classSetCache.delete(oldest);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Shell detection with both required signals: near-zero own classes AND
|
|
34
|
+
* bundled nested jars (META-INF/jars scan or fabric.mod.json "jars"
|
|
35
|
+
* declarations that really exist in the archive). Returns the inventory for
|
|
36
|
+
* shells and undefined for every regular jar.
|
|
37
|
+
*/
|
|
38
|
+
export async function detectShellJarInventory(jarPath) {
|
|
39
|
+
let entries;
|
|
40
|
+
try {
|
|
41
|
+
entries = await listJarEntries(jarPath);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
let ownClassCount = 0;
|
|
47
|
+
for (const entry of entries) {
|
|
48
|
+
if (entry.endsWith(".class")) {
|
|
49
|
+
ownClassCount += 1;
|
|
50
|
+
if (ownClassCount > SHELL_JAR_MAX_OUTER_CLASSES) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
let declared;
|
|
56
|
+
if (entries.includes("fabric.mod.json")) {
|
|
57
|
+
try {
|
|
58
|
+
const parsed = JSON.parse(await readJarEntryAsUtf8(jarPath, "fabric.mod.json"));
|
|
59
|
+
const jars = parsed?.jars;
|
|
60
|
+
if (Array.isArray(jars)) {
|
|
61
|
+
declared = jars
|
|
62
|
+
.map((entry) => entry?.file)
|
|
63
|
+
.filter((file) => typeof file === "string");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// Unreadable metadata leaves only the META-INF/jars scan signal.
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const inventory = collectNestedJars(entries, declared);
|
|
71
|
+
return inventory.length > 0 ? inventory : undefined;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Content-addressed on-disk location of an extracted nested jar. The digest
|
|
75
|
+
* covers the outer jar path, its signature, and the entry name, so the same
|
|
76
|
+
* shell always maps to the same extracted file and re-resolution reuses it.
|
|
77
|
+
*/
|
|
78
|
+
export function nestedJarCachePath(cacheDir, outerJarPath, outerSignature, entryName) {
|
|
79
|
+
const digest = createHash("sha256")
|
|
80
|
+
.update(`${outerJarPath}|${outerSignature}|${entryName}`)
|
|
81
|
+
.digest("hex");
|
|
82
|
+
return join(cacheDir, NESTED_JAR_CACHE_DIRNAME, `${digest}.jar`);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Extracts one nested jar to the content-addressed cache (no-op when already
|
|
86
|
+
* present). Entry-name safety is enforced by readJarEntryAsBuffer, and the
|
|
87
|
+
* on-disk name is the digest — never derived from the entry name — so a
|
|
88
|
+
* hostile entry name cannot escape the cache directory.
|
|
89
|
+
*/
|
|
90
|
+
export async function extractNestedJar(cacheDir, outerJarPath, outerSignature, entryName) {
|
|
91
|
+
const finalPath = nestedJarCachePath(cacheDir, outerJarPath, outerSignature, entryName);
|
|
92
|
+
try {
|
|
93
|
+
await access(finalPath);
|
|
94
|
+
return finalPath;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// fall through to extraction
|
|
98
|
+
}
|
|
99
|
+
const bytes = await readJarEntryAsBuffer(outerJarPath, entryName);
|
|
100
|
+
await mkdir(dirname(finalPath), { recursive: true });
|
|
101
|
+
const tempPath = `${finalPath}.tmp.${process.pid}.${Date.now()}.${randomBytes(6).toString("hex")}`;
|
|
102
|
+
await writeFile(tempPath, bytes);
|
|
103
|
+
try {
|
|
104
|
+
await rename(tempPath, finalPath);
|
|
105
|
+
}
|
|
106
|
+
catch (renameError) {
|
|
107
|
+
// A concurrent extraction of the same entry may have won the rename.
|
|
108
|
+
// The content-addressed final file being present makes this call a
|
|
109
|
+
// success; anything else is a real failure.
|
|
110
|
+
try {
|
|
111
|
+
await access(finalPath);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
throw renameError;
|
|
115
|
+
}
|
|
116
|
+
await rm(tempPath, { force: true });
|
|
117
|
+
}
|
|
118
|
+
return finalPath;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Finds every nested jar of a shell that contains the class. Zero matches
|
|
122
|
+
* means the class genuinely is not bundled; more than one means the caller
|
|
123
|
+
* must return candidates instead of picking silently.
|
|
124
|
+
*
|
|
125
|
+
* The lookup is deliberately single-level: it inspects each nested jar's own
|
|
126
|
+
* class list only. A nested jar that is itself a shell surfaces its content
|
|
127
|
+
* when resolved directly as its own artifact.
|
|
128
|
+
*/
|
|
129
|
+
export async function findNestedJarsContainingClass(args) {
|
|
130
|
+
const classEntry = `${args.internalName}.class`;
|
|
131
|
+
const matches = [];
|
|
132
|
+
for (const entryName of args.inventory) {
|
|
133
|
+
let extractedPath;
|
|
134
|
+
try {
|
|
135
|
+
extractedPath = await extractNestedJar(args.cacheDir, args.outerJarPath, args.outerSignature, entryName);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// Unsafe or unreadable nested entries never become candidates.
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
let classSet = classSetCache.get(extractedPath);
|
|
142
|
+
if (!classSet) {
|
|
143
|
+
try {
|
|
144
|
+
const entries = await listJarEntries(extractedPath);
|
|
145
|
+
classSet = new Set(entries.filter((entry) => entry.endsWith(".class")));
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
rememberClassSet(extractedPath, classSet);
|
|
151
|
+
}
|
|
152
|
+
if (classSet.has(classEntry)) {
|
|
153
|
+
matches.push({ entryName, extractedPath });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return matches;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Resolves the single nested jar containing a class. Zero matches returns
|
|
160
|
+
* undefined (the caller keeps its not-found contract); several matches throw
|
|
161
|
+
* candidates instead of picking one silently.
|
|
162
|
+
*/
|
|
163
|
+
export async function resolveUniqueNestedJarForClass(args) {
|
|
164
|
+
const internalName = args.className.replace(/\./g, "/");
|
|
165
|
+
const matches = await findNestedJarsContainingClass({
|
|
166
|
+
cacheDir: args.cacheDir,
|
|
167
|
+
outerJarPath: args.outerJarPath,
|
|
168
|
+
outerSignature: args.outerArtifactId,
|
|
169
|
+
inventory: args.inventory,
|
|
170
|
+
internalName
|
|
171
|
+
});
|
|
172
|
+
if (matches.length === 0) {
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
const single = matches.length === 1 ? matches[0] : undefined;
|
|
176
|
+
if (single) {
|
|
177
|
+
return single;
|
|
178
|
+
}
|
|
179
|
+
throw createError({
|
|
180
|
+
code: ERROR_CODES.NESTED_JAR_AMBIGUOUS,
|
|
181
|
+
message: `Class "${args.className}" exists in ${matches.length} nested jars bundled by this shell jar; refusing to pick one automatically.`,
|
|
182
|
+
details: {
|
|
183
|
+
className: args.className,
|
|
184
|
+
shellArtifactId: args.outerArtifactId,
|
|
185
|
+
nestedJarCandidates: matches.map((match) => match.entryName),
|
|
186
|
+
nextAction: "Resolve the intended nested jar as its own artifact, then query the class against that artifactId.",
|
|
187
|
+
...buildSuggestedCall({
|
|
188
|
+
tool: "resolve-artifact",
|
|
189
|
+
params: undefined,
|
|
190
|
+
examples: matches.map((match) => ({
|
|
191
|
+
params: { target: { kind: "jar", value: match.extractedPath } },
|
|
192
|
+
reason: `Query classes inside "${match.entryName}" directly.`
|
|
193
|
+
}))
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
//# sourceMappingURL=nested-jars.js.map
|
|
@@ -243,7 +243,7 @@ export async function synthesizeDependencyTarget(svc, input, dep) {
|
|
|
243
243
|
? `Multiple cached versions for ${group}:${name} in ~/.gradle/caches/modules-2 (${result.candidatesSeen.join(", ")}); refusing to pick without project-specific evidence.`
|
|
244
244
|
: `Could not resolve a version for dependency ${group}:${name} from gradle.properties or modules-2 cache.`;
|
|
245
245
|
const nextAction = ambiguous
|
|
246
|
-
? `Set ${name}_version (or another supported gradle.properties key) so the project's intended version is unambiguous,
|
|
246
|
+
? `Set ${name}_version (or another supported gradle.properties key) so the project's intended version is unambiguous, pass an explicit version on the dependency target, or declare the umbrella version property so the cached umbrella POM can supply the submodule version.`
|
|
247
247
|
: "Provide an explicit version on the dependency target, or add a property to gradle.properties so detectDependencyVersion can find it.";
|
|
248
248
|
throw createError({
|
|
249
249
|
code: ERROR_CODES.DEPENDENCY_VERSION_UNRESOLVED,
|
|
@@ -305,7 +305,10 @@ export async function synthesizeDependencyTarget(svc, input, dep) {
|
|
|
305
305
|
source: result.source,
|
|
306
306
|
candidatesSeen: result.candidatesSeen,
|
|
307
307
|
attempts: result.attempts,
|
|
308
|
-
cacheHit: false
|
|
308
|
+
cacheHit: false,
|
|
309
|
+
...(result.submoduleVersionSource
|
|
310
|
+
? { submoduleVersionSource: result.submoduleVersionSource }
|
|
311
|
+
: {})
|
|
309
312
|
}
|
|
310
313
|
};
|
|
311
314
|
}
|
|
@@ -3,6 +3,7 @@ export declare function __getZipOpenCount(): number;
|
|
|
3
3
|
export declare function __resetZipOpenCount(): void;
|
|
4
4
|
interface ZipEntry {
|
|
5
5
|
fileName: string;
|
|
6
|
+
uncompressedSize: number;
|
|
6
7
|
}
|
|
7
8
|
export interface ZipFile {
|
|
8
9
|
readEntry(): void;
|
|
@@ -38,6 +39,21 @@ export declare function listJavaEntries(jarPath: string): Promise<string[]>;
|
|
|
38
39
|
export declare function hasAnyJarEntry(jarPath: string, predicate: (entryPath: string) => boolean): Promise<boolean>;
|
|
39
40
|
export declare function readJarEntryAsUtf8(jarPath: string, entryPath: string): Promise<string>;
|
|
40
41
|
export declare function readJarEntryAsBuffer(jarPath: string, entryPath: string): Promise<Buffer>;
|
|
42
|
+
export interface CappedJarEntry {
|
|
43
|
+
/** At most the requested byte budget of the entry (empty when maxBytes<=0). */
|
|
44
|
+
buffer: Buffer;
|
|
45
|
+
/** Full uncompressed size from the zip metadata, independent of the cap. */
|
|
46
|
+
entrySize: number;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Reads at most maxBytes of one entry, reporting the full uncompressed size
|
|
50
|
+
* from the zip metadata. Unlike readJarEntryAsBuffer, an oversized entry is
|
|
51
|
+
* never fully materialized: the stream is destroyed once the budget is
|
|
52
|
+
* exceeded and the collected prefix is returned. maxBytes<=0 skips the read
|
|
53
|
+
* entirely (metadata-only probe).
|
|
54
|
+
*/
|
|
55
|
+
export declare function readJarEntryCapped(jarPath: string, entryPath: string, maxBytes: number): Promise<CappedJarEntry>;
|
|
56
|
+
export declare function decodeJarEntryUtf8OrThrow(contentBuffer: Buffer, jarPath: string, entryPath: string): string;
|
|
41
57
|
export interface JarEntryReader {
|
|
42
58
|
getEntryBuffer(entryPath: string): Promise<Buffer>;
|
|
43
59
|
close(): void;
|
|
@@ -203,6 +203,88 @@ export async function readJarEntryAsBuffer(jarPath, entryPath) {
|
|
|
203
203
|
}
|
|
204
204
|
});
|
|
205
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* Reads at most maxBytes of one entry, reporting the full uncompressed size
|
|
208
|
+
* from the zip metadata. Unlike readJarEntryAsBuffer, an oversized entry is
|
|
209
|
+
* never fully materialized: the stream is destroyed once the budget is
|
|
210
|
+
* exceeded and the collected prefix is returned. maxBytes<=0 skips the read
|
|
211
|
+
* entirely (metadata-only probe).
|
|
212
|
+
*/
|
|
213
|
+
export async function readJarEntryCapped(jarPath, entryPath, maxBytes) {
|
|
214
|
+
const normalizedTargetPath = entryPath.replaceAll("\\", "/");
|
|
215
|
+
if (!isSecureJarEntryPath(normalizedTargetPath)) {
|
|
216
|
+
throw createError({
|
|
217
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
218
|
+
message: `Entry path "${normalizedTargetPath}" is not allowed.`,
|
|
219
|
+
details: { jarPath, entryPath: normalizedTargetPath }
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
return withZipFile(jarPath, async (zipFile) => {
|
|
223
|
+
while (true) {
|
|
224
|
+
const entry = await readNextEntry(zipFile);
|
|
225
|
+
if (!entry) {
|
|
226
|
+
throw createError({
|
|
227
|
+
code: ERROR_CODES.SOURCE_NOT_FOUND,
|
|
228
|
+
message: `Entry "${normalizedTargetPath}" was not found in "${jarPath}".`,
|
|
229
|
+
details: { jarPath, entryPath: normalizedTargetPath }
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
if (!isSecureJarEntryPath(entry.fileName)) {
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (entry.fileName !== normalizedTargetPath) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
const entrySize = entry.uncompressedSize;
|
|
239
|
+
if (maxBytes <= 0) {
|
|
240
|
+
return { buffer: Buffer.alloc(0), entrySize };
|
|
241
|
+
}
|
|
242
|
+
const buffer = await readEntryStreamPrefix(zipFile, entry, jarPath, maxBytes);
|
|
243
|
+
return { buffer: buffer.length > maxBytes ? buffer.slice(0, maxBytes) : buffer, entrySize };
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
/** Like readEntryStream, but resolves with the collected prefix instead of rejecting when the budget is exceeded. */
|
|
248
|
+
function readEntryStreamPrefix(zipFile, entry, jarPath, maxBytes) {
|
|
249
|
+
return new Promise((resolve, reject) => {
|
|
250
|
+
zipFile.openReadStream(entry, (error, stream) => {
|
|
251
|
+
if (error || !stream) {
|
|
252
|
+
reject(new Error(`Failed to read entry "${entry.fileName}" from "${jarPath}": ${toErrorMessage(error)}`));
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
let settled = false;
|
|
256
|
+
let totalBytes = 0;
|
|
257
|
+
const chunks = [];
|
|
258
|
+
stream.on("data", (chunk) => {
|
|
259
|
+
if (settled)
|
|
260
|
+
return;
|
|
261
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
262
|
+
totalBytes += buf.length;
|
|
263
|
+
chunks.push(buf);
|
|
264
|
+
if (totalBytes >= maxBytes) {
|
|
265
|
+
settled = true;
|
|
266
|
+
stream.destroy();
|
|
267
|
+
resolve(Buffer.concat(chunks));
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
stream.once("error", (streamError) => {
|
|
271
|
+
if (settled)
|
|
272
|
+
return;
|
|
273
|
+
settled = true;
|
|
274
|
+
reject(new Error(`Failed to read entry "${entry.fileName}" from "${jarPath}": ${toErrorMessage(streamError)}`));
|
|
275
|
+
});
|
|
276
|
+
stream.once("end", () => {
|
|
277
|
+
if (settled)
|
|
278
|
+
return;
|
|
279
|
+
settled = true;
|
|
280
|
+
resolve(Buffer.concat(chunks));
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
export function decodeJarEntryUtf8OrThrow(contentBuffer, jarPath, entryPath) {
|
|
286
|
+
return decodeUtf8OrThrow(contentBuffer, jarPath, entryPath);
|
|
287
|
+
}
|
|
206
288
|
/**
|
|
207
289
|
* Opens a jar ONCE, drains its central directory into a name->entry index, and
|
|
208
290
|
* serves repeated entry reads via O(1) lookup + openReadStream. Use this when a
|
package/dist/source-service.d.ts
CHANGED
|
@@ -140,6 +140,35 @@ export type GetArtifactFileOutput = {
|
|
|
140
140
|
mappingApplied: SourceMapping;
|
|
141
141
|
returnedNamespace: SourceMapping;
|
|
142
142
|
artifactContents: ArtifactContentsSummary;
|
|
143
|
+
/**
|
|
144
|
+
* Present when the file was served directly from the backing jar instead of
|
|
145
|
+
* the source index (non-indexed assets/ and data/ text files).
|
|
146
|
+
*/
|
|
147
|
+
deliveryMode?: "jar-read-through";
|
|
148
|
+
/** Set when content was withheld (e.g. a binary entry); explains why. */
|
|
149
|
+
contentOmittedReason?: string;
|
|
150
|
+
};
|
|
151
|
+
export type GetModClassMembersInput = {
|
|
152
|
+
jarPath: string;
|
|
153
|
+
className: string;
|
|
154
|
+
};
|
|
155
|
+
export type GetModClassMembersOutput = {
|
|
156
|
+
className: string;
|
|
157
|
+
jarPath: string;
|
|
158
|
+
members: {
|
|
159
|
+
constructors: SignatureMember[];
|
|
160
|
+
fields: SignatureMember[];
|
|
161
|
+
methods: SignatureMember[];
|
|
162
|
+
};
|
|
163
|
+
counts: {
|
|
164
|
+
constructors: number;
|
|
165
|
+
fields: number;
|
|
166
|
+
methods: number;
|
|
167
|
+
total: number;
|
|
168
|
+
};
|
|
169
|
+
/** Members are read from bytecode; no decompiler runs on this path. */
|
|
170
|
+
extractionMethod: "bytecode-only";
|
|
171
|
+
warnings: string[];
|
|
143
172
|
};
|
|
144
173
|
export type ListArtifactFilesInput = {
|
|
145
174
|
artifactId: string;
|
|
@@ -256,6 +285,7 @@ export type GetClassMembersInput = {
|
|
|
256
285
|
access?: MemberAccess;
|
|
257
286
|
includeSynthetic?: boolean;
|
|
258
287
|
includeInherited?: boolean;
|
|
288
|
+
includeAnnotations?: boolean;
|
|
259
289
|
memberPattern?: string;
|
|
260
290
|
maxMembers?: number;
|
|
261
291
|
cursor?: string;
|
|
@@ -623,6 +653,12 @@ export declare class SourceService {
|
|
|
623
653
|
getRegistryData(input: GetRegistryDataInput): Promise<GetRegistryDataOutput>;
|
|
624
654
|
compareVersions(input: CompareVersionsInput): Promise<CompareVersionsOutput>;
|
|
625
655
|
decompileModJar(input: DecompileModJarInput): Promise<DecompileModJarOutput>;
|
|
656
|
+
/**
|
|
657
|
+
* Member-level view of a third-party mod jar class, read from bytecode
|
|
658
|
+
* only — no decompiler is involved on this path, so it answers in
|
|
659
|
+
* milliseconds where a decompile-backed lookup costs a full Vineflower run.
|
|
660
|
+
*/
|
|
661
|
+
getModClassMembers(input: GetModClassMembersInput): Promise<GetModClassMembersOutput>;
|
|
626
662
|
getModClassSource(input: GetModClassSourceInput): Promise<GetModClassSourceOutput>;
|
|
627
663
|
searchModSource(input: SearchModSourceInput): Promise<SearchModSourceOutput>;
|
|
628
664
|
findMapping(input: FindMappingInput): Promise<FindMappingOutput>;
|