first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,93 @@
# 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.