Files
homarr/packages/custom-widgets/src/jsx/interpreter-foundation.ts
T
+12 fcd7c979df feat!: release v2 (#6545)
Co-authored-by: Artur <145776561+arturict@users.noreply.github.com>
Co-authored-by: arturict <arturict@users.noreply.github.com>
Co-authored-by: Thomas <thomas@ajnart.fr>
Co-authored-by: Afonso Jorge Ramos <afonsojorgeramos@gmail.com>
Co-authored-by: Danura30082 <danura30082+github@gmail.com>
Co-authored-by: Kushie <122986913+kushiemoon-dev@users.noreply.github.com>
Co-authored-by: ajnart <thomas@ajnart.fr>
Co-authored-by: ajnart <thomas@ajnart.dev>
Co-authored-by: chenjun <chenjun@foxmail.com>
Co-authored-by: Wikibear <wikibear@users.noreply.github.com>
Co-authored-by: Bergasha <bergasha@gmail.com>
Co-authored-by: leafyelin <43306196+leafyelin@users.noreply.github.com>
Co-authored-by: Lucas Araujo <lucas.lra@gmail.com>
Co-authored-by: GreenMonito <86085570+GreenMonito@users.noreply.github.com>
Co-authored-by: Nerdy-Technician <roffo@nerdy-technician.social>
Co-authored-by: Jun <34545896+jcstrong@users.noreply.github.com>
Co-authored-by: Edmond Abraham <54119952+gitedmond@users.noreply.github.com>
Co-authored-by: thrawnx <tight.bulb9384@fastmail.com>
Co-authored-by: Chris <58918656+nordada@users.noreply.github.com>
Co-authored-by: yi111 <153097222+Yi-111-a@users.noreply.github.com>
Co-authored-by: Yanis <naamaneyanis@gmail.com>
Co-authored-by: Wikibear <me@sej.de>
Co-authored-by: alessiottavo <93441886+alessiottavo@users.noreply.github.com>
2026-10-02 21:00:14 +02:00

116 lines
4.1 KiB
TypeScript

import { CUSTOM_JSX_LIMITS } from "./policy";
export interface AstNode {
type: string;
start?: number;
end?: number;
[key: string]: unknown;
}
export interface EvaluationBudgets {
maxAstDepth: number;
maxOperations: number;
maxCollectionItems: number;
maxRenderedNodes: number;
maxStringLength: number;
}
export interface SafeJsxBudgets extends Partial<EvaluationBudgets> {}
export const INTERPRETER_CALLBACK = Symbol("custom-jsx-interpreter-callback");
export const DEFAULT_BUDGETS: EvaluationBudgets = {
maxAstDepth: CUSTOM_JSX_LIMITS.astDepth,
maxOperations: CUSTOM_JSX_LIMITS.operations,
maxCollectionItems: CUSTOM_JSX_LIMITS.collectionItems,
maxRenderedNodes: CUSTOM_JSX_LIMITS.renderedNodes,
maxStringLength: CUSTOM_JSX_LIMITS.stringLength,
};
export interface InterpreterCallback {
readonly kind: typeof INTERPRETER_CALLBACK;
readonly params: readonly string[];
readonly body: AstNode;
readonly environment: Environment;
}
export class SafeJsxError extends Error {
public constructor(message: string) {
super(message);
this.name = "SafeJsxError";
}
}
export class SafeJsxBudgetError extends SafeJsxError {
public constructor(message: string) {
super(message);
this.name = "SafeJsxBudgetError";
}
}
export class Environment {
public constructor(
private readonly values: Readonly<Record<string, unknown>>,
private readonly parent?: Environment,
) {}
public get(name: string): unknown {
if (Object.hasOwn(this.values, name)) return this.values[name];
if (this.parent) return this.parent.get(name);
if (name === "undefined") return undefined;
if (name === "NaN") return Number.NaN;
if (name === "Infinity") return Number.POSITIVE_INFINITY;
throw new SafeJsxError(`Unknown binding: ${name}`);
}
}
export class Budget {
private operations = 0;
private collectionItems = 0;
private renderedNodes = 0;
public constructor(private readonly limits: EvaluationBudgets) {}
public operation(depth: number): void {
if (depth > this.limits.maxAstDepth)
throw new SafeJsxBudgetError(`Template exceeded the AST depth limit (${this.limits.maxAstDepth})`);
if (++this.operations > this.limits.maxOperations)
throw new SafeJsxBudgetError(`Template exceeded the operation limit (${this.limits.maxOperations})`);
}
public collection(count = 1): void {
this.collectionItems += count;
if (this.collectionItems > this.limits.maxCollectionItems)
throw new SafeJsxBudgetError(`Template exceeded the collection limit (${this.limits.maxCollectionItems})`);
}
public rendered(): void {
if (++this.renderedNodes > this.limits.maxRenderedNodes)
throw new SafeJsxBudgetError(`Template exceeded the rendered node limit (${this.limits.maxRenderedNodes})`);
}
public string(value: string): string {
if (value.length > this.limits.maxStringLength)
throw new SafeJsxBudgetError(`Template exceeded the string length limit (${this.limits.maxStringLength})`);
return value;
}
}
export function asNode(value: unknown, context: string): AstNode {
if (!value || typeof value !== "object" || typeof (value as { type?: unknown }).type !== "string")
throw new SafeJsxError(`Invalid ${context} node`);
return value as AstNode;
}
export function asNodeArray(value: unknown, context: string): AstNode[] {
if (!Array.isArray(value)) throw new SafeJsxError(`Invalid ${context}`);
return value.map((item) => asNode(item, context));
}
export const isInterpreterCallback = (value: unknown): value is InterpreterCallback =>
typeof value === "object" && value !== null && (value as { kind?: unknown }).kind === INTERPRETER_CALLBACK;
export function numericArgument(value: unknown, fallback: number): number {
const number = value === undefined ? fallback : Number(value);
if (!Number.isFinite(number)) return fallback;
return Math.trunc(number);
}
export function normalizedSliceIndex(value: unknown, length: number, fallback: number): number {
const index = numericArgument(value, fallback);
if (index < 0) return Math.max(length + index, 0);
return Math.min(index, length);
}