/** * Validação defensiva de arquivos 3D antes da conversão. * * Objetivos: * - Bloquear arquivos absurdamente grandes (estoura memória do navegador). * - Conferir "magic bytes" / assinaturas mínimas para evitar que um PDF * renomeado para .glb derrube o loader com erro críptico. * - Não tornar a validação rígida demais: STL/OBJ/IFC são formatos texto * ou semi-texto antigos e tolerantes a variações de header. * * Esta função NUNCA lança — devolve um resultado estruturado para o caller * decidir como exibir o erro. */ export type ModelExt = 'glb' | 'obj' | 'stl' | 'ifc'; export interface ValidationResult { ok: boolean; /** Mensagem amigável pronta para `toast.error(...)` quando ok=false. */ reason?: string; /** Detalhe técnico opcional para console.warn. */ detail?: string; } /** Limite de tamanho por arquivo (bytes). 250 MB cobre IFCs grandes sem OOM. */ export const MAX_MODEL_BYTES = 250 * 1024 * 1024; /** Tamanho mínimo plausível: arquivos < 16 bytes são certamente lixo. */ const MIN_MODEL_BYTES = 16; /** Quantos bytes ler do início para inspecionar a assinatura. */ const SNIFF_BYTES = 256; /** * Valida tamanho + assinatura mágica. * `buffer` é opcional — quando ausente, faz só a validação de tamanho/extensão. */ export function validateModelFile( fileName: string, fileSize: number, ext: ModelExt, buffer?: ArrayBuffer, ): ValidationResult { if (fileSize < MIN_MODEL_BYTES) { return { ok: false, reason: `"${fileName}" está vazio ou corrompido.` }; } if (fileSize > MAX_MODEL_BYTES) { const mb = (fileSize / 1024 / 1024).toFixed(0); const limit = (MAX_MODEL_BYTES / 1024 / 1024).toFixed(0); return { ok: false, reason: `"${fileName}" tem ${mb} MB — limite ${limit} MB para evitar travar o navegador.`, }; } if (!buffer) return { ok: true }; const head = new Uint8Array(buffer.slice(0, Math.min(SNIFF_BYTES, buffer.byteLength))); const asciiHead = new TextDecoder('utf-8', { fatal: false }) .decode(head) .replace(/^\uFEFF/, '') // strip BOM .trimStart() .toLowerCase(); switch (ext) { case 'glb': { // GLB binário começa com "glTF" (0x67 0x6C 0x54 0x46) const magicOk = head[0] === 0x67 && head[1] === 0x6c && head[2] === 0x54 && head[3] === 0x46; if (!magicOk) { return { ok: false, reason: `"${fileName}" não parece um GLB válido (assinatura "glTF" ausente).`, detail: `bytes iniciais: ${[...head.slice(0, 8)].map((b) => b.toString(16)).join(' ')}`, }; } return { ok: true }; } case 'ifc': { // IFC STEP começa com "ISO-10303-21" if (!asciiHead.startsWith('iso-10303-21')) { return { ok: false, reason: `"${fileName}" não parece um IFC válido (cabeçalho ISO-10303-21 ausente).`, }; } return { ok: true }; } case 'obj': { // OBJ é texto puro; aceita qualquer linha começando com diretivas comuns const validStart = /^(#|v |vn |vt |vp |f |g |o |s |usemtl |mtllib |l )/m; if (!validStart.test(asciiHead)) { return { ok: false, reason: `"${fileName}" não parece um OBJ válido.`, }; } return { ok: true }; } case 'stl': { // STL ASCII começa com "solid"; STL binário tem 80 bytes de header arbitrário // seguido de uint32 com a contagem de triângulos. A heurística mais segura: // - ASCII: começa com "solid" e não contém zeros nos primeiros 80 bytes // - Binário: tamanho == 84 + 50 * n_triangles if (asciiHead.startsWith('solid')) return { ok: true }; if (buffer.byteLength >= 84) { const dv = new DataView(buffer); const nTris = dv.getUint32(80, true); const expected = 84 + 50 * nTris; if (expected === buffer.byteLength) return { ok: true }; } return { ok: false, reason: `"${fileName}" não parece um STL válido (nem ASCII nem binário coerente).`, }; } default: return { ok: true }; } }