Files
bitfielder/src/ShaderWorker.ts
2025-07-05 02:34:28 +02:00

182 lines
6.3 KiB
TypeScript

// WebWorker for safe shader compilation and execution
interface WorkerMessage {
id: string;
type: 'compile' | 'render';
code?: string;
width?: number;
height?: number;
time?: number;
}
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<WorkerMessage>) => {
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!);
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): 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 color = Math.abs(safeValue) % 256;
data[i] = color; // R
data[i + 1] = (color * 2) % 256; // G
data[i + 2] = (color * 3) % 256; // 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 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();