// WebWorker for safe shader compilation and execution interface WorkerMessage { id: string; type: 'compile' | 'render'; code?: string; width?: number; height?: number; time?: number; renderMode?: string; } interface WorkerResponse { id: string; type: 'compiled' | 'rendered' | 'error'; success: boolean; imageData?: ImageData; error?: string; } class ShaderWorker { private compiledFunction: Function | null = null; private lastCode: string = ''; constructor() { self.onmessage = (e: MessageEvent) => { this.handleMessage(e.data); }; } private handleMessage(message: WorkerMessage): void { try { switch (message.type) { case 'compile': this.compileShader(message.id, message.code!); break; case 'render': this.renderShader(message.id, message.width!, message.height!, message.time!, message.renderMode || 'classic'); break; } } catch (error) { this.postError(message.id, error instanceof Error ? error.message : 'Unknown error'); } } private compileShader(id: string, code: string): void { if (code === this.lastCode && this.compiledFunction) { this.postMessage({ id, type: 'compiled', success: true }); return; } try { const safeCode = this.sanitizeCode(code); this.compiledFunction = new Function('x', 'y', 't', 'i', ` // Timeout protection const startTime = performance.now(); let iterations = 0; function checkTimeout() { iterations++; if (iterations % 1000 === 0 && performance.now() - startTime > 5) { throw new Error('Shader timeout'); } } return (function() { checkTimeout(); return ${safeCode}; })(); `); this.lastCode = code; this.postMessage({ id, type: 'compiled', success: true }); } catch (error) { this.compiledFunction = null; this.postError(id, error instanceof Error ? error.message : 'Compilation failed'); } } private renderShader(id: string, width: number, height: number, time: number, renderMode: string): void { if (!this.compiledFunction) { this.postError(id, 'No compiled shader'); return; } const imageData = new ImageData(width, height); const data = imageData.data; const startTime = performance.now(); const maxRenderTime = 50; // 50ms max render time try { for (let y = 0; y < height; y++) { // Check timeout every row if (performance.now() - startTime > maxRenderTime) { // Fill remaining pixels with black and break for (let remainingY = y; remainingY < height; remainingY++) { for (let remainingX = 0; remainingX < width; remainingX++) { const i = (remainingY * width + remainingX) * 4; data[i] = 0; // R data[i + 1] = 0; // G data[i + 2] = 0; // B data[i + 3] = 255; // A } } break; } for (let x = 0; x < width; x++) { const i = (y * width + x) * 4; const pixelIndex = y * width + x; try { const value = this.compiledFunction(x, y, time, pixelIndex); const safeValue = isFinite(value) ? value : 0; const [r, g, b] = this.calculateColor(safeValue, renderMode); data[i] = r; // R data[i + 1] = g; // G data[i + 2] = b; // B data[i + 3] = 255; // A } catch (error) { data[i] = 0; // R data[i + 1] = 0; // G data[i + 2] = 0; // B data[i + 3] = 255; // A } } } this.postMessage({ id, type: 'rendered', success: true, imageData }); } catch (error) { this.postError(id, error instanceof Error ? error.message : 'Render failed'); } } private calculateColor(value: number, renderMode: string): [number, number, number] { const absValue = Math.abs(value) % 256; switch (renderMode) { case 'classic': return [ absValue, (absValue * 2) % 256, (absValue * 3) % 256 ]; case 'grayscale': return [absValue, absValue, absValue]; case 'red': return [absValue, 0, 0]; case 'green': return [0, absValue, 0]; case 'blue': return [0, 0, absValue]; case 'rgb': return [ (absValue * 255 / 256) | 0, ((absValue * 2) % 256 * 255 / 256) | 0, ((absValue * 3) % 256 * 255 / 256) | 0 ]; case 'hsv': return this.hsvToRgb(absValue / 255.0, 1.0, 1.0); case 'rainbow': return this.rainbowColor(absValue); default: return [absValue, absValue, absValue]; } } private hsvToRgb(h: number, s: number, v: number): [number, number, number] { const c = v * s; const x = c * (1 - Math.abs((h * 6) % 2 - 1)); const m = v - c; let r = 0, g = 0, b = 0; if (h < 1/6) { r = c; g = x; b = 0; } else if (h < 2/6) { r = x; g = c; b = 0; } else if (h < 3/6) { r = 0; g = c; b = x; } else if (h < 4/6) { r = 0; g = x; b = c; } else if (h < 5/6) { r = x; g = 0; b = c; } else { r = c; g = 0; b = x; } return [ Math.round((r + m) * 255), Math.round((g + m) * 255), Math.round((b + m) * 255) ]; } private rainbowColor(value: number): [number, number, number] { const phase = (value / 255.0) * 6; const segment = Math.floor(phase); const remainder = phase - segment; const t = remainder; const q = 1 - t; switch (segment % 6) { case 0: return [255, Math.round(t * 255), 0]; case 1: return [Math.round(q * 255), 255, 0]; case 2: return [0, 255, Math.round(t * 255)]; case 3: return [0, Math.round(q * 255), 255]; case 4: return [Math.round(t * 255), 0, 255]; case 5: return [255, 0, Math.round(q * 255)]; default: return [255, 255, 255]; } } private sanitizeCode(code: string): string { // Strict whitelist approach const allowedPattern = /^[0-9a-zA-Z\s\+\-\*\/\%\^\&\|\(\)\<\>\~\?:,\.xyti]+$/; if (!allowedPattern.test(code)) { throw new Error('Invalid characters in shader code'); } // Check for dangerous keywords const dangerousKeywords = [ 'eval', 'Function', 'constructor', 'prototype', '__proto__', 'window', 'document', 'global', 'process', 'require', 'import', 'export', 'class', 'function', 'var', 'let', 'const', 'while', 'for', 'do', 'if', 'else', 'switch', 'case', 'break', 'continue', 'return', 'throw', 'try', 'catch', 'finally' ]; const codeWords = code.toLowerCase().split(/[^a-z]/); for (const keyword of dangerousKeywords) { if (codeWords.includes(keyword)) { throw new Error(`Forbidden keyword: ${keyword}`); } } // Limit expression complexity const complexity = (code.match(/[\(\)]/g) || []).length; if (complexity > 20) { throw new Error('Expression too complex'); } // Limit code length if (code.length > 200) { throw new Error('Code too long'); } return code; } private postMessage(response: WorkerResponse): void { self.postMessage(response); } private postError(id: string, error: string): void { this.postMessage({ id, type: 'error', success: false, error }); } } // Initialize worker new ShaderWorker();