@adhisang/minecraft-modding-mcp 6.2.0 → 7.0.0-rc.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 +71 -0
- package/README.md +21 -5
- package/dist/cache-policy.d.ts +71 -0
- package/dist/cache-policy.js +83 -0
- package/dist/cache-registry.js +6 -6
- package/dist/cli.js +74 -3
- package/dist/compat-stdio-transport.d.ts +1 -1
- package/dist/compat-stdio-transport.js +13 -1
- package/dist/config.d.ts +3 -0
- package/dist/config.js +8 -2
- package/dist/decompiler/vineflower.d.ts +1 -0
- package/dist/decompiler/vineflower.js +8 -5
- package/dist/entry-tools/analyze-mod-service.d.ts +70 -136
- package/dist/entry-tools/analyze-symbol-service.d.ts +112 -150
- package/dist/entry-tools/compare-minecraft-service.d.ts +59 -145
- package/dist/entry-tools/entry-tool-schema.d.ts +38 -4
- package/dist/entry-tools/entry-tool-schema.js +4 -1
- package/dist/entry-tools/inspect-minecraft/internal.d.ts +235 -799
- package/dist/entry-tools/inspect-minecraft/internal.js +65 -28
- package/dist/entry-tools/inspect-minecraft-service.d.ts +372 -1736
- package/dist/entry-tools/inspect-minecraft-service.js +1 -1
- package/dist/entry-tools/manage-cache-service.d.ts +81 -91
- package/dist/entry-tools/validate-project/cases/project-summary.d.ts +7 -7
- package/dist/entry-tools/validate-project-service.d.ts +164 -592
- package/dist/entry-tools/verify-mixin-target-service.d.ts +3 -19
- package/dist/era-classifier.d.ts +161 -0
- package/dist/era-classifier.js +292 -0
- package/dist/error-mapping.js +9 -2
- package/dist/index.d.ts +42 -4
- package/dist/index.js +637 -475
- package/dist/java-process.d.ts +2 -0
- package/dist/java-process.js +22 -2
- package/dist/json-rpc-framing.d.ts +77 -1
- package/dist/json-rpc-framing.js +249 -13
- package/dist/mapping/loaders/tiny-loom-selection.d.ts +88 -0
- package/dist/mapping/loaders/tiny-loom-selection.js +223 -0
- package/dist/mapping/loaders/tiny-loom.js +45 -33
- package/dist/mapping/loaders/tiny-maven.js +6 -11
- package/dist/mapping/parsers/tiny.d.ts +57 -0
- package/dist/mapping/parsers/tiny.js +99 -22
- package/dist/mapping-service.d.ts +19 -0
- package/dist/mapping-service.js +93 -9
- package/dist/mcp-helpers.d.ts +19 -2
- package/dist/mcp-helpers.js +48 -6
- package/dist/minecraft-explorer-service.d.ts +1 -1
- package/dist/mixin/types.d.ts +8 -0
- package/dist/mod-analyzer.js +7 -7
- package/dist/mod-decompile-service.js +1 -0
- package/dist/nbt/java-nbt-codec.js +12 -2
- package/dist/nbt/json-patch.js +14 -3
- package/dist/nbt/pipeline.js +40 -3
- package/dist/nbt/typed-json.js +26 -1
- package/dist/registration-adapter.d.ts +32 -0
- package/dist/registration-adapter.js +52 -0
- package/dist/request-context.d.ts +7 -0
- package/dist/request-context.js +9 -0
- package/dist/resources.d.ts +1 -1
- package/dist/resources.js +25 -19
- package/dist/server-identity.d.ts +27 -0
- package/dist/server-identity.js +26 -0
- package/dist/source/access-validate.js +53 -0
- package/dist/source/artifact-resolver.d.ts +69 -1
- package/dist/source/artifact-resolver.js +215 -14
- package/dist/source/class-source.d.ts +22 -0
- package/dist/source/class-source.js +162 -29
- package/dist/source/did-you-mean.d.ts +12 -1
- package/dist/source/did-you-mean.js +6 -2
- package/dist/source/file-access.js +150 -46
- package/dist/source/indexer.js +1 -0
- package/dist/source/nested-jars.d.ts +19 -0
- package/dist/source/nested-jars.js +90 -21
- package/dist/source/shared-utils.d.ts +21 -0
- package/dist/source/shared-utils.js +23 -0
- package/dist/source-service.d.ts +12 -0
- package/dist/source-service.js +3 -0
- package/dist/stdio-supervisor.d.ts +357 -2
- package/dist/stdio-supervisor.js +1031 -80
- package/dist/storage/db.d.ts +2 -1
- package/dist/storage/db.js +15 -8
- package/dist/synthetic-decorator.d.ts +24 -0
- package/dist/synthetic-decorator.js +48 -0
- package/dist/tool-contract-manifest.js +1 -1
- package/dist/tool-guidance.d.ts +17 -1
- package/dist/tool-guidance.js +417 -13
- package/dist/tool-schema-registry.d.ts +2 -0
- package/dist/tool-schema-registry.js +4 -0
- package/dist/tool-schemas.d.ts +2214 -3915
- package/dist/tool-schemas.js +34 -7
- package/dist/types.d.ts +35 -0
- package/dist/v1-parity-schemas.d.ts +7 -0
- package/dist/v1-parity-schemas.js +5584 -0
- package/dist/version-diff-service.d.ts +33 -0
- package/dist/version-diff-service.js +148 -3
- package/dist/version-service.js +36 -14
- package/dist/warning-details.js +18 -1
- package/docs/README-ja.md +9 -3
- package/docs/tool-reference.md +267 -22
- package/package.json +12 -9
package/dist/java-process.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
1
2
|
export declare const MAX_STDIO_SNAPSHOT = 6240;
|
|
2
3
|
export interface JavaProcessOptions {
|
|
3
4
|
jarPath: string;
|
|
@@ -18,6 +19,7 @@ export declare function isAbsolutePath(value: string): boolean;
|
|
|
18
19
|
export declare function isOptionArg(value: string): boolean;
|
|
19
20
|
export declare function normalizeArgs(args: string[]): string[];
|
|
20
21
|
export declare function assertJavaAvailable(): Promise<void>;
|
|
22
|
+
export declare function resetJavaAvailabilityCacheForTests(spawnHook?: typeof spawn): void;
|
|
21
23
|
export declare function runJavaProcess(options: JavaProcessOptions): Promise<JavaProcessResult>;
|
|
22
24
|
export declare const javaRunner: {
|
|
23
25
|
run: (options: JavaProcessOptions) => Promise<JavaProcessResult>;
|
package/dist/java-process.js
CHANGED
|
@@ -3,6 +3,8 @@ import { createError, ERROR_CODES } from "./errors.js";
|
|
|
3
3
|
import { normalizePathForHost } from "./path-converter.js";
|
|
4
4
|
const JAVA_CHECK_TIMEOUT_MS = 2_000;
|
|
5
5
|
export const MAX_STDIO_SNAPSHOT = 6_240;
|
|
6
|
+
let javaAvailabilitySpawn = spawn;
|
|
7
|
+
let javaAvailabilityPromise;
|
|
6
8
|
export function limitStdio(text) {
|
|
7
9
|
if (text.length <= MAX_STDIO_SNAPSHOT) {
|
|
8
10
|
return text;
|
|
@@ -26,9 +28,9 @@ export function normalizeArgs(args) {
|
|
|
26
28
|
return arg;
|
|
27
29
|
});
|
|
28
30
|
}
|
|
29
|
-
|
|
31
|
+
function probeJavaAvailability() {
|
|
30
32
|
return new Promise((resolve, reject) => {
|
|
31
|
-
const proc =
|
|
33
|
+
const proc = javaAvailabilitySpawn("java", ["-version"], { stdio: "ignore" });
|
|
32
34
|
const timer = setTimeout(() => {
|
|
33
35
|
proc.kill();
|
|
34
36
|
reject(createError({
|
|
@@ -56,6 +58,24 @@ export async function assertJavaAvailable() {
|
|
|
56
58
|
});
|
|
57
59
|
});
|
|
58
60
|
}
|
|
61
|
+
export function assertJavaAvailable() {
|
|
62
|
+
if (javaAvailabilityPromise) {
|
|
63
|
+
return javaAvailabilityPromise;
|
|
64
|
+
}
|
|
65
|
+
const probePromise = Promise.resolve().then(() => probeJavaAvailability());
|
|
66
|
+
const cachedPromise = probePromise.catch((error) => {
|
|
67
|
+
if (javaAvailabilityPromise === cachedPromise) {
|
|
68
|
+
javaAvailabilityPromise = undefined;
|
|
69
|
+
}
|
|
70
|
+
throw error;
|
|
71
|
+
});
|
|
72
|
+
javaAvailabilityPromise = cachedPromise;
|
|
73
|
+
return cachedPromise;
|
|
74
|
+
}
|
|
75
|
+
export function resetJavaAvailabilityCacheForTests(spawnHook = spawn) {
|
|
76
|
+
javaAvailabilityPromise = undefined;
|
|
77
|
+
javaAvailabilitySpawn = spawnHook;
|
|
78
|
+
}
|
|
59
79
|
export function runJavaProcess(options) {
|
|
60
80
|
const { jarPath, args, cwd, timeoutMs = 120_000, maxMemoryMb, minMemoryMb, normalizePathArgs = false } = options;
|
|
61
81
|
const jvmArgs = [];
|
|
@@ -1,18 +1,75 @@
|
|
|
1
|
-
import { type JSONRPCMessage } from "@modelcontextprotocol/
|
|
1
|
+
import { type JSONRPCMessage } from "@modelcontextprotocol/server";
|
|
2
2
|
export type FramingMode = "unknown" | "line" | "content-length";
|
|
3
3
|
export type ConcreteFramingMode = Exclude<FramingMode, "unknown">;
|
|
4
4
|
export type ParsedJsonRpcFrame = {
|
|
5
5
|
message: JSONRPCMessage;
|
|
6
6
|
mode: ConcreteFramingMode;
|
|
7
7
|
};
|
|
8
|
+
/**
|
|
9
|
+
* A framing violation the reader cannot provably recover from.
|
|
10
|
+
*
|
|
11
|
+
* The reader's framing invariant is: after ANY framing violation it either
|
|
12
|
+
* provably resynchronizes — resuming at a byte position the peer itself
|
|
13
|
+
* delimited — or it terminates the session with a diagnostic. It must never
|
|
14
|
+
* silently consume subsequent valid frames, and it must never wait on bytes
|
|
15
|
+
* an untrusted declared length says are coming.
|
|
16
|
+
*
|
|
17
|
+
* Recoverable violations (plain `Error`, reader keeps running):
|
|
18
|
+
* - a line-delimited frame that is oversized or unparseable — the newline
|
|
19
|
+
* that terminates it is a delimiter the reader can prove,
|
|
20
|
+
* - a Content-Length header block carrying no usable length at all
|
|
21
|
+
* (`Content-Length: nope`, a missing header, a malformed header line) —
|
|
22
|
+
* the CRLFCRLF boundary delimits the header block and no body length was
|
|
23
|
+
* ever declared, so only the header block is consumed,
|
|
24
|
+
* - an oversized Content-Length frame whose complete declared body is ALREADY
|
|
25
|
+
* buffered — dropping exactly those bytes lands on the byte the peer named
|
|
26
|
+
* as the next frame's first,
|
|
27
|
+
* - a Content-Length body that is valid JSON but not a valid JSON-RPC message
|
|
28
|
+
* — valid JSON of exactly the declared length proves the boundary was
|
|
29
|
+
* right, so this is a message-level error, not a framing one.
|
|
30
|
+
*
|
|
31
|
+
* Fatal violations (this class, reader stops permanently):
|
|
32
|
+
* - an oversized Content-Length whose declared body has NOT fully arrived
|
|
33
|
+
* (waiting on it is what let a single unanswerable header wedge the
|
|
34
|
+
* transport for the process lifetime),
|
|
35
|
+
* - a Content-Length body that is not valid JSON — under-declaration,
|
|
36
|
+
* over-declaration and an honestly-framed bad body are indistinguishable,
|
|
37
|
+
* and the first two have already desynchronized the stream,
|
|
38
|
+
* - duplicate Content-Length headers — the body length is ambiguous,
|
|
39
|
+
* - a Content-Length header block that never terminates within the header
|
|
40
|
+
* limit — there is no delimiter left to resynchronize on.
|
|
41
|
+
*/
|
|
42
|
+
export declare class JsonRpcFramingFatalError extends Error {
|
|
43
|
+
readonly framingFatal = true;
|
|
44
|
+
constructor(message: string);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Whether an error reported through `processChunk`'s `onError` handler ends
|
|
48
|
+
* the session. Every transport that owns a {@link JsonRpcFrameReader} MUST
|
|
49
|
+
* check this and tear its session down: the reader has stopped accepting
|
|
50
|
+
* input, so ignoring the signal would leave a silently deaf transport.
|
|
51
|
+
*/
|
|
52
|
+
export declare function isJsonRpcFramingFatalError(error: unknown): error is JsonRpcFramingFatalError;
|
|
53
|
+
export declare function loadMaxFrameBytes(value?: string | undefined): number;
|
|
8
54
|
export declare function encodeJsonRpcMessage(message: JSONRPCMessage, mode: ConcreteFramingMode): Buffer;
|
|
9
55
|
export declare class JsonRpcFrameReader {
|
|
56
|
+
private readonly maxFrameBytes;
|
|
10
57
|
private mode;
|
|
11
58
|
private buffer;
|
|
12
59
|
private pendingChunks;
|
|
13
60
|
private pendingBytes;
|
|
14
61
|
private awaitedFrameEnd;
|
|
62
|
+
private fatal;
|
|
63
|
+
constructor(options?: {
|
|
64
|
+
maxFrameBytes?: number;
|
|
65
|
+
});
|
|
15
66
|
get currentMode(): FramingMode;
|
|
67
|
+
/**
|
|
68
|
+
* Whether an unrecoverable framing violation has stopped this reader. No
|
|
69
|
+
* further input is examined and no further frame is ever emitted until
|
|
70
|
+
* `reset()` or `clear()` explicitly re-arms it.
|
|
71
|
+
*/
|
|
72
|
+
get isFatal(): boolean;
|
|
16
73
|
reset(): void;
|
|
17
74
|
clear(): void;
|
|
18
75
|
processChunk(chunk: Buffer, handlers: {
|
|
@@ -20,6 +77,25 @@ export declare class JsonRpcFrameReader {
|
|
|
20
77
|
onError: (error: Error) => void;
|
|
21
78
|
}): void;
|
|
22
79
|
private canCompleteFrame;
|
|
80
|
+
private rejectOversizedIncompleteInput;
|
|
81
|
+
/**
|
|
82
|
+
* Rejects a Content-Length frame whose declared body can never be accepted
|
|
83
|
+
* (over the frame limit, or behind an over-limit header block).
|
|
84
|
+
*
|
|
85
|
+
* The body is skipped ONLY when every declared byte is already buffered. In
|
|
86
|
+
* that case the skip is a bounded operation on bytes in hand and it resumes
|
|
87
|
+
* at exactly the offset the peer itself named as the next frame's first
|
|
88
|
+
* byte — a resynchronization the reader can prove without extending trust to
|
|
89
|
+
* a single unarrived byte.
|
|
90
|
+
*
|
|
91
|
+
* When the body has NOT fully arrived the reader must not wait for it: the
|
|
92
|
+
* declared length is attacker-controlled, and arming a countdown with it is
|
|
93
|
+
* precisely what let a 29-byte header (`Content-Length: 999999999\r\n\r\n`
|
|
94
|
+
* with no body) silently swallow every later frame for the process lifetime.
|
|
95
|
+
* There is no delimiter to scan forward to either — an arbitrary binary body
|
|
96
|
+
* offers none — so the session is terminated instead.
|
|
97
|
+
*/
|
|
98
|
+
private rejectDeclaredBody;
|
|
23
99
|
private detectMode;
|
|
24
100
|
private readLineDelimitedMessage;
|
|
25
101
|
private readContentLengthMessage;
|
package/dist/json-rpc-framing.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { parseJSONRPCMessage } from "@modelcontextprotocol/server";
|
|
2
|
+
const DEFAULT_MAX_FRAME_BYTES = 64 * 1024 * 1024;
|
|
3
|
+
const MIN_MAX_FRAME_BYTES = 1024 * 1024;
|
|
4
|
+
const MAX_CONTENT_LENGTH_HEADER_BYTES = 8 * 1024;
|
|
2
5
|
function findHeaderBoundary(buffer) {
|
|
3
6
|
const crlfBoundary = buffer.indexOf("\r\n\r\n");
|
|
4
7
|
if (crlfBoundary !== -1) {
|
|
@@ -11,11 +14,71 @@ function findHeaderBoundary(buffer) {
|
|
|
11
14
|
return undefined;
|
|
12
15
|
}
|
|
13
16
|
function parseJsonRpcMessage(json) {
|
|
14
|
-
return
|
|
17
|
+
return parseJSONRPCMessage(JSON.parse(json));
|
|
15
18
|
}
|
|
16
19
|
function asError(value) {
|
|
17
20
|
return value instanceof Error ? value : new Error(String(value));
|
|
18
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* A framing violation the reader cannot provably recover from.
|
|
24
|
+
*
|
|
25
|
+
* The reader's framing invariant is: after ANY framing violation it either
|
|
26
|
+
* provably resynchronizes — resuming at a byte position the peer itself
|
|
27
|
+
* delimited — or it terminates the session with a diagnostic. It must never
|
|
28
|
+
* silently consume subsequent valid frames, and it must never wait on bytes
|
|
29
|
+
* an untrusted declared length says are coming.
|
|
30
|
+
*
|
|
31
|
+
* Recoverable violations (plain `Error`, reader keeps running):
|
|
32
|
+
* - a line-delimited frame that is oversized or unparseable — the newline
|
|
33
|
+
* that terminates it is a delimiter the reader can prove,
|
|
34
|
+
* - a Content-Length header block carrying no usable length at all
|
|
35
|
+
* (`Content-Length: nope`, a missing header, a malformed header line) —
|
|
36
|
+
* the CRLFCRLF boundary delimits the header block and no body length was
|
|
37
|
+
* ever declared, so only the header block is consumed,
|
|
38
|
+
* - an oversized Content-Length frame whose complete declared body is ALREADY
|
|
39
|
+
* buffered — dropping exactly those bytes lands on the byte the peer named
|
|
40
|
+
* as the next frame's first,
|
|
41
|
+
* - a Content-Length body that is valid JSON but not a valid JSON-RPC message
|
|
42
|
+
* — valid JSON of exactly the declared length proves the boundary was
|
|
43
|
+
* right, so this is a message-level error, not a framing one.
|
|
44
|
+
*
|
|
45
|
+
* Fatal violations (this class, reader stops permanently):
|
|
46
|
+
* - an oversized Content-Length whose declared body has NOT fully arrived
|
|
47
|
+
* (waiting on it is what let a single unanswerable header wedge the
|
|
48
|
+
* transport for the process lifetime),
|
|
49
|
+
* - a Content-Length body that is not valid JSON — under-declaration,
|
|
50
|
+
* over-declaration and an honestly-framed bad body are indistinguishable,
|
|
51
|
+
* and the first two have already desynchronized the stream,
|
|
52
|
+
* - duplicate Content-Length headers — the body length is ambiguous,
|
|
53
|
+
* - a Content-Length header block that never terminates within the header
|
|
54
|
+
* limit — there is no delimiter left to resynchronize on.
|
|
55
|
+
*/
|
|
56
|
+
export class JsonRpcFramingFatalError extends Error {
|
|
57
|
+
framingFatal = true;
|
|
58
|
+
constructor(message) {
|
|
59
|
+
super(message);
|
|
60
|
+
this.name = "JsonRpcFramingFatalError";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Whether an error reported through `processChunk`'s `onError` handler ends
|
|
65
|
+
* the session. Every transport that owns a {@link JsonRpcFrameReader} MUST
|
|
66
|
+
* check this and tear its session down: the reader has stopped accepting
|
|
67
|
+
* input, so ignoring the signal would leave a silently deaf transport.
|
|
68
|
+
*/
|
|
69
|
+
export function isJsonRpcFramingFatalError(error) {
|
|
70
|
+
return error instanceof JsonRpcFramingFatalError;
|
|
71
|
+
}
|
|
72
|
+
export function loadMaxFrameBytes(value = process.env.MCP_MAX_FRAME_BYTES) {
|
|
73
|
+
if (!/^[0-9]+$/.test(value ?? "")) {
|
|
74
|
+
return DEFAULT_MAX_FRAME_BYTES;
|
|
75
|
+
}
|
|
76
|
+
const parsed = Number(value);
|
|
77
|
+
if (!Number.isSafeInteger(parsed)) {
|
|
78
|
+
return DEFAULT_MAX_FRAME_BYTES;
|
|
79
|
+
}
|
|
80
|
+
return Math.max(parsed, MIN_MAX_FRAME_BYTES);
|
|
81
|
+
}
|
|
19
82
|
export function encodeJsonRpcMessage(message, mode) {
|
|
20
83
|
const json = JSON.stringify(message);
|
|
21
84
|
return Buffer.from(mode === "content-length"
|
|
@@ -23,17 +86,31 @@ export function encodeJsonRpcMessage(message, mode) {
|
|
|
23
86
|
: `${json}\n`, "utf8");
|
|
24
87
|
}
|
|
25
88
|
export class JsonRpcFrameReader {
|
|
89
|
+
maxFrameBytes;
|
|
26
90
|
mode = "unknown";
|
|
27
91
|
buffer = Buffer.alloc(0);
|
|
28
92
|
pendingChunks = [];
|
|
29
93
|
pendingBytes = 0;
|
|
30
94
|
awaitedFrameEnd = -1;
|
|
95
|
+
fatal = false;
|
|
96
|
+
constructor(options = {}) {
|
|
97
|
+
this.maxFrameBytes = options.maxFrameBytes ?? loadMaxFrameBytes();
|
|
98
|
+
}
|
|
31
99
|
get currentMode() {
|
|
32
100
|
return this.mode;
|
|
33
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Whether an unrecoverable framing violation has stopped this reader. No
|
|
104
|
+
* further input is examined and no further frame is ever emitted until
|
|
105
|
+
* `reset()` or `clear()` explicitly re-arms it.
|
|
106
|
+
*/
|
|
107
|
+
get isFatal() {
|
|
108
|
+
return this.fatal;
|
|
109
|
+
}
|
|
34
110
|
reset() {
|
|
35
111
|
this.mode = "unknown";
|
|
36
112
|
this.awaitedFrameEnd = -1;
|
|
113
|
+
this.fatal = false;
|
|
37
114
|
}
|
|
38
115
|
clear() {
|
|
39
116
|
this.mode = "unknown";
|
|
@@ -41,9 +118,10 @@ export class JsonRpcFrameReader {
|
|
|
41
118
|
this.pendingChunks = [];
|
|
42
119
|
this.pendingBytes = 0;
|
|
43
120
|
this.awaitedFrameEnd = -1;
|
|
121
|
+
this.fatal = false;
|
|
44
122
|
}
|
|
45
123
|
processChunk(chunk, handlers) {
|
|
46
|
-
if (chunk.length === 0) {
|
|
124
|
+
if (chunk.length === 0 || this.fatal) {
|
|
47
125
|
return;
|
|
48
126
|
}
|
|
49
127
|
this.pendingChunks.push(chunk);
|
|
@@ -56,6 +134,7 @@ export class JsonRpcFrameReader {
|
|
|
56
134
|
this.pendingBytes = 0;
|
|
57
135
|
while (true) {
|
|
58
136
|
try {
|
|
137
|
+
this.rejectOversizedIncompleteInput();
|
|
59
138
|
if (this.mode === "unknown") {
|
|
60
139
|
const detected = this.detectMode();
|
|
61
140
|
if (!detected) {
|
|
@@ -80,16 +159,91 @@ export class JsonRpcFrameReader {
|
|
|
80
159
|
});
|
|
81
160
|
}
|
|
82
161
|
catch (caughtError) {
|
|
162
|
+
const error = asError(caughtError);
|
|
83
163
|
this.mode = "unknown";
|
|
84
|
-
|
|
164
|
+
this.awaitedFrameEnd = -1;
|
|
165
|
+
if (error instanceof JsonRpcFramingFatalError) {
|
|
166
|
+
// Terminal: drop everything buffered and refuse all further input so
|
|
167
|
+
// no byte after the violation can be mistaken for a frame. The
|
|
168
|
+
// transport owns the teardown.
|
|
169
|
+
this.fatal = true;
|
|
170
|
+
this.buffer = Buffer.alloc(0);
|
|
171
|
+
this.pendingChunks = [];
|
|
172
|
+
this.pendingBytes = 0;
|
|
173
|
+
handlers.onError(error);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
handlers.onError(error);
|
|
85
177
|
}
|
|
86
178
|
}
|
|
87
179
|
}
|
|
88
180
|
canCompleteFrame(chunk) {
|
|
181
|
+
const bufferedBytes = this.buffer.length + this.pendingBytes;
|
|
89
182
|
if (this.mode === "content-length" && this.awaitedFrameEnd >= 0) {
|
|
90
|
-
return
|
|
183
|
+
return bufferedBytes >= this.awaitedFrameEnd;
|
|
184
|
+
}
|
|
185
|
+
if (this.mode === "content-length" &&
|
|
186
|
+
bufferedBytes > MAX_CONTENT_LENGTH_HEADER_BYTES) {
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
return chunk.includes(0x0a) || bufferedBytes > this.maxFrameBytes;
|
|
190
|
+
}
|
|
191
|
+
rejectOversizedIncompleteInput() {
|
|
192
|
+
const headerBoundary = this.mode === "content-length" ? findHeaderBoundary(this.buffer) : undefined;
|
|
193
|
+
if (this.mode === "content-length" &&
|
|
194
|
+
!headerBoundary &&
|
|
195
|
+
this.buffer.length > MAX_CONTENT_LENGTH_HEADER_BYTES) {
|
|
196
|
+
// No header terminator anywhere in an over-limit header block: there is
|
|
197
|
+
// no delimiter left to resynchronize on (content-length mode has no
|
|
198
|
+
// newline delimiter, and a line frame at the head would already have
|
|
199
|
+
// switched the mode), so the session cannot continue.
|
|
200
|
+
throw new JsonRpcFramingFatalError(`Content-Length header is ${this.buffer.length} bytes with no header terminator, exceeding ` +
|
|
201
|
+
`the header limit of ${MAX_CONTENT_LENGTH_HEADER_BYTES} bytes; the stdio session is terminated.`);
|
|
202
|
+
}
|
|
203
|
+
if (this.buffer.length <= this.maxFrameBytes) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (this.mode === "content-length" && headerBoundary) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (this.mode !== "content-length" && this.buffer.includes(0x0a)) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const observedBytes = this.buffer.length;
|
|
213
|
+
const description = this.mode === "line" ? "Line-delimited JSON-RPC frame" : "Headerless JSON-RPC input";
|
|
214
|
+
this.buffer = Buffer.alloc(0);
|
|
215
|
+
throw new Error(`${description} is ${observedBytes} bytes, exceeding the configured frame limit of ` +
|
|
216
|
+
`${this.maxFrameBytes} bytes.`);
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Rejects a Content-Length frame whose declared body can never be accepted
|
|
220
|
+
* (over the frame limit, or behind an over-limit header block).
|
|
221
|
+
*
|
|
222
|
+
* The body is skipped ONLY when every declared byte is already buffered. In
|
|
223
|
+
* that case the skip is a bounded operation on bytes in hand and it resumes
|
|
224
|
+
* at exactly the offset the peer itself named as the next frame's first
|
|
225
|
+
* byte — a resynchronization the reader can prove without extending trust to
|
|
226
|
+
* a single unarrived byte.
|
|
227
|
+
*
|
|
228
|
+
* When the body has NOT fully arrived the reader must not wait for it: the
|
|
229
|
+
* declared length is attacker-controlled, and arming a countdown with it is
|
|
230
|
+
* precisely what let a 29-byte header (`Content-Length: 999999999\r\n\r\n`
|
|
231
|
+
* with no body) silently swallow every later frame for the process lifetime.
|
|
232
|
+
* There is no delimiter to scan forward to either — an arbitrary binary body
|
|
233
|
+
* offers none — so the session is terminated instead.
|
|
234
|
+
*/
|
|
235
|
+
rejectDeclaredBody(messageStart, contentLength, reason) {
|
|
236
|
+
const frameEnd = BigInt(messageStart) + contentLength;
|
|
237
|
+
if (BigInt(this.buffer.length) >= frameEnd) {
|
|
238
|
+
this.buffer = this.buffer.subarray(Number(frameEnd));
|
|
239
|
+
this.awaitedFrameEnd = -1;
|
|
240
|
+
this.mode = "unknown";
|
|
241
|
+
throw new Error(reason);
|
|
91
242
|
}
|
|
92
|
-
|
|
243
|
+
const arrivedBodyBytes = Math.max(0, this.buffer.length - messageStart);
|
|
244
|
+
throw new JsonRpcFramingFatalError(`${reason} Only ${arrivedBodyBytes} of the declared ${contentLength.toString()} body bytes have ` +
|
|
245
|
+
"arrived, so the reader cannot resynchronize without trusting bytes that may never be sent; " +
|
|
246
|
+
"the stdio session is terminated.");
|
|
93
247
|
}
|
|
94
248
|
detectMode() {
|
|
95
249
|
while (this.buffer.length > 0) {
|
|
@@ -129,14 +283,26 @@ export class JsonRpcFrameReader {
|
|
|
129
283
|
if (newlineIndex === -1) {
|
|
130
284
|
return undefined;
|
|
131
285
|
}
|
|
286
|
+
const lineBytes = newlineIndex > 0 && this.buffer[newlineIndex - 1] === 0x0d
|
|
287
|
+
? newlineIndex - 1
|
|
288
|
+
: newlineIndex;
|
|
289
|
+
if (lineBytes > this.maxFrameBytes) {
|
|
290
|
+
this.buffer = this.buffer.subarray(newlineIndex + 1);
|
|
291
|
+
throw new Error(`Line-delimited JSON-RPC frame is ${lineBytes} bytes, exceeding the configured ` +
|
|
292
|
+
`frame limit of ${this.maxFrameBytes} bytes.`);
|
|
293
|
+
}
|
|
132
294
|
const line = this.buffer.subarray(0, newlineIndex).toString("utf8").replace(/\r$/, "");
|
|
133
295
|
this.buffer = this.buffer.subarray(newlineIndex + 1);
|
|
134
296
|
if (line.trim().length === 0) {
|
|
135
297
|
continue;
|
|
136
298
|
}
|
|
137
299
|
if (/^\s*content-length\s*:/i.test(line)) {
|
|
138
|
-
|
|
139
|
-
|
|
300
|
+
// Always re-inject with CRLF: choosing the separator from the next
|
|
301
|
+
// buffered byte raced byte-granular chunking (the peer's \r may not
|
|
302
|
+
// have arrived yet, yielding a "\n\r\n" boundary findHeaderBoundary
|
|
303
|
+
// cannot see). With CRLF both peer styles stay recognizable:
|
|
304
|
+
// "...\r\n" + "\r\n…" → "\r\n\r\n", "...\r\n" + "\n…" → "\n\n".
|
|
305
|
+
this.buffer = Buffer.concat([Buffer.from(`${line}\r\n`, "utf8"), this.buffer]);
|
|
140
306
|
this.mode = "content-length";
|
|
141
307
|
return undefined;
|
|
142
308
|
}
|
|
@@ -145,6 +311,42 @@ export class JsonRpcFrameReader {
|
|
|
145
311
|
}
|
|
146
312
|
readContentLengthMessage() {
|
|
147
313
|
this.awaitedFrameEnd = -1;
|
|
314
|
+
// Skip blank separator lines between frames so the mid-stream mode check
|
|
315
|
+
// below sees the first byte of the next frame.
|
|
316
|
+
while (this.buffer.length > 0) {
|
|
317
|
+
if (this.buffer[0] === 0x0a) {
|
|
318
|
+
this.buffer = this.buffer.subarray(1);
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
if (this.buffer.length >= 2 && this.buffer[0] === 0x0d && this.buffer[1] === 0x0a) {
|
|
322
|
+
this.buffer = this.buffer.subarray(2);
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
// Mirror of the line→content-length switch in readLineDelimitedMessage:
|
|
328
|
+
// a JSON object or array opener can never begin a Content-Length header block,
|
|
329
|
+
// so this is a line-delimited frame arriving after a Content-Length frame.
|
|
330
|
+
// Arrays are re-dispatched only to surface their JSON-RPC schema error, not
|
|
331
|
+
// accepted as batch messages. Peek past leading whitespace (pure line mode
|
|
332
|
+
// tolerates it: blank lines are skipped and JSON.parse accepts a
|
|
333
|
+
// whitespace-prefixed line) without consuming it, then switch modes and let
|
|
334
|
+
// processChunk re-dispatch the buffered bytes, so every frame is delivered
|
|
335
|
+
// with its own true mode.
|
|
336
|
+
let probeIndex = 0;
|
|
337
|
+
while (probeIndex < this.buffer.length &&
|
|
338
|
+
(this.buffer[probeIndex] === 0x20 ||
|
|
339
|
+
this.buffer[probeIndex] === 0x09 ||
|
|
340
|
+
this.buffer[probeIndex] === 0x0d ||
|
|
341
|
+
this.buffer[probeIndex] === 0x0a)) {
|
|
342
|
+
probeIndex += 1;
|
|
343
|
+
}
|
|
344
|
+
if (probeIndex < this.buffer.length &&
|
|
345
|
+
(this.buffer[probeIndex] === 0x7b /* '{' */ ||
|
|
346
|
+
this.buffer[probeIndex] === 0x5b /* '[' */)) {
|
|
347
|
+
this.mode = "line";
|
|
348
|
+
return undefined;
|
|
349
|
+
}
|
|
148
350
|
const headerBoundary = findHeaderBoundary(this.buffer);
|
|
149
351
|
if (!headerBoundary) {
|
|
150
352
|
return undefined;
|
|
@@ -164,12 +366,19 @@ export class JsonRpcFrameReader {
|
|
|
164
366
|
const headerName = headerLine.slice(0, separatorIndex).trim().toLowerCase();
|
|
165
367
|
const headerValue = headerLine.slice(separatorIndex + 1).trim();
|
|
166
368
|
if (headerName === "content-length") {
|
|
167
|
-
|
|
168
|
-
|
|
369
|
+
if (contentLength !== undefined) {
|
|
370
|
+
// Two declarations, no way to tell which delimits the body: the
|
|
371
|
+
// classic frame-smuggling shape. Last-wins would hand an attacker
|
|
372
|
+
// the choice of where the reader thinks this frame ends.
|
|
373
|
+
throw new JsonRpcFramingFatalError(`Duplicate Content-Length header (${contentLength.toString()} then ${headerValue}): the ` +
|
|
374
|
+
"declared body length is ambiguous, so the reader cannot determine where this frame " +
|
|
375
|
+
"ends; the stdio session is terminated.");
|
|
376
|
+
}
|
|
377
|
+
if (!/^[0-9]+$/.test(headerValue)) {
|
|
169
378
|
this.buffer = this.buffer.subarray(headerBoundary.index + headerBoundary.delimiterBytes);
|
|
170
379
|
throw new Error(`Invalid Content-Length header value: ${headerValue}`);
|
|
171
380
|
}
|
|
172
|
-
contentLength =
|
|
381
|
+
contentLength = BigInt(headerValue);
|
|
173
382
|
}
|
|
174
383
|
}
|
|
175
384
|
if (contentLength === undefined) {
|
|
@@ -177,14 +386,41 @@ export class JsonRpcFrameReader {
|
|
|
177
386
|
throw new Error("Missing Content-Length header.");
|
|
178
387
|
}
|
|
179
388
|
const messageStart = headerBoundary.index + headerBoundary.delimiterBytes;
|
|
180
|
-
|
|
389
|
+
if (contentLength > BigInt(this.maxFrameBytes)) {
|
|
390
|
+
this.rejectDeclaredBody(messageStart, contentLength, `Content-Length ${contentLength.toString()} exceeds the configured frame limit of ` +
|
|
391
|
+
`${this.maxFrameBytes} bytes.`);
|
|
392
|
+
}
|
|
393
|
+
if (messageStart > MAX_CONTENT_LENGTH_HEADER_BYTES) {
|
|
394
|
+
this.rejectDeclaredBody(messageStart, contentLength, `Content-Length header is ${messageStart} bytes, exceeding the header limit of ` +
|
|
395
|
+
`${MAX_CONTENT_LENGTH_HEADER_BYTES} bytes.`);
|
|
396
|
+
}
|
|
397
|
+
const frameEnd = messageStart + Number(contentLength);
|
|
181
398
|
if (this.buffer.length < frameEnd) {
|
|
182
399
|
this.awaitedFrameEnd = frameEnd;
|
|
183
400
|
return undefined;
|
|
184
401
|
}
|
|
185
402
|
const body = this.buffer.subarray(messageStart, frameEnd).toString("utf8");
|
|
186
403
|
this.buffer = this.buffer.subarray(frameEnd);
|
|
187
|
-
|
|
404
|
+
let payload;
|
|
405
|
+
try {
|
|
406
|
+
payload = JSON.parse(body);
|
|
407
|
+
}
|
|
408
|
+
catch (parseError) {
|
|
409
|
+
// The declared length is the ONLY delimiter a Content-Length frame has,
|
|
410
|
+
// and it just failed its one verification: bytes cut at that offset are
|
|
411
|
+
// not a JSON value. An under-declared length (tail garbage left in the
|
|
412
|
+
// buffer, which used to corrupt the NEXT frame), an over-declared length
|
|
413
|
+
// (the next frame already swallowed into this body) and an honestly
|
|
414
|
+
// framed but malformed body are indistinguishable here — and the first
|
|
415
|
+
// two have already desynchronized the stream. Line framing keeps its
|
|
416
|
+
// recoverable parse errors; its newline proves the boundary.
|
|
417
|
+
throw new JsonRpcFramingFatalError(`Content-Length frame body of ${Number(contentLength)} bytes is not valid JSON ` +
|
|
418
|
+
`(${asError(parseError).message}); the declared length cannot be trusted to delimit the ` +
|
|
419
|
+
"next frame, so the stdio session is terminated.");
|
|
420
|
+
}
|
|
421
|
+
// Valid JSON of exactly the declared length: the frame boundary is proven,
|
|
422
|
+
// so a JSON-RPC schema violation is an ordinary message-level error.
|
|
423
|
+
return parseJSONRPCMessage(payload);
|
|
188
424
|
}
|
|
189
425
|
}
|
|
190
426
|
//# sourceMappingURL=json-rpc-framing.js.map
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { SourceMapping } from "../../types.js";
|
|
2
|
+
import type { PairKey } from "../internal-types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Bytes of JS heap one index slot costs, measured on Minecraft 1.21.10 loom
|
|
5
|
+
* mappings (2,080 MB peak / 6,755,994 slots ~= 323 B; the Mojang proguard index
|
|
6
|
+
* lands at 330 B). Rounded up so the derived budget errs toward stopping early.
|
|
7
|
+
*/
|
|
8
|
+
export declare const HEAP_BYTES_PER_INDEX_ENTRY = 340;
|
|
9
|
+
/**
|
|
10
|
+
* How many index slots one Loom tiny load may accumulate before it stops and
|
|
11
|
+
* warns instead of exhausting the heap.
|
|
12
|
+
*
|
|
13
|
+
* The default is derived from the live V8 heap limit, so raising
|
|
14
|
+
* `--max-old-space-size` raises the budget automatically. `MCP_LOOM_TINY_MAX_INDEX_ENTRIES`
|
|
15
|
+
* overrides it outright.
|
|
16
|
+
*/
|
|
17
|
+
export declare function resolveTinyIndexEntryBudget(value?: string | undefined, heapStats?: {
|
|
18
|
+
heap_size_limit: number;
|
|
19
|
+
used_heap_size: number;
|
|
20
|
+
}): number;
|
|
21
|
+
export type TinyFileCandidate = {
|
|
22
|
+
path: string;
|
|
23
|
+
bytes: number;
|
|
24
|
+
/**
|
|
25
|
+
* Normalized first namespace column: the coordinates every descriptor in the file
|
|
26
|
+
* uses. `official` and `obfuscated` normalize to the same value.
|
|
27
|
+
*/
|
|
28
|
+
descriptorNamespace: SourceMapping | string;
|
|
29
|
+
namespaces: SourceMapping[];
|
|
30
|
+
};
|
|
31
|
+
export type TinySelection = {
|
|
32
|
+
/** Files to merge, in the order they should be merged. */
|
|
33
|
+
selected: TinyFileCandidate[];
|
|
34
|
+
/** Files whose bytes duplicate an already-selected file exactly. */
|
|
35
|
+
duplicateOf: Map<string, string>;
|
|
36
|
+
/**
|
|
37
|
+
* Files skipped because they restate namespace pairs already covered by a
|
|
38
|
+
* selected file while declaring their descriptors in a different namespace.
|
|
39
|
+
*/
|
|
40
|
+
descriptorConflicts: TinyFileCandidate[];
|
|
41
|
+
/** Files whose header is not a usable tiny v2 header. */
|
|
42
|
+
unsupported: string[];
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Read just the first line of a tiny file and derive its namespace layout.
|
|
46
|
+
* Returns `undefined` for anything {@link parseTinyHeader} rejects, so the body
|
|
47
|
+
* of an unusable file is never read.
|
|
48
|
+
*/
|
|
49
|
+
export declare function readTinyFileCandidate(path: string): Promise<TinyFileCandidate | undefined>;
|
|
50
|
+
/** Every ordered namespace pair a candidate's rows would populate. */
|
|
51
|
+
export declare function candidatePairKeys(candidate: TinyFileCandidate): PairKey[];
|
|
52
|
+
/**
|
|
53
|
+
* Richest first: more namespaces beats fewer; then a file whose descriptors are
|
|
54
|
+
* written in obfuscated coordinates beats one that is not, because that is the
|
|
55
|
+
* form callers query with and the form every other tiny source this server reads
|
|
56
|
+
* (Fabric intermediary and yarn artifacts, the Maven loader) produces; then larger
|
|
57
|
+
* beats smaller; then path order so the result never depends on filesystem
|
|
58
|
+
* enumeration order.
|
|
59
|
+
*
|
|
60
|
+
* The descriptor-coordinate rule also decides which rendering becomes the one the
|
|
61
|
+
* descriptor-conflict filter measures the rest against, so it must come before size:
|
|
62
|
+
* Loom's `mappings-base.tiny` is written with `intermediary` first AND is the larger
|
|
63
|
+
* file, and letting it win would leave the whole index carrying intermediary-coordinate
|
|
64
|
+
* descriptors.
|
|
65
|
+
*/
|
|
66
|
+
export declare function compareCandidates(left: TinyFileCandidate, right: TinyFileCandidate): number;
|
|
67
|
+
/**
|
|
68
|
+
* Decide which of the discovered tiny files to merge.
|
|
69
|
+
*
|
|
70
|
+
* Loom keeps several renderings of one layered mapping set side by side in one
|
|
71
|
+
* output directory: the final `mappings.tiny`, a `-mojang` rendering with an extra
|
|
72
|
+
* column, `-migrated` renderings per loader build, and a `mappings-base.tiny`
|
|
73
|
+
* written with the namespace columns rotated. Two filters apply:
|
|
74
|
+
*
|
|
75
|
+
* 1. Byte-identical files are collapsed. Loom copies the same rendering into every
|
|
76
|
+
* layered variant directory, so re-reading them is pure repeated work.
|
|
77
|
+
* 2. A file is dropped as an alternative rendering when ALL of the following hold:
|
|
78
|
+
* it declares its descriptors in a different namespace than the rendering
|
|
79
|
+
* already chosen, it sits in a directory that already yielded a selected file,
|
|
80
|
+
* and every namespace pair it carries is already covered. Tiny v2 writes each
|
|
81
|
+
* descriptor once, in the file's first namespace; merging an `intermediary`-first
|
|
82
|
+
* rendering into an `official`-first index registers every method a second time
|
|
83
|
+
* under an incompatible descriptor, which turns unique lookups into ambiguous
|
|
84
|
+
* ones. All three conditions are required so that a genuinely different mapping
|
|
85
|
+
* file — a standalone yarn tiny, a cache in another Gradle root — is never
|
|
86
|
+
* mistaken for a rendering of data already loaded.
|
|
87
|
+
*/
|
|
88
|
+
export declare function selectTinyFiles(paths: string[]): Promise<TinySelection>;
|