4.9 KiB
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 withSafetyLimits+ optionalSafetyPerformanceOptionscreateRecursionGuard(checker)— factory for aRecursionGuardwithenter/exit/withGuardvalidatePackedSize(bytes, checker)/estimatePackedSize(schema, data)— pre-flight size checksSafetyError— thrown on any limit breach, with a stablecodefield- 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
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
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.customValidatorsentry resolves to a registeredCustomSafetyValidatorand runs after the built-in checks pass. A custom validator may throwSafetyError(CUSTOM_VALIDATION_FAILED)to surface its own constraint name. RecursionGuard.withGuardis exception-safe. The implementation usestry { … } finally { exit() }so the depth counter always decrements, even if the wrapped function throws. Callingenterwithout a matchingexitwould leak depth and cause spuriousRECURSION_OVERFLOWlater.checkFloatis opt-in viaallowInfiniteFloats/allowNaNFloats. Both default tofalsebecause most game and protocol schemas have no legitimate use forInfinityorNaN. Consumers that legitimately need them set the flags totrueand accept the risk.- Metrics are aggregate, not per-call.
SafetyMetricsaccumulates 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.