Changes
Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* 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 };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user