/* * Copyright 2025 the original author or authors. *

* Licensed under the Moderne Source Available License (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at *

* https://docs.moderne.io/licensing/moderne-source-available-license *

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import {constants} from "buffer"; import type {Message, MessageReaderOptions} from "vscode-jsonrpc/node"; /** * The content-type decoder shape vscode-jsonrpc's {@link MessageReaderOptions} accepts. Derived * from the reader options rather than deep-imported, because the concrete `ContentTypeDecoder` * type is not re-exported from the package entrypoint. */ export type JsonRpcContentTypeDecoder = NonNullable; const TAB = 0x09, NEWLINE = 0x0a, CARRIAGE_RETURN = 0x0d, SPACE = 0x20; const QUOTE = 0x22, PLUS = 0x2b, COMMA = 0x2c, MINUS = 0x2d, PERIOD = 0x2e; const ZERO = 0x30, NINE = 0x39, COLON = 0x3a, UPPER_E = 0x45; const OPEN_BRACKET = 0x5b, BACKSLASH = 0x5c, CLOSE_BRACKET = 0x5d; const LOWER_A = 0x61, LOWER_E = 0x65, LOWER_F = 0x66, LOWER_N = 0x6e, LOWER_T = 0x74, LOWER_Z = 0x7a; const OPEN_BRACE = 0x7b, CLOSE_BRACE = 0x7d; function isNumberByte(c: number): boolean { return (c >= ZERO && c <= NINE) || c === MINUS || c === PLUS || c === PERIOD || c === LOWER_E || c === UPPER_E; } interface Frame { readonly array?: unknown[]; readonly object?: Record; key?: string; } /** * Parses JSON directly from its UTF-8 bytes, never materializing the whole document as one JS * string. Structural punctuation is recognized byte-by-byte, while every scalar and every object key * is handed to the engine's `JSON.parse` as its own small slice — so escaping, number formatting and * literal validation match it exactly. This is the mirror of how `jsonFragments` defers to * `JSON.stringify`, and it accepts precisely the grammar `JSON.parse` accepts. * * Byte-level scanning is safe for the ASCII-compatible charsets vscode-jsonrpc supports (`utf-8`, * `ascii`): a UTF-8 continuation byte is always >= 0x80, so it can never be mistaken for a quote, * backslash, or structural character. Containers are tracked on an explicit stack, so nesting depth * is bounded by heap rather than by the call stack. */ export function parseJsonBytes(buf: Buffer, charset: BufferEncoding): unknown { const len = buf.length; let i = 0; const fail = (what: string): never => { throw new SyntaxError(`${what} in JSON at position ${i}`); }; // Advance past insignificant whitespace and return the next byte, which must exist. const seek = (): number => { while (i < len) { const c = buf[i]; if (c !== SPACE && c !== TAB && c !== NEWLINE && c !== CARRIAGE_RETURN) { return c; } i++; } return fail("Unexpected end of JSON input"); }; const token = (start: number, end: number): unknown => JSON.parse(buf.toString(charset, start, end)); const readString = (): string => { const start = i++; // opening quote while (i < len) { const c = buf[i]; if (c === BACKSLASH) { i += 2; // the byte after a backslash is ASCII, so it can never be the closing quote } else if (c === QUOTE) { return token(start, ++i) as string; } else { i++; } } return fail("Unterminated string"); }; const readKey = (): string => { if (seek() !== QUOTE) { fail("Expected property name"); } const key = readString(); if (seek() !== COLON) { fail("Expected ':' after property name"); } i++; return key; }; // JSON.parse gives __proto__ an own data property rather than setting the prototype. const setProperty = (object: Record, key: string, value: unknown): void => { if (key === "__proto__") { Object.defineProperty(object, key, {value, writable: true, enumerable: true, configurable: true}); } else { object[key] = value; } }; const parseValue = (): unknown => { const stack: Frame[] = []; let value: unknown; nextValue: for (; ;) { const c = seek(); if (c === OPEN_BRACE) { i++; const frame: Frame = {object: {}}; if (seek() === CLOSE_BRACE) { i++; value = frame.object; } else { frame.key = readKey(); stack.push(frame); continue nextValue; } } else if (c === OPEN_BRACKET) { i++; const frame: Frame = {array: []}; if (seek() === CLOSE_BRACKET) { i++; value = frame.array; } else { stack.push(frame); continue nextValue; } } else if (c === QUOTE) { value = readString(); } else if (c === LOWER_T || c === LOWER_F || c === LOWER_N) { const start = i; while (i < len && buf[i] >= LOWER_A && buf[i] <= LOWER_Z) { i++; } value = token(start, i); // true | false | null, validated by JSON.parse } else { const start = i; while (i < len && isNumberByte(buf[i])) { i++; } if (i === start) { fail(`Unexpected token ${JSON.stringify(buf.toString(charset, i, i + 1))}`); } value = token(start, i); } // Attach the finished value to its container, closing containers as they end. for (; ;) { const frame = stack[stack.length - 1]; if (frame === undefined) { return value; } if (frame.array !== undefined) { frame.array.push(value); const c = seek(); if (c === COMMA) { i++; continue nextValue; } if (c !== CLOSE_BRACKET) { fail("Expected ',' or ']' after array element"); } i++; value = frame.array; } else { setProperty(frame.object!, frame.key!, value); const c = seek(); if (c === COMMA) { i++; frame.key = readKey(); continue nextValue; } if (c !== CLOSE_BRACE) { fail("Expected ',' or '}' after property value"); } i++; value = frame.object!; } stack.pop(); } } }; const value = parseValue(); while (i < len) { const c = buf[i]; if (c !== SPACE && c !== TAB && c !== NEWLINE && c !== CARRIAGE_RETURN) { fail("Unexpected non-whitespace character after JSON"); } i++; } return value; } /** * A drop-in replacement for vscode-jsonrpc's default `application/json` content-type decoder, which * does {@code JSON.parse(buffer.toString(charset))}. That intermediate string is capped at V8's * String.kMaxLength (~512 MB), so an inbound message above the cap throws "Cannot create a string * longer than 0x1fffffe8 characters" before it is ever parsed — the reader drops the message, the * peer's request is never answered, and the call hangs until it times out. Parsing straight from the * bytes raises the inbound ceiling to Node's Buffer.MAX_LENGTH, matching what {@link * chunkedJsonEncoder} does for outbound messages. * * A UTF-8 (or ASCII) decode never yields more UTF-16 code units than there are bytes, so a body at * or under the cap cannot overflow the intermediate string; those take the faster single-string * parse, and only genuinely oversized bodies pay for the byte-level walk. */ export const chunkedJsonDecoder: JsonRpcContentTypeDecoder = { name: "application/json", decode(buffer: Uint8Array, options: { charset: BufferEncoding }): Promise { try { const buf = Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer.buffer, buffer.byteOffset, buffer.byteLength); return Promise.resolve((buf.byteLength <= constants.MAX_STRING_LENGTH ? JSON.parse(buf.toString(options.charset)) : parseJsonBytes(buf, options.charset)) as Message); } catch (err) { return Promise.reject(err); } } };