# Safety Checks ## Purpose Production-time defensive checks against pathological inputs to the bitpacker — schemas with infinite recursion, arrays with millions of elements, objects with thousands of keys, floats that are `NaN` or `Infinity`. The `SafetyChecker` class wraps every public-boundary pack/unpack call in a lightweight check that fails fast on the limits it was constructed with, before the bitpacker hot path consumes excessive memory or compute. Safety is opt-in (each `SafetyEnabledSchema` carries an embedded `_safety` block) and selective (the `SafetyPerformanceOptions` lets a consumer disable individual checks for benchmarked-acceptable workloads). The module also ships the `RecursionGuard` factory that the `BitPacker.unpack` path uses to enforce `maxRecursionDepth`. Without the guard, an attacker-controlled schema with a self-referential `_ref` could blow the JS stack via O(n) recursion on a large input. The guard's `withGuard(fn)` form auto-cleans up on exception so a deeply-nested unpack failure doesn't leave the depth counter wedged. ## Features - `SafetyChecker` — main class, constructed with `SafetyLimits` + optional `SafetyPerformanceOptions` - `createRecursionGuard(checker)` — factory for a `RecursionGuard` with `enter`/`exit`/`withGuard` - `validatePackedSize(bytes, checker)` / `estimatePackedSize(schema, data)` — pre-flight size checks - `SafetyError` — thrown on any limit breach, with a stable `code` field - Six published codes: `BUFFER_OVERFLOW`, `RECURSION_OVERFLOW`, `ARRAY_TOO_LARGE`, `OBJECT_TOO_LARGE`, `INVALID_FLOAT`, `CUSTOM_VALIDATION_FAILED` - `DEFAULT_SAFETY_LIMITS` — `{ maxBufferSize: 10MB, maxRecursionDepth: 100, maxArrayLength: 10_000, maxObjectKeys: 1_000, allowInfiniteFloats: false, allowNaNFloats: false }` ## Usage: Basic Example ```ts import { SafetyChecker, DEFAULT_SAFETY_LIMITS } from "@suaveplan/bitpacker-validation"; const checker = new SafetyChecker(DEFAULT_SAFETY_LIMITS); checker.checkArray(new Array(5000)); // ok checker.checkArray(new Array(50_000)); // throws SafetyError(ARRAY_TOO_LARGE) ``` ## Usage: Advanced Example ```ts import { SafetyChecker, SafetyError, createRecursionGuard, } from "@suaveplan/bitpacker-validation"; const checker = new SafetyChecker({ maxBufferSize: 1024 * 1024, maxRecursionDepth: 32, maxArrayLength: 1000, maxObjectKeys: 100, allowInfiniteFloats: false, allowNaNFloats: false, }); const guard = createRecursionGuard(checker); function unpackSafely(schema: unknown, data: unknown): unknown { return guard.withGuard(() => { try { return performUnpack(schema, data); } catch (err) { if (err instanceof SafetyError && err.code === "RECURSION_OVERFLOW") { // …emit metric, fall back to a depth-limited representation… } throw err; } }); } ``` ## API Reference ### `class SafetyChecker` Constructor: `new SafetyChecker(limits?, performance?)`. `limits` defaults to `DEFAULT_SAFETY_LIMITS`. `performance` defaults to `{}` (every check enabled). #### `checkArray(arr: unknown[])`, `checkObject(obj: object)`, `checkBuffer(bytes: Uint8Array)`, `checkFloat(value: number)` Each method throws `SafetyError` with the corresponding code on limit breach. #### `getMetrics(): SafetyMetrics` Returns the per-violation counters and the timestamp of the last violation. ### `function createRecursionGuard(checker: SafetyChecker): RecursionGuard` Returns a guard with `enter()`, `exit()`, and `withGuard(fn)`. `withGuard` auto-cleans on exception. ### `class SafetyError extends Error` `code: string`. Six published codes (see Features). ### `function estimatePackedSize(schema, data)` / `validatePackedSize(bytes, checker)` Pre-flight estimation and post-pack validation. The estimator is cheap; use it before `checker.checkBuffer` to short-circuit clearly-oversize packs. ## Implementation Notes - **Custom validators run after built-ins.** A `SchemaSafetyLimits.customValidators` entry resolves to a registered `CustomSafetyValidator` and runs after the built-in checks pass. A custom validator may throw `SafetyError(CUSTOM_VALIDATION_FAILED)` to surface its own constraint name. - **`RecursionGuard.withGuard` is exception-safe.** The implementation uses `try { … } finally { exit() }` so the depth counter always decrements, even if the wrapped function throws. Calling `enter` without a matching `exit` would leak depth and cause spurious `RECURSION_OVERFLOW` later. - **`checkFloat` is opt-in via `allowInfiniteFloats` / `allowNaNFloats`.** Both default to `false` because most game and protocol schemas have no legitimate use for `Infinity` or `NaN`. Consumers that legitimately need them set the flags to `true` and accept the risk. - **Metrics are aggregate, not per-call.** `SafetyMetrics` accumulates violation counts across the checker's lifetime. Callers who want per-call metrics must reset the checker between calls or accumulate at their own layer.