@adhisang/minecraft-modding-mcp 7.0.0-rc.3 → 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 +43 -0
- package/README.md +3 -2
- package/dist/cache-registry.d.ts +16 -0
- package/dist/cache-registry.js +78 -10
- 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/json-rpc-framing.d.ts +20 -0
- package/dist/json-rpc-framing.js +80 -7
- package/dist/mapping/loaders/tiny-maven.d.ts +9 -0
- package/dist/mapping/loaders/tiny-maven.js +10 -2
- package/dist/repo-downloader.js +13 -2
- 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 +229 -26
- 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/stdio-supervisor.js +193 -34
- package/dist/storage/db.d.ts +62 -2
- package/dist/storage/db.js +186 -21
- 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
|
@@ -15,6 +15,40 @@ async function safeEmit(emitter, stage, payload) {
|
|
|
15
15
|
// swallow telemetry failure
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
|
+
const DISCOVER_KIND_LABELS = [
|
|
19
|
+
["mixins", "mixin configs"],
|
|
20
|
+
["access-wideners", "access wideners"],
|
|
21
|
+
["access-transformers", "access transformers"]
|
|
22
|
+
];
|
|
23
|
+
function joinWithOr(items) {
|
|
24
|
+
if (items.length <= 2) {
|
|
25
|
+
return items.join(" or ");
|
|
26
|
+
}
|
|
27
|
+
return `${items.slice(0, -1).join(", ")}, or ${items[items.length - 1]}`;
|
|
28
|
+
}
|
|
29
|
+
// A run that discovered no files keeps status "ok" (nothing failed), so the
|
|
30
|
+
// headline and a warning must say that nothing was checked; otherwise the
|
|
31
|
+
// result reads as a pass. Only the kinds subject.discover searched are named.
|
|
32
|
+
function describeNothingToValidate(discover, projectPath) {
|
|
33
|
+
const searched = DISCOVER_KIND_LABELS
|
|
34
|
+
.filter(([kind]) => discover.includes(kind))
|
|
35
|
+
.map(([, label]) => label);
|
|
36
|
+
const notOk = "so status \"ok\" does not mean any file passed validation.";
|
|
37
|
+
const accessTransformerHint = discover.includes("access-transformers")
|
|
38
|
+
? ""
|
|
39
|
+
: " Access Transformer files are searched only when subject.discover includes \"access-transformers\".";
|
|
40
|
+
if (searched.length === 0) {
|
|
41
|
+
return {
|
|
42
|
+
headline: "Nothing to validate: subject.discover selected no file kinds to search.",
|
|
43
|
+
warning: `Nothing was validated: subject.discover selected no file kinds to search, ${notOk}${accessTransformerHint}`
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const list = joinWithOr(searched);
|
|
47
|
+
return {
|
|
48
|
+
headline: `Nothing to validate: no ${list} were found.`,
|
|
49
|
+
warning: `Nothing was validated: no ${list} were found under ${projectPath}, ${notOk}${accessTransformerHint}`
|
|
50
|
+
};
|
|
51
|
+
}
|
|
18
52
|
export async function handleProjectSummary(deps, input, detail, include, options = {}) {
|
|
19
53
|
// Forwarded emitter for nested validators and probes; same swallow contract
|
|
20
54
|
// as safeEmit so a rejecting raw emitter cannot leak into their outcomes.
|
|
@@ -27,7 +61,9 @@ export async function handleProjectSummary(deps, input, detail, include, options
|
|
|
27
61
|
message: "task=project-summary requires subject.kind=workspace."
|
|
28
62
|
});
|
|
29
63
|
}
|
|
30
|
-
|
|
64
|
+
// An omitted version is inferred from the project below; only an explicit
|
|
65
|
+
// preferProjectVersion=false opts out of that inference.
|
|
66
|
+
if (!input.version && input.preferProjectVersion === false) {
|
|
31
67
|
const baseResult = buildEntryToolResult({
|
|
32
68
|
task: "project-summary",
|
|
33
69
|
detail,
|
|
@@ -52,7 +88,7 @@ export async function handleProjectSummary(deps, input, detail, include, options
|
|
|
52
88
|
}
|
|
53
89
|
],
|
|
54
90
|
notes: [
|
|
55
|
-
"Pass version explicitly, or retry
|
|
91
|
+
"Version inference was suppressed by preferProjectVersion=false. Pass version explicitly, or retry without preferProjectVersion=false to infer it from gradle.properties. The suggested retry sets preferProjectVersion=true for you."
|
|
56
92
|
]
|
|
57
93
|
},
|
|
58
94
|
blocks: {
|
|
@@ -79,10 +115,21 @@ export async function handleProjectSummary(deps, input, detail, include, options
|
|
|
79
115
|
projectPath,
|
|
80
116
|
discover
|
|
81
117
|
});
|
|
82
|
-
|
|
118
|
+
// preferProjectVersion=true overrides an explicit version; an omitted version is
|
|
119
|
+
// inferred (the explicit-false opt-out returned above).
|
|
120
|
+
const detectedProjectVersion = input.preferProjectVersion === true || !input.version
|
|
83
121
|
? await deps.detectProjectMinecraftVersion?.(projectPath)
|
|
84
122
|
: undefined;
|
|
85
123
|
const resolvedVersion = detectedProjectVersion ?? input.version;
|
|
124
|
+
const versionInferred = !input.version && Boolean(detectedProjectVersion);
|
|
125
|
+
// An inferred version is a project-version resolution: sub-validators and the
|
|
126
|
+
// artifact probe see exactly what an explicit preferProjectVersion=true run sees.
|
|
127
|
+
const effectivePreferProjectVersion = versionInferred ? true : input.preferProjectVersion;
|
|
128
|
+
const versionInferenceWarnings = versionInferred
|
|
129
|
+
? [
|
|
130
|
+
`version was inferred from the workspace: ${detectedProjectVersion} (source: projectPath:gradle.properties (${projectPath})).`
|
|
131
|
+
]
|
|
132
|
+
: [];
|
|
86
133
|
const [mixinConfigs, accessWideners, accessTransformers] = await Promise.all([
|
|
87
134
|
discover.includes("mixins")
|
|
88
135
|
? deps.discoverMixins(projectPath, input.configPaths)
|
|
@@ -111,17 +158,20 @@ export async function handleProjectSummary(deps, input, detail, include, options
|
|
|
111
158
|
sourcePriority: input.sourcePriority,
|
|
112
159
|
scope: input.scope
|
|
113
160
|
}),
|
|
161
|
+
// Retrying without a version would infer, fail, and block again, so the
|
|
162
|
+
// recovery asks for an explicit version instead.
|
|
114
163
|
nextActions: [
|
|
115
164
|
{
|
|
116
165
|
tool: "validate-project",
|
|
117
166
|
params: {
|
|
118
167
|
task: "project-summary",
|
|
119
|
-
subject: input.subject
|
|
168
|
+
subject: input.subject,
|
|
169
|
+
version: "<minecraft-version>"
|
|
120
170
|
}
|
|
121
171
|
}
|
|
122
172
|
],
|
|
123
173
|
notes: [
|
|
124
|
-
"
|
|
174
|
+
"The Minecraft version could not be inferred from gradle.properties (minecraft_version, mc_version, or minecraftVersion). Retry with version set explicitly: replace \"<minecraft-version>\" in the suggested call with the project's Minecraft version, or declare it in gradle.properties."
|
|
125
175
|
]
|
|
126
176
|
},
|
|
127
177
|
blocks: {
|
|
@@ -150,14 +200,16 @@ export async function handleProjectSummary(deps, input, detail, include, options
|
|
|
150
200
|
]
|
|
151
201
|
};
|
|
152
202
|
}
|
|
203
|
+
// Reached only when discovery found nothing (the branch above handles found files).
|
|
153
204
|
if (!resolvedVersion) {
|
|
205
|
+
const nothingToValidate = describeNothingToValidate(discover, projectPath);
|
|
154
206
|
const baseResult = buildEntryToolResult({
|
|
155
207
|
task: "project-summary",
|
|
156
208
|
detail,
|
|
157
209
|
include,
|
|
158
210
|
summary: {
|
|
159
211
|
status: "ok",
|
|
160
|
-
headline:
|
|
212
|
+
headline: nothingToValidate.headline,
|
|
161
213
|
subject: createSummarySubject({
|
|
162
214
|
task: "project-summary",
|
|
163
215
|
kind: input.subject.kind,
|
|
@@ -187,11 +239,11 @@ export async function handleProjectSummary(deps, input, detail, include, options
|
|
|
187
239
|
return {
|
|
188
240
|
...baseResult,
|
|
189
241
|
...(tasks ? { tasks } : {}),
|
|
190
|
-
warnings: []
|
|
242
|
+
warnings: [nothingToValidate.warning]
|
|
191
243
|
};
|
|
192
244
|
}
|
|
193
245
|
const validationVersion = resolvedVersion;
|
|
194
|
-
const warnings = [];
|
|
246
|
+
const warnings = [...versionInferenceWarnings];
|
|
195
247
|
const mixinDurationStart = Date.now();
|
|
196
248
|
let validMixins = 0;
|
|
197
249
|
let partialMixins = 0;
|
|
@@ -288,7 +340,7 @@ export async function handleProjectSummary(deps, input, detail, include, options
|
|
|
288
340
|
projectPath,
|
|
289
341
|
gradleUserHome,
|
|
290
342
|
scope: input.scope,
|
|
291
|
-
preferProjectVersion:
|
|
343
|
+
preferProjectVersion: effectivePreferProjectVersion
|
|
292
344
|
});
|
|
293
345
|
if (output.valid) {
|
|
294
346
|
validAw += 1;
|
|
@@ -337,7 +389,7 @@ export async function handleProjectSummary(deps, input, detail, include, options
|
|
|
337
389
|
projectPath,
|
|
338
390
|
gradleUserHome,
|
|
339
391
|
scope: input.scope,
|
|
340
|
-
preferProjectVersion:
|
|
392
|
+
preferProjectVersion: effectivePreferProjectVersion
|
|
341
393
|
});
|
|
342
394
|
if (output.valid) {
|
|
343
395
|
validAt += 1;
|
|
@@ -361,13 +413,20 @@ export async function handleProjectSummary(deps, input, detail, include, options
|
|
|
361
413
|
const invalidCount = invalidMixins + invalidAw + invalidAt;
|
|
362
414
|
const partialCount = partialMixins;
|
|
363
415
|
const status = invalidCount > 0 ? "invalid" : partialCount > 0 ? "partial" : "ok";
|
|
416
|
+
const nothingToValidate = mixinConfigs.length === 0 && accessWideners.length === 0 && accessTransformers.length === 0
|
|
417
|
+
? describeNothingToValidate(discover, projectPath)
|
|
418
|
+
: undefined;
|
|
419
|
+
if (nothingToValidate) {
|
|
420
|
+
warnings.push(nothingToValidate.warning);
|
|
421
|
+
}
|
|
364
422
|
const baseResult = buildEntryToolResult({
|
|
365
423
|
task: "project-summary",
|
|
366
424
|
detail,
|
|
367
425
|
include,
|
|
368
426
|
summary: {
|
|
369
427
|
status,
|
|
370
|
-
headline:
|
|
428
|
+
headline: nothingToValidate?.headline ??
|
|
429
|
+
`Validated ${mixinConfigs.length} mixin config(s), ${accessWideners.length} access widener(s), and ${accessTransformers.length} access transformer(s).`,
|
|
371
430
|
subject: createSummarySubject({
|
|
372
431
|
task: "project-summary",
|
|
373
432
|
kind: input.subject.kind,
|
|
@@ -416,7 +475,7 @@ export async function handleProjectSummary(deps, input, detail, include, options
|
|
|
416
475
|
sourcePriority: input.sourcePriority,
|
|
417
476
|
gradleUserHome,
|
|
418
477
|
scope: input.scope,
|
|
419
|
-
preferProjectVersion:
|
|
478
|
+
preferProjectVersion: effectivePreferProjectVersion,
|
|
420
479
|
mixinDiscoveryCount: mixinConfigs.length,
|
|
421
480
|
mixinCaughtErrors,
|
|
422
481
|
mixinCounts: { ok: validMixins, partial: partialMixins, invalid: invalidMixins },
|
|
@@ -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;
|
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
|
|
@@ -78,6 +78,15 @@ export declare class JsonRpcFrameReader {
|
|
|
78
78
|
private awaitedBodyStart;
|
|
79
79
|
private idleTimer;
|
|
80
80
|
private fatal;
|
|
81
|
+
/**
|
|
82
|
+
* Set when an oversized, not-yet-terminated line/header-less run was just
|
|
83
|
+
* discarded with no newline in hand. The bytes that eventually complete
|
|
84
|
+
* that same logical line are not a delimiter the reader can trust as the
|
|
85
|
+
* start of a fresh frame, so every byte up to and including the next 0x0a —
|
|
86
|
+
* however many chunks it takes to arrive — is swallowed unread before
|
|
87
|
+
* normal parsing resumes. See `rejectOversizedIncompleteInput`.
|
|
88
|
+
*/
|
|
89
|
+
private discardingOversizedLine;
|
|
81
90
|
/**
|
|
82
91
|
* @param options.maxFrameBytes Largest accepted frame; defaults to
|
|
83
92
|
* {@link loadMaxFrameBytes}.
|
|
@@ -128,6 +137,17 @@ export declare class JsonRpcFrameReader {
|
|
|
128
137
|
* a caller that does not still gets a live pair rather than a stale one.
|
|
129
138
|
*/
|
|
130
139
|
private armIdleTimer;
|
|
140
|
+
/**
|
|
141
|
+
* Whether the buffer, despite `mode` still sticking at "content-length"
|
|
142
|
+
* from an earlier frame, actually opens a line-mode frame — the same probe
|
|
143
|
+
* `readContentLengthMessage` uses to detect the mid-stream switch back to
|
|
144
|
+
* line framing (a JSON object/array opener can never begin a Content-Length
|
|
145
|
+
* header block). Used to keep the header-size ceiling scoped to buffers
|
|
146
|
+
* still being accumulated as a header block, so it never judges a line
|
|
147
|
+
* frame's bytes as an oversized header just because the switch hasn't been
|
|
148
|
+
* recognized yet.
|
|
149
|
+
*/
|
|
150
|
+
private looksLikeLineFrame;
|
|
131
151
|
private canCompleteFrame;
|
|
132
152
|
private rejectOversizedIncompleteInput;
|
|
133
153
|
/**
|
package/dist/json-rpc-framing.js
CHANGED
|
@@ -40,8 +40,10 @@ const clearIdleTimerHandle = (handle) => {
|
|
|
40
40
|
*
|
|
41
41
|
* Consequence worth stating, because it is load-bearing for the caller: an
|
|
42
42
|
* EXTRA empty line after the terminator is body, not header. The body window
|
|
43
|
-
* then opens on that empty line
|
|
44
|
-
*
|
|
43
|
+
* then opens on that empty line, shifted by however many bytes that line's own
|
|
44
|
+
* terminator took — one for a bare LF, two for CRLF — bytes the peer's
|
|
45
|
+
* declared length did not count, so the window no longer covers the same span
|
|
46
|
+
* as the JSON value.
|
|
45
47
|
* All four extra-blank-line shapes behave alike here, which is the point: the
|
|
46
48
|
* reading does not depend on which terminator style the peer chose.
|
|
47
49
|
*
|
|
@@ -171,6 +173,15 @@ export class JsonRpcFrameReader {
|
|
|
171
173
|
awaitedBodyStart = -1;
|
|
172
174
|
idleTimer;
|
|
173
175
|
fatal = false;
|
|
176
|
+
/**
|
|
177
|
+
* Set when an oversized, not-yet-terminated line/header-less run was just
|
|
178
|
+
* discarded with no newline in hand. The bytes that eventually complete
|
|
179
|
+
* that same logical line are not a delimiter the reader can trust as the
|
|
180
|
+
* start of a fresh frame, so every byte up to and including the next 0x0a —
|
|
181
|
+
* however many chunks it takes to arrive — is swallowed unread before
|
|
182
|
+
* normal parsing resumes. See `rejectOversizedIncompleteInput`.
|
|
183
|
+
*/
|
|
184
|
+
discardingOversizedLine = false;
|
|
174
185
|
/**
|
|
175
186
|
* @param options.maxFrameBytes Largest accepted frame; defaults to
|
|
176
187
|
* {@link loadMaxFrameBytes}.
|
|
@@ -206,6 +217,7 @@ export class JsonRpcFrameReader {
|
|
|
206
217
|
this.awaitedFrameEnd = -1;
|
|
207
218
|
this.awaitedBodyStart = -1;
|
|
208
219
|
this.fatal = false;
|
|
220
|
+
this.discardingOversizedLine = false;
|
|
209
221
|
}
|
|
210
222
|
clear() {
|
|
211
223
|
this.clearIdleTimer();
|
|
@@ -216,6 +228,7 @@ export class JsonRpcFrameReader {
|
|
|
216
228
|
this.awaitedFrameEnd = -1;
|
|
217
229
|
this.awaitedBodyStart = -1;
|
|
218
230
|
this.fatal = false;
|
|
231
|
+
this.discardingOversizedLine = false;
|
|
219
232
|
}
|
|
220
233
|
processChunk(chunk, handlers) {
|
|
221
234
|
if (chunk.length === 0 || this.fatal) {
|
|
@@ -242,6 +255,24 @@ export class JsonRpcFrameReader {
|
|
|
242
255
|
this.pendingBytes = 0;
|
|
243
256
|
while (true) {
|
|
244
257
|
try {
|
|
258
|
+
if (this.discardingOversizedLine) {
|
|
259
|
+
// Swallow bytes up to and including the next newline WITHOUT
|
|
260
|
+
// interpreting them as a frame — they are the tail of the line just
|
|
261
|
+
// rejected as oversized, not a fresh start, even if they happen to
|
|
262
|
+
// look like a well-formed message on their own. Only once that
|
|
263
|
+
// terminator is found does this resynchronize on the byte position
|
|
264
|
+
// the peer itself delimited.
|
|
265
|
+
const newlineIndex = this.buffer.indexOf(0x0a);
|
|
266
|
+
if (newlineIndex === -1) {
|
|
267
|
+
// The discarded bytes carry no information, so there is nothing
|
|
268
|
+
// to hold onto while waiting for the terminator.
|
|
269
|
+
this.buffer = Buffer.alloc(0);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
this.buffer = this.buffer.subarray(newlineIndex + 1);
|
|
273
|
+
this.discardingOversizedLine = false;
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
245
276
|
this.rejectOversizedIncompleteInput();
|
|
246
277
|
if (this.mode === "unknown") {
|
|
247
278
|
const detected = this.detectMode();
|
|
@@ -297,6 +328,7 @@ export class JsonRpcFrameReader {
|
|
|
297
328
|
this.buffer = Buffer.alloc(0);
|
|
298
329
|
this.pendingChunks = [];
|
|
299
330
|
this.pendingBytes = 0;
|
|
331
|
+
this.discardingOversizedLine = false;
|
|
300
332
|
handlers.onError(error);
|
|
301
333
|
return;
|
|
302
334
|
}
|
|
@@ -361,6 +393,28 @@ export class JsonRpcFrameReader {
|
|
|
361
393
|
handle = this.scheduleTimer(expire, this.incompleteFrameIdleMs);
|
|
362
394
|
this.idleTimer = handle;
|
|
363
395
|
}
|
|
396
|
+
/**
|
|
397
|
+
* Whether the buffer, despite `mode` still sticking at "content-length"
|
|
398
|
+
* from an earlier frame, actually opens a line-mode frame — the same probe
|
|
399
|
+
* `readContentLengthMessage` uses to detect the mid-stream switch back to
|
|
400
|
+
* line framing (a JSON object/array opener can never begin a Content-Length
|
|
401
|
+
* header block). Used to keep the header-size ceiling scoped to buffers
|
|
402
|
+
* still being accumulated as a header block, so it never judges a line
|
|
403
|
+
* frame's bytes as an oversized header just because the switch hasn't been
|
|
404
|
+
* recognized yet.
|
|
405
|
+
*/
|
|
406
|
+
looksLikeLineFrame() {
|
|
407
|
+
let probeIndex = 0;
|
|
408
|
+
while (probeIndex < this.buffer.length &&
|
|
409
|
+
(this.buffer[probeIndex] === 0x20 ||
|
|
410
|
+
this.buffer[probeIndex] === 0x09 ||
|
|
411
|
+
this.buffer[probeIndex] === 0x0d ||
|
|
412
|
+
this.buffer[probeIndex] === 0x0a)) {
|
|
413
|
+
probeIndex += 1;
|
|
414
|
+
}
|
|
415
|
+
return (probeIndex < this.buffer.length &&
|
|
416
|
+
(this.buffer[probeIndex] === 0x7b /* '{' */ || this.buffer[probeIndex] === 0x5b /* '[' */));
|
|
417
|
+
}
|
|
364
418
|
canCompleteFrame(chunk) {
|
|
365
419
|
const bufferedBytes = this.buffer.length + this.pendingBytes;
|
|
366
420
|
if (this.mode === "content-length" && this.awaitedFrameEnd >= 0) {
|
|
@@ -373,8 +427,17 @@ export class JsonRpcFrameReader {
|
|
|
373
427
|
return chunk.includes(0x0a) || bufferedBytes > this.maxFrameBytes;
|
|
374
428
|
}
|
|
375
429
|
rejectOversizedIncompleteInput() {
|
|
376
|
-
|
|
377
|
-
|
|
430
|
+
// Sticky "content-length" mode only means a header block is being
|
|
431
|
+
// accumulated when the buffer doesn't already look like a line frame; a
|
|
432
|
+
// JSON object/array opener here is the same mid-stream switch
|
|
433
|
+
// `readContentLengthMessage` recognizes, just not yet reached. The header
|
|
434
|
+
// ceiling below must be scoped to actual header accumulation, or a large
|
|
435
|
+
// line-mode frame arriving right after a Content-Length frame gets judged
|
|
436
|
+
// as an oversized header before the switch is detected.
|
|
437
|
+
const isLineFrameAfterContentLength = this.mode === "content-length" && this.looksLikeLineFrame();
|
|
438
|
+
const inHeaderAccumulation = this.mode === "content-length" && !isLineFrameAfterContentLength;
|
|
439
|
+
const headerBoundary = inHeaderAccumulation ? findHeaderBoundary(this.buffer) : undefined;
|
|
440
|
+
if (inHeaderAccumulation &&
|
|
378
441
|
!headerBoundary &&
|
|
379
442
|
this.buffer.length > MAX_CONTENT_LENGTH_HEADER_BYTES) {
|
|
380
443
|
// No header terminator anywhere in an over-limit header block: there is
|
|
@@ -387,15 +450,25 @@ export class JsonRpcFrameReader {
|
|
|
387
450
|
if (this.buffer.length <= this.maxFrameBytes) {
|
|
388
451
|
return;
|
|
389
452
|
}
|
|
390
|
-
if (
|
|
453
|
+
if (inHeaderAccumulation && headerBoundary) {
|
|
391
454
|
return;
|
|
392
455
|
}
|
|
393
|
-
if (
|
|
456
|
+
if (!inHeaderAccumulation && this.buffer.includes(0x0a)) {
|
|
394
457
|
return;
|
|
395
458
|
}
|
|
396
459
|
const observedBytes = this.buffer.length;
|
|
397
|
-
const description = this.mode === "line"
|
|
460
|
+
const description = this.mode === "line" || isLineFrameAfterContentLength
|
|
461
|
+
? "Line-delimited JSON-RPC frame"
|
|
462
|
+
: "Headerless JSON-RPC input";
|
|
398
463
|
this.buffer = Buffer.alloc(0);
|
|
464
|
+
if (!inHeaderAccumulation) {
|
|
465
|
+
// The oversized run has no newline anywhere in it yet (the check above
|
|
466
|
+
// would otherwise have returned): remember to swallow bytes through the
|
|
467
|
+
// eventual terminator — wherever it arrives — before resuming normal
|
|
468
|
+
// parsing, so the discarded line's own tail is never re-read as a fresh
|
|
469
|
+
// frame (see `discardingOversizedLine` in `drainChunk`).
|
|
470
|
+
this.discardingOversizedLine = true;
|
|
471
|
+
}
|
|
399
472
|
throw new Error(`${description} is ${observedBytes} bytes, exceeding the configured frame limit of ` +
|
|
400
473
|
`${this.maxFrameBytes} bytes.`);
|
|
401
474
|
}
|
|
@@ -1,2 +1,11 @@
|
|
|
1
|
+
import type { DirectionIndex, PairKey } from "../internal-types.js";
|
|
1
2
|
import type { MappingLoaderDeps, MappingLoaderResult } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* `maxEntryBytes` reuses the same ceiling as nested-jar extraction
|
|
5
|
+
* ({@link loadMaxNestedJarEntryBytes}): the downloaded jar is itself
|
|
6
|
+
* download-size-capped, but a single `.tiny`/`.tinyv2` entry inside it is
|
|
7
|
+
* decompressed in full before parsing, so an entry with a small compressed
|
|
8
|
+
* size and a huge inflated size (zip-bomb style) must still be bounded here.
|
|
9
|
+
*/
|
|
10
|
+
export declare function parseTinyFromJar(jarPath: string, maxEntryBytes?: number): Promise<Map<PairKey, DirectionIndex>>;
|
|
2
11
|
export declare function loadTinyPairsFromMaven(deps: MappingLoaderDeps, version: string): Promise<MappingLoaderResult>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { defaultDownloadPath, downloadToCache } from "../../repo-downloader.js";
|
|
2
|
+
import { loadMaxNestedJarEntryBytes } from "../../source/nested-jars.js";
|
|
2
3
|
import { collectMatchedJarEntriesAsUtf8 } from "../../source-jar-reader.js";
|
|
3
4
|
import { mergeDirectionIndexes } from "../parsers/symbol-records.js";
|
|
4
5
|
import { parseTinyMappingsInto } from "../parsers/tiny.js";
|
|
@@ -27,8 +28,15 @@ async function fetchYarnCoordinates(fetchFn, repoBase, version) {
|
|
|
27
28
|
return [version];
|
|
28
29
|
}
|
|
29
30
|
}
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
/**
|
|
32
|
+
* `maxEntryBytes` reuses the same ceiling as nested-jar extraction
|
|
33
|
+
* ({@link loadMaxNestedJarEntryBytes}): the downloaded jar is itself
|
|
34
|
+
* download-size-capped, but a single `.tiny`/`.tinyv2` entry inside it is
|
|
35
|
+
* decompressed in full before parsing, so an entry with a small compressed
|
|
36
|
+
* size and a huge inflated size (zip-bomb style) must still be bounded here.
|
|
37
|
+
*/
|
|
38
|
+
export async function parseTinyFromJar(jarPath, maxEntryBytes = loadMaxNestedJarEntryBytes()) {
|
|
39
|
+
const tinyEntries = (await collectMatchedJarEntriesAsUtf8(jarPath, (entry) => entry.toLowerCase().endsWith(".tiny") || entry.toLowerCase().endsWith(".tinyv2"), { continueOnError: true, maxBytes: maxEntryBytes })).sort((left, right) => left.filePath.localeCompare(right.filePath));
|
|
32
40
|
// Parsed straight into the shared accumulator: a parse-then-merge loop would
|
|
33
41
|
// hold each entry's full index alongside the accumulated one. `ensurePairIndex`
|
|
34
42
|
// + `addLookupEntries` union into what is already there, matching what
|
package/dist/repo-downloader.js
CHANGED
|
@@ -771,13 +771,24 @@ async function cachedBytesResult(url, destinationPath, sidecar, cacheStatus) {
|
|
|
771
771
|
* a zip reader opens it - so it must satisfy neither an immutable hit nor a
|
|
772
772
|
* stale-if-error fallback. Treating it as absent lets the next transfer replace
|
|
773
773
|
* it instead of pinning it forever.
|
|
774
|
+
*
|
|
775
|
+
* That equivalence stops at "missing", the same line {@link describeFileIfPresent}
|
|
776
|
+
* draws: only {@link isMissingFileError} answers a cache miss. A present-but-
|
|
777
|
+
* unreadable entry (EACCES from a locked-down cache directory, EISDIR, EIO)
|
|
778
|
+
* propagates instead of collapsing to 0, because this is the first stat this
|
|
779
|
+
* module makes on the path - collapsing it here would let `resolveCachedDownload`
|
|
780
|
+
* read "no cached bytes" and fall through to a live transfer without ever
|
|
781
|
+
* reaching the read path that already reports this failure correctly.
|
|
774
782
|
*/
|
|
775
783
|
function cachedByteCount(filePath) {
|
|
776
784
|
try {
|
|
777
785
|
return statSync(filePath).size;
|
|
778
786
|
}
|
|
779
|
-
catch {
|
|
780
|
-
|
|
787
|
+
catch (error) {
|
|
788
|
+
if (isMissingFileError(error)) {
|
|
789
|
+
return 0;
|
|
790
|
+
}
|
|
791
|
+
throw error;
|
|
781
792
|
}
|
|
782
793
|
}
|
|
783
794
|
/**
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type JavaSourceScan } from "../source-jar-reader.js";
|
|
1
2
|
import type { SourceService } from "../source-service.js";
|
|
2
3
|
import type { ArtifactContentsSummary, ProbeMinecraftArtifactInput, ProbeMinecraftArtifactOutput, ResolveArtifactInput, ResolveArtifactOutput } from "../source-service.js";
|
|
3
4
|
import type { AccessTransformerNamespace, ArtifactProvenance, ArtifactScope, ArtifactTargetKind, ResolvedSourceArtifact, RuntimeLoader, RuntimeValidationProvenance, SourceMapping } from "../types.js";
|
|
@@ -143,6 +144,19 @@ export declare function hasLoaderRuntimeVersionToken(path: string, mcVersion: st
|
|
|
143
144
|
* of the path is the version the jar was built for.
|
|
144
145
|
*/
|
|
145
146
|
export declare function inferRuntimeJarMinecraftVersion(path: string): string | undefined;
|
|
147
|
+
/**
|
|
148
|
+
* The Minecraft version a `kind: "jar"` target PROVES it is the unobfuscated
|
|
149
|
+
* runtime of, or undefined. The file path is never evidence: Loom and other
|
|
150
|
+
* tools lay jars out in ways that path heuristics have misread before.
|
|
151
|
+
*
|
|
152
|
+
* All three must hold: the walk met no `.java` entry, the jar ships
|
|
153
|
+
* `net/minecraft/SharedConstants.class`, and its root `version.json` parses with a
|
|
154
|
+
* string `id` that `isUnobfuscatedVersion` accepts. The id is put to that test
|
|
155
|
+
* only once the other two hold, so it is the id a Minecraft `version.json`
|
|
156
|
+
* declares, never a library's own release number. A Loom-mapped 1.x jar carries
|
|
157
|
+
* both signals too; its `1.x` id is what keeps it out. The path is never read.
|
|
158
|
+
*/
|
|
159
|
+
export declare function provenUnobfuscatedRuntimeJarVersion(scan: JavaSourceScan | undefined): string | undefined;
|
|
146
160
|
/**
|
|
147
161
|
* Loader a runtime jar belongs to, read from the tool-written part of its path.
|
|
148
162
|
*
|