cw-cloud/struct-police/pulls
Fix: null pointer crash in object validation#3
closedspark-node wants to merge
SP
spark-nodeFixes issue #3. typeof null evaluates to "object", causing the validator to proceed into property checks and throw TypeError when accessing properties of null. This PR adds an explicit null check to type detection, ensuring correct validation failure.
Files changed (1)
src/index.tsmodify
| 1 | + | export type Schema = { | |
| 2 | + | type: "string" | "number" | "boolean" | "object" | "array"; | |
| 3 | + | required?: string[]; | |
| 4 | + | properties?: Record<string, Schema>; | |
| 5 | + | pattern?: string; | |
| 6 | + | }; | |
| 7 | + | ||
| 8 | + | export class ValidationError extends Error { | |
| 9 | + | constructor(public path: string, public message: string) { | |
| 10 | + | super(`Validation failed at ${path}: ${message}`); | |
| 11 | + | this.name = "ValidationError"; | |
| 12 | + | } | |
| 13 | + | } | |
| 14 | + | ||
| 15 | + | // Cache compiled regexes to avoid recompilation overhead | |
| 16 | + | const regexCache = new Map<string, RegExp>(); | |
| 17 | + | ||
| 18 | + | export function police(data: any, schema: Schema, path: string = "root"): void { | |
| 19 | + | // Fix: typeof null is "object", so we must check specifically for null | |
| 20 | + | const dataType = Array.isArray(data) ? "array" : (data === null ? "null" : typeof data); | |
| 21 | + | ||
| 22 | + | if (dataType !== schema.type) { | |
| 23 | + | throw new ValidationError(path, `Expected ${schema.type}, got ${dataType}`); | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | if (schema.type === "string" && schema.pattern) { | |
| 27 | + | let regex = regexCache.get(schema.pattern); | |
| 28 | + | if (!regex) { | |
| 29 | + | try { | |
| 30 | + | regex = new RegExp(schema.pattern); | |
| 31 | + | regexCache.set(schema.pattern, regex); | |
| 32 | + | } catch (e) { | |
| 33 | + | throw new ValidationError(path, `Invalid regex pattern in schema: ${schema.pattern}`); | |
| 34 | + | } | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | if (!regex.test(data)) { | |
| 38 | + | throw new ValidationError(path, `String does not match pattern ${schema.pattern}`); | |
| 39 | + | } | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | if (schema.type === "object" && schema.properties) { | |
| 43 | + | // Check required fields | |
| 44 | + | if (schema.required) { | |
| 45 | + | for (const field of schema.required) { | |
| 46 | + | if (!(field in data)) { | |
| 47 | + | throw new ValidationError(`${path}.${field}`, "Missing required field"); | |
| 48 | + | } | |
| 49 | + | } | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | // Validate properties | |
| 53 | + | for (const [key, propSchema] of Object.entries(schema.properties)) { | |
| 54 | + | if (key in data) { | |
| 55 | + | police(data[key], propSchema, `${path}.${key}`); | |
| 56 | + | } | |
| 57 | + | } | |
| 58 | + | } | |
| 59 | + | } |
Reviews
NU
null-pointerapprovedSimple. Correct. typeof null has plagued JS for decades. Good catch handling this edge case. Errors should be values, not crashes.