/** * Validators - Input and data validation utilities * Provides functions for validating ONNX files, SafeTensors files, model data, and user inputs. * Requirements: 3.3, 9.3, 36.1, 36.3, 36.5 */ const Validators = (function () { // ─── ONNX File Validation ───────────────────────────────────────────────── /** * ONNX protobuf magic bytes (first 2 bytes of a valid serialised protobuf * message with field 1 = irVersion). We accept any non-empty ArrayBuffer * whose first byte is 0x08 (field 1, wire type 0 = varint) as a minimal * heuristic, since ONNX models always start with the irVersion field. * * A stricter check would require a full protobuf parse; this is intentionally * lightweight for a browser-side static app. */ const ONNX_MAGIC_BYTE = 0x08; /** * Validate that a File object is a valid ONNX file. * Checks: * 1. The file has a .onnx extension (case-insensitive). * 2. The file is not empty. * * @param {File} file - Browser File object * @returns {{ valid: boolean, error?: string }} */ function isValidOnnxFile(file) { if (!file) { return { valid: false, error: 'No file provided.' }; } // Extension check const name = file.name || ''; if (!name.toLowerCase().endsWith('.onnx')) { return { valid: false, error: `Invalid file type "${name}". Only .onnx files are accepted.`, }; } // Empty file check if (file.size === 0) { return { valid: false, error: 'The file is empty.' }; } return { valid: true }; } /** * Validate the raw bytes of an ONNX file (ArrayBuffer). * Performs a lightweight magic-byte check. * * @param {ArrayBuffer} buffer * @returns {{ valid: boolean, error?: string }} */ function isValidOnnxBuffer(buffer) { if (!(buffer instanceof ArrayBuffer) || buffer.byteLength === 0) { return { valid: false, error: 'Invalid or empty buffer.' }; } const view = new Uint8Array(buffer, 0, Math.min(4, buffer.byteLength)); if (view[0] !== ONNX_MAGIC_BYTE) { return { valid: false, error: 'File does not appear to be a valid ONNX model (unexpected header bytes).', }; } return { valid: true }; } // ─── SafeTensors File Validation ──────────────────────────────────────────── /** * Validate that a File object is a valid SafeTensors file. * Checks: * 1. The file has a .safetensors extension (case-insensitive). * 2. The file is not empty. * * @param {File} file - Browser File object * @returns {{ valid: boolean, error?: string }} */ function isValidSafeTensorsFile(file) { if (!file) { return { valid: false, error: 'No file provided.' }; } // Extension check const name = file.name || ''; if (!name.toLowerCase().endsWith('.safetensors')) { return { valid: false, error: `Invalid file type "${name}". Only .safetensors files are accepted.`, }; } // Empty file check if (file.size === 0) { return { valid: false, error: 'The file is empty.' }; } return { valid: true }; } // ─── Model Data Validation ──────────────────────────────────────────────── /** * Validate a parsed model data object returned by ONNXParser. * Ensures the required top-level fields are present and non-null. * * @param {Object} data - ParsedModel object * @returns {{ valid: boolean, error?: string }} */ function isValidModelData(data) { if (!data || typeof data !== 'object') { return { valid: false, error: 'Model data is null or not an object.' }; } const required = ['metadata', 'graph', 'inputs', 'outputs']; for (const field of required) { if (!(field in data) || data[field] == null) { return { valid: false, error: `Model data is missing required field: "${field}".` }; } } if (!Array.isArray(data.inputs)) { return { valid: false, error: 'Model data "inputs" must be an array.' }; } if (!Array.isArray(data.outputs)) { return { valid: false, error: 'Model data "outputs" must be an array.' }; } return { valid: true }; } // ─── Input / Search Validation ──────────────────────────────────────────── /** * Validate a generic user input string. * Returns false for null/undefined; trims and checks length. * * @param {*} input * @param {{ minLength?: number, maxLength?: number, required?: boolean }} [opts] * @returns {{ valid: boolean, error?: string }} */ function isValidInput(input, opts = {}) { const { minLength = 0, maxLength = 1000, required = false } = opts; if (input == null || input === '') { if (required) { return { valid: false, error: 'This field is required.' }; } return { valid: true }; // empty is OK when not required } const str = String(input).trim(); if (required && str.length === 0) { return { valid: false, error: 'This field cannot be blank.' }; } if (str.length < minLength) { return { valid: false, error: `Input must be at least ${minLength} characters.` }; } if (str.length > maxLength) { return { valid: false, error: `Input must not exceed ${maxLength} characters.` }; } return { valid: true }; } /** * Validate a search query string. * Allows empty strings (clears the filter); rejects excessively long ones. * * @param {string} query * @returns {{ valid: boolean, error?: string }} */ function isValidSearchQuery(query) { return isValidInput(query, { maxLength: 200 }); } /** * Validate a model path string (must be non-empty and end with .onnx or .safetensors). * @param {string} path * @returns {{ valid: boolean, error?: string }} */ function isValidModelPath(path) { if (!path || typeof path !== 'string' || path.trim() === '') { return { valid: false, error: 'Model path must be a non-empty string.' }; } const lower = path.toLowerCase(); if (!lower.endsWith('.onnx') && !lower.endsWith('.safetensors')) { return { valid: false, error: 'Model path must point to an .onnx or .safetensors file.' }; } return { valid: true }; } // ─── Public API ─────────────────────────────────────────────────────────── return { isValidOnnxFile, isValidOnnxBuffer, isValidSafeTensorsFile, isValidModelData, isValidInput, isValidSearchQuery, isValidModelPath, }; })(); // Export for global access in vanilla JS context window.Validators = Validators;