270 lines
8.9 KiB
TypeScript
270 lines
8.9 KiB
TypeScript
// 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<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!, 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(); |