Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
124 lines
4.3 KiB
JavaScript
124 lines
4.3 KiB
JavaScript
"use strict";
|
|
|
|
const PLUGIN_JSON_MAX_DEPTH = 128;
|
|
const PLUGIN_JSON_MAX_NODES = 100_000;
|
|
|
|
function jsonStringByteLength(value) {
|
|
let bytes = 2;
|
|
for (let index = 0; index < value.length; index += 1) {
|
|
const code = value.charCodeAt(index);
|
|
if (code === 0x22 || code === 0x5c || code === 0x08 || code === 0x09
|
|
|| code === 0x0a || code === 0x0c || code === 0x0d) {
|
|
bytes += 2;
|
|
} else if (code < 0x20) {
|
|
bytes += 6;
|
|
} else if (code < 0x80) {
|
|
bytes += 1;
|
|
} else if (code < 0x800) {
|
|
bytes += 2;
|
|
} else if (code >= 0xd800 && code <= 0xdbff) {
|
|
const next = value.charCodeAt(index + 1);
|
|
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
bytes += 4;
|
|
index += 1;
|
|
} else {
|
|
bytes += 6;
|
|
}
|
|
} else if (code >= 0xdc00 && code <= 0xdfff) {
|
|
bytes += 6;
|
|
} else {
|
|
bytes += 3;
|
|
}
|
|
}
|
|
return bytes;
|
|
}
|
|
|
|
function assertPluginJsonValue(value, options = {}) {
|
|
const maxBytes = options.maxBytes ?? Number.MAX_SAFE_INTEGER;
|
|
if (!Number.isSafeInteger(maxBytes) || maxBytes < 1) {
|
|
throw new TypeError("Plugin JSON maxBytes must be a positive safe integer");
|
|
}
|
|
const stack = [{ value, depth: 0 }];
|
|
const ancestors = new Set();
|
|
let nodes = 0;
|
|
let bytes = 0;
|
|
const addBytes = (count) => {
|
|
bytes += count;
|
|
if (bytes > maxBytes) throw new RangeError(`Plugin JSON exceeds ${maxBytes} bytes`);
|
|
};
|
|
while (stack.length > 0) {
|
|
const current = stack.pop();
|
|
if (current.exit) {
|
|
ancestors.delete(current.value);
|
|
continue;
|
|
}
|
|
nodes += 1;
|
|
if (nodes > PLUGIN_JSON_MAX_NODES) {
|
|
throw new RangeError(`Plugin JSON exceeds ${PLUGIN_JSON_MAX_NODES} values`);
|
|
}
|
|
if (current.depth > PLUGIN_JSON_MAX_DEPTH) {
|
|
throw new RangeError(`Plugin JSON exceeds ${PLUGIN_JSON_MAX_DEPTH} levels`);
|
|
}
|
|
const item = current.value;
|
|
if (item === null) {
|
|
addBytes(4);
|
|
continue;
|
|
}
|
|
if (typeof item === "string") {
|
|
addBytes(jsonStringByteLength(item));
|
|
continue;
|
|
}
|
|
if (typeof item === "boolean") {
|
|
addBytes(item ? 4 : 5);
|
|
continue;
|
|
}
|
|
if (typeof item === "number") {
|
|
if (!Number.isFinite(item)) throw new TypeError("Plugin JSON numbers must be finite");
|
|
addBytes(Buffer.byteLength(String(item), "utf8"));
|
|
continue;
|
|
}
|
|
if (typeof item !== "object") throw new TypeError("Plugin wire values must be JSON values");
|
|
if (ancestors.has(item)) throw new TypeError("Plugin JSON values must not contain cycles");
|
|
ancestors.add(item);
|
|
stack.push({ value: item, depth: current.depth, exit: true });
|
|
if (Array.isArray(item)) {
|
|
addBytes(Math.max(2, item.length + 1));
|
|
if (Object.keys(item).length !== item.length || Reflect.ownKeys(item).length !== item.length + 1) {
|
|
throw new TypeError("Plugin JSON arrays must be dense and contain no named properties");
|
|
}
|
|
for (let index = item.length - 1; index >= 0; index -= 1) {
|
|
const descriptor = Object.getOwnPropertyDescriptor(item, String(index));
|
|
if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) {
|
|
throw new TypeError("Plugin JSON arrays must contain enumerable data properties only");
|
|
}
|
|
stack.push({ value: descriptor.value, depth: current.depth + 1 });
|
|
}
|
|
continue;
|
|
}
|
|
const prototype = Object.getPrototypeOf(item);
|
|
if (prototype !== Object.prototype && prototype !== null) {
|
|
throw new TypeError("Plugin JSON objects must be plain objects");
|
|
}
|
|
const stringKeys = Object.keys(item);
|
|
addBytes(Math.max(2, stringKeys.length + 1));
|
|
if (Reflect.ownKeys(item).length !== stringKeys.length) {
|
|
throw new TypeError("Plugin JSON objects must not contain symbols or non-enumerable properties");
|
|
}
|
|
for (let index = stringKeys.length - 1; index >= 0; index -= 1) {
|
|
const descriptor = Object.getOwnPropertyDescriptor(item, stringKeys[index]);
|
|
if (!descriptor || !("value" in descriptor)) {
|
|
throw new TypeError("Plugin JSON objects must contain own data properties");
|
|
}
|
|
addBytes(jsonStringByteLength(stringKeys[index]) + 1);
|
|
stack.push({ value: descriptor.value, depth: current.depth + 1 });
|
|
}
|
|
}
|
|
return value;
|
|
}
|
|
|
|
module.exports = {
|
|
PLUGIN_JSON_MAX_DEPTH,
|
|
PLUGIN_JSON_MAX_NODES,
|
|
assertPluginJsonValue,
|
|
};
|