fix(housekeeping): close schema reflection escapes
This commit is contained in:
@@ -357,6 +357,22 @@ function isZodInternalNode(value: object): value is ZodInternalNode {
|
||||
|
||||
function createReadonlyValidationFacade<T extends z.ZodType>(schema: T): T {
|
||||
const views = new WeakMap<object, unknown>();
|
||||
const parseResultMethods = new Set<PropertyKey>([
|
||||
"parse",
|
||||
"safeParse",
|
||||
"parseAsync",
|
||||
"safeParseAsync",
|
||||
"spa",
|
||||
"encode",
|
||||
"decode",
|
||||
"encodeAsync",
|
||||
"decodeAsync",
|
||||
"safeEncode",
|
||||
"safeDecode",
|
||||
"safeEncodeAsync",
|
||||
"safeDecodeAsync",
|
||||
"toJSONSchema",
|
||||
]);
|
||||
|
||||
function readonlyView(value: unknown): unknown {
|
||||
if (
|
||||
@@ -369,56 +385,208 @@ function createReadonlyValidationFacade<T extends z.ZodType>(schema: T): T {
|
||||
if (cached !== undefined) return cached;
|
||||
if (value instanceof z.ZodType) return schemaFacade(value);
|
||||
if (typeof value === "function") {
|
||||
const wrapped = (...args: unknown[]) =>
|
||||
readonlyView(Reflect.apply(value, undefined, args));
|
||||
views.set(value, wrapped);
|
||||
return Object.freeze(wrapped);
|
||||
return functionFacade(value as (...args: never[]) => unknown);
|
||||
}
|
||||
|
||||
const view = new Proxy(value, {
|
||||
defineProperty: () => false,
|
||||
deleteProperty: () => false,
|
||||
get: (target, property, receiver) => {
|
||||
const raw = Reflect.get(target, property, receiver);
|
||||
if (typeof raw !== "function") return readonlyView(raw);
|
||||
return (...args: unknown[]) =>
|
||||
readonlyView(Reflect.apply(raw, receiver, args));
|
||||
},
|
||||
set: () => false,
|
||||
setPrototypeOf: () => false,
|
||||
});
|
||||
views.set(value, view);
|
||||
return view;
|
||||
if (Array.isArray(value)) return arrayFacade(value);
|
||||
if (value instanceof Map) return mapFacade(value);
|
||||
if (value instanceof Set) return setFacade(value);
|
||||
if (value instanceof RegExp) {
|
||||
const detached = new RegExp(value.source, value.flags);
|
||||
views.set(value, detached);
|
||||
return Object.freeze(detached);
|
||||
}
|
||||
if (value instanceof Date) {
|
||||
const detached = new Date(value.getTime());
|
||||
views.set(value, detached);
|
||||
return Object.freeze(detached);
|
||||
}
|
||||
return objectFacade(value);
|
||||
}
|
||||
|
||||
function functionFacade(value: (...args: never[]) => unknown): unknown {
|
||||
const wrapped = function (this: unknown, ...args: unknown[]) {
|
||||
const result = new.target
|
||||
? Reflect.construct(value, args)
|
||||
: Reflect.apply(value, this, args);
|
||||
return readonlyView(result);
|
||||
};
|
||||
views.set(value, wrapped);
|
||||
return Object.freeze(wrapped);
|
||||
}
|
||||
|
||||
function arrayFacade(value: readonly unknown[]): readonly unknown[] {
|
||||
const detached: unknown[] = [];
|
||||
views.set(value, detached);
|
||||
for (const item of value) detached.push(readonlyView(item));
|
||||
return Object.freeze(detached);
|
||||
}
|
||||
|
||||
function mapFacade(
|
||||
value: ReadonlyMap<unknown, unknown>,
|
||||
): ReadonlyMap<unknown, unknown> {
|
||||
const detached = new Map<unknown, unknown>();
|
||||
views.set(value, detached);
|
||||
for (const [key, item] of value) {
|
||||
detached.set(readonlyView(key), readonlyView(item));
|
||||
}
|
||||
return Object.freeze(detached);
|
||||
}
|
||||
|
||||
function setFacade(value: ReadonlySet<unknown>): ReadonlySet<unknown> {
|
||||
const detached = new Set<unknown>();
|
||||
views.set(value, detached);
|
||||
for (const item of value) detached.add(readonlyView(item));
|
||||
return Object.freeze(detached);
|
||||
}
|
||||
|
||||
function objectFacade(value: object): object {
|
||||
const detached = Object.create(Object.getPrototypeOf(value)) as Record<
|
||||
PropertyKey,
|
||||
unknown
|
||||
>;
|
||||
views.set(value, detached);
|
||||
copyReadonlyProperties(value, detached, (method) =>
|
||||
boundFunctionFacade(method, value),
|
||||
);
|
||||
return Object.freeze(detached);
|
||||
}
|
||||
|
||||
function boundFunctionFacade(
|
||||
method: (...args: never[]) => unknown,
|
||||
receiver: object,
|
||||
): (...args: unknown[]) => unknown {
|
||||
return Object.freeze((...args: unknown[]) =>
|
||||
readonlyView(Reflect.apply(method, receiver, args)),
|
||||
);
|
||||
}
|
||||
|
||||
function copyReadonlyProperties(
|
||||
source: object,
|
||||
destination: Record<PropertyKey, unknown>,
|
||||
wrapMethod: (
|
||||
method: (...args: never[]) => unknown,
|
||||
property: PropertyKey,
|
||||
) => unknown,
|
||||
): void {
|
||||
for (const key of Reflect.ownKeys(source)) {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(source, key);
|
||||
if (!descriptor) continue;
|
||||
if ("value" in descriptor) {
|
||||
Object.defineProperty(destination, key, {
|
||||
configurable: false,
|
||||
enumerable: descriptor.enumerable,
|
||||
value:
|
||||
typeof descriptor.value === "function"
|
||||
? wrapMethod(descriptor.value, key)
|
||||
: readonlyView(descriptor.value),
|
||||
writable: false,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const getter = descriptor.get;
|
||||
const safeGetter = getter
|
||||
? () => readonlyView(Reflect.apply(getter, source, []))
|
||||
: undefined;
|
||||
Object.defineProperty(destination, key, {
|
||||
configurable: false,
|
||||
enumerable: descriptor.enumerable,
|
||||
get: safeGetter,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function materializeSchemaPrototypeMethods(target: z.ZodType): void {
|
||||
let prototype = Object.getPrototypeOf(target);
|
||||
while (prototype && prototype !== Object.prototype) {
|
||||
for (const property of Reflect.ownKeys(prototype)) {
|
||||
if (Object.hasOwn(target, property)) continue;
|
||||
const descriptor = Object.getOwnPropertyDescriptor(prototype, property);
|
||||
if (
|
||||
!descriptor?.get ||
|
||||
!descriptor.set ||
|
||||
!descriptor.configurable ||
|
||||
descriptor.enumerable
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
Reflect.get(target, property, target);
|
||||
}
|
||||
prototype = Object.getPrototypeOf(prototype);
|
||||
}
|
||||
}
|
||||
function schemaFacade<S extends z.ZodType>(target: S): S {
|
||||
const cached = views.get(target);
|
||||
if (cached !== undefined) return cached as S;
|
||||
|
||||
const facade = Object.create(Object.getPrototypeOf(target)) as Record<
|
||||
materializeSchemaPrototypeMethods(target);
|
||||
const shell = Object.create(Object.getPrototypeOf(target)) as Record<
|
||||
PropertyKey,
|
||||
unknown
|
||||
>;
|
||||
let facade: S;
|
||||
facade = new Proxy(shell, {
|
||||
defineProperty: () => false,
|
||||
deleteProperty: () => false,
|
||||
get: (publicShell, property, receiver) => {
|
||||
if (Object.hasOwn(publicShell, property)) {
|
||||
return Reflect.get(publicShell, property, receiver);
|
||||
}
|
||||
const inherited = Reflect.get(publicShell, property, receiver);
|
||||
return readonlyView(inherited);
|
||||
},
|
||||
set: () => false,
|
||||
setPrototypeOf: () => false,
|
||||
}) as unknown as S;
|
||||
views.set(target, facade);
|
||||
for (const key of Reflect.ownKeys(target)) {
|
||||
const raw = Reflect.get(target, key);
|
||||
const exposed =
|
||||
typeof raw === "function"
|
||||
? (...args: unknown[]) => {
|
||||
const result = Reflect.apply(raw, target, args);
|
||||
return result instanceof z.ZodType
|
||||
? isolateValidationGraph(result, "derived-schema")
|
||||
: result;
|
||||
}
|
||||
: readonlyView(raw);
|
||||
Object.defineProperty(facade, key, {
|
||||
configurable: false,
|
||||
enumerable: Object.prototype.propertyIsEnumerable.call(target, key),
|
||||
value: exposed,
|
||||
writable: false,
|
||||
copyReadonlyProperties(target, shell, (method, property) =>
|
||||
schemaMethodFacade(method, property, target, facade),
|
||||
);
|
||||
Object.freeze(shell);
|
||||
return facade;
|
||||
}
|
||||
|
||||
function schemaMethodFacade<S extends z.ZodType>(
|
||||
method: (...args: never[]) => unknown,
|
||||
property: PropertyKey,
|
||||
target: S,
|
||||
facade: S,
|
||||
): (...args: unknown[]) => unknown {
|
||||
if (property === "apply") {
|
||||
return Object.freeze((...args: unknown[]) => {
|
||||
const callback = args[0];
|
||||
if (typeof callback !== "function") {
|
||||
throw new TypeError("schema apply callback missing");
|
||||
}
|
||||
return Reflect.apply(callback, undefined, [facade]);
|
||||
});
|
||||
}
|
||||
return Object.freeze(facade) as S;
|
||||
if (property === "register") {
|
||||
return Object.freeze((...args: unknown[]) => {
|
||||
const registry = args[0];
|
||||
if (
|
||||
typeof registry !== "object" ||
|
||||
registry === null ||
|
||||
typeof (registry as { add?: unknown }).add !== "function"
|
||||
) {
|
||||
throw new TypeError("schema registry missing");
|
||||
}
|
||||
(
|
||||
registry as {
|
||||
add(schema: S, metadata?: unknown): unknown;
|
||||
}
|
||||
).add(facade, args[1]);
|
||||
return facade;
|
||||
});
|
||||
}
|
||||
|
||||
return Object.freeze((...args: unknown[]) => {
|
||||
const result = Reflect.apply(method, target, args);
|
||||
if (result instanceof z.ZodType) {
|
||||
return isolateValidationGraph(result, "derived-schema");
|
||||
}
|
||||
return parseResultMethods.has(property) ? result : readonlyView(result);
|
||||
});
|
||||
}
|
||||
|
||||
return schemaFacade(schema);
|
||||
|
||||
Reference in New Issue
Block a user