From 0ef51a0085135b723f5b8de93f379c83f786bc71 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 11:48:02 +0000 Subject: [PATCH 01/10] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/lib/validateModelFile.ts | 118 +++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 src/lib/validateModelFile.ts diff --git a/src/lib/validateModelFile.ts b/src/lib/validateModelFile.ts new file mode 100644 index 0000000..b263765 --- /dev/null +++ b/src/lib/validateModelFile.ts @@ -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 }; + } +} From 1708398dd8d6825a9a2d24d73257f78632dd1b81 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 11:48:12 +0000 Subject: [PATCH 02/10] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/pages/Index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index 6e570c2..0c05a82 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -6,6 +6,7 @@ import { useModelStore } from '@/stores/useModelStore'; import { toast } from 'sonner'; import { getSupportedExtension, convertToGLB, ACCEPTED_EXTENSIONS } from '@/lib/convertToGLB'; import { convertIFCtoGLB } from '@/lib/convertIFC'; +import { validateModelFile, MAX_MODEL_BYTES } from '@/lib/validateModelFile'; import { CloudLoader } from '@/components/CloudLoader'; import { SceneModelList } from '@/components/SceneModelList'; From acbb02cc94b65a6e8ea6af00c9d5ad45cb904338 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 11:48:29 +0000 Subject: [PATCH 03/10] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/pages/Index.tsx | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index 0c05a82..f6a4b49 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -37,7 +37,29 @@ const Index = () => { return; } + // Pré-validação rápida (tamanho) antes de ler o arquivo inteiro na memória + const sizeCheck = validateModelFile(file.name, file.size, ext); + if (!sizeCheck.ok) { + toast.error(sizeCheck.reason!); + return; + } + if (ext === 'glb') { + // Para GLB, valida a assinatura antes de criar o objectURL + try { + const head = await file.slice(0, 16).arrayBuffer(); + const headBuf = new ArrayBuffer(file.size); + // Para validar assinatura precisamos só do header — slice já basta + const sigCheck = validateModelFile(file.name, file.size, ext, head); + if (!sigCheck.ok) { + toast.error(sigCheck.reason!); + if (sigCheck.detail) console.warn('[validateModelFile]', sigCheck.detail); + return; + } + void headBuf; + } catch (err) { + console.error('[upload] header read failed', err); + } const url = URL.createObjectURL(file); addModel({ fileName: file.name, fileSize: file.size, url }); toast.success(`Modelo "${file.name}" carregado com sucesso!`); @@ -48,6 +70,12 @@ const Index = () => { setConverting(true); try { const buffer = await file.arrayBuffer(); + const sigCheck = validateModelFile(file.name, file.size, ext, buffer); + if (!sigCheck.ok) { + toast.error(sigCheck.reason!); + if (sigCheck.detail) console.warn('[validateModelFile]', sigCheck.detail); + return; + } let result; if (ext === 'ifc') { result = await convertIFCtoGLB(buffer, file.name); @@ -58,8 +86,9 @@ const Index = () => { addModel({ fileName: result.fileName, fileSize: result.fileSize, url }); toast.success(`"${file.name}" convertido para GLB e carregado!`); } catch (err) { - console.error(err); - toast.error(`Falha ao converter "${file.name}"`); + console.error('[upload] conversion failed', err); + const msg = err instanceof Error ? err.message : 'erro desconhecido'; + toast.error(`Falha ao converter "${file.name}": ${msg}`); } finally { setConverting(false); } From 0b437bce8a36c08dd8b4f3fcc148c3fca27c7da3 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 11:48:41 +0000 Subject: [PATCH 04/10] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/pages/Index.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index f6a4b49..0126af5 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -45,18 +45,15 @@ const Index = () => { } if (ext === 'glb') { - // Para GLB, valida a assinatura antes de criar o objectURL + // Para GLB, valida assinatura "glTF" antes de criar o objectURL try { const head = await file.slice(0, 16).arrayBuffer(); - const headBuf = new ArrayBuffer(file.size); - // Para validar assinatura precisamos só do header — slice já basta const sigCheck = validateModelFile(file.name, file.size, ext, head); if (!sigCheck.ok) { toast.error(sigCheck.reason!); if (sigCheck.detail) console.warn('[validateModelFile]', sigCheck.detail); return; } - void headBuf; } catch (err) { console.error('[upload] header read failed', err); } From 2ae86140bb26991d193b93a1b50bcb535a2d8bf7 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 11:48:51 +0000 Subject: [PATCH 05/10] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/pages/Index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index 0126af5..e8018b7 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -6,7 +6,7 @@ import { useModelStore } from '@/stores/useModelStore'; import { toast } from 'sonner'; import { getSupportedExtension, convertToGLB, ACCEPTED_EXTENSIONS } from '@/lib/convertToGLB'; import { convertIFCtoGLB } from '@/lib/convertIFC'; -import { validateModelFile, MAX_MODEL_BYTES } from '@/lib/validateModelFile'; +import { validateModelFile } from '@/lib/validateModelFile'; import { CloudLoader } from '@/components/CloudLoader'; import { SceneModelList } from '@/components/SceneModelList'; From 0566a6a36b48f6b980be1ea96f59678fa63dfbc0 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 11:48:57 +0000 Subject: [PATCH 06/10] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/pages/Viewer.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/Viewer.tsx b/src/pages/Viewer.tsx index 3a8a672..b3b9bc8 100644 --- a/src/pages/Viewer.tsx +++ b/src/pages/Viewer.tsx @@ -16,6 +16,7 @@ import { toast } from "sonner"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Separator } from "@/components/ui/separator"; import { getSupportedExtension, convertToGLB, ACCEPTED_EXTENSIONS } from "@/lib/convertToGLB"; +import { validateModelFile } from "@/lib/validateModelFile"; import { convertIFCtoGLB } from "@/lib/convertIFC"; const Viewer = () => { From a90fdbf36453753f855ddc4e243311c12d1409ba Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 11:49:13 +0000 Subject: [PATCH 07/10] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/pages/Viewer.tsx | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/pages/Viewer.tsx b/src/pages/Viewer.tsx index b3b9bc8..d1282ba 100644 --- a/src/pages/Viewer.tsx +++ b/src/pages/Viewer.tsx @@ -44,7 +44,22 @@ const Viewer = () => { toast.error('Formato inválido. Selecione .GLB, .OBJ, .STL ou .IFC'); return; } + const sizeCheck = validateModelFile(file.name, file.size, ext); + if (!sizeCheck.ok) { + toast.error(sizeCheck.reason!); + return; + } if (ext === 'glb') { + try { + const head = await file.slice(0, 16).arrayBuffer(); + const sig = validateModelFile(file.name, file.size, ext, head); + if (!sig.ok) { + toast.error(sig.reason!); + return; + } + } catch (err) { + console.error('[upload] header read failed', err); + } const url = URL.createObjectURL(file); addModel({ fileName: file.name, fileSize: file.size, url }); toast.success(`"${file.name}" adicionado à cena`); @@ -53,6 +68,12 @@ const Viewer = () => { setConverting(true); try { const buffer = await file.arrayBuffer(); + const sig = validateModelFile(file.name, file.size, ext, buffer); + if (!sig.ok) { + toast.error(sig.reason!); + if (sig.detail) console.warn('[validateModelFile]', sig.detail); + return; + } const result = ext === 'ifc' ? await convertIFCtoGLB(buffer, file.name) : await convertToGLB(buffer, ext, file.name); @@ -60,8 +81,9 @@ const Viewer = () => { addModel({ fileName: result.fileName, fileSize: result.fileSize, url }); toast.success(`"${file.name}" convertido e adicionado!`); } catch (err) { - console.error(err); - toast.error(`Falha ao converter "${file.name}"`); + console.error('[upload] conversion failed', err); + const msg = err instanceof Error ? err.message : 'erro desconhecido'; + toast.error(`Falha ao converter "${file.name}": ${msg}`); } finally { setConverting(false); } From 94d4bb66a010ff3625d9850d5622e6864a9ed7e0 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 11:49:23 +0000 Subject: [PATCH 08/10] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/components/CloudLoader.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/CloudLoader.tsx b/src/components/CloudLoader.tsx index d8af547..6b82903 100644 --- a/src/components/CloudLoader.tsx +++ b/src/components/CloudLoader.tsx @@ -10,6 +10,7 @@ import { useModelStore } from '@/stores/useModelStore'; import { toast } from 'sonner'; import { getSupportedExtension, convertToGLB } from '@/lib/convertToGLB'; import { convertIFCtoGLB } from '@/lib/convertIFC'; +import { validateModelFile } from '@/lib/validateModelFile'; const DEFAULT_URL = 'https://store.tracksteel.com.br/demo.ifc'; From c5ad42480c28ddff3d46672e0d5dc59d823030ec Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 11:49:36 +0000 Subject: [PATCH 09/10] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/components/CloudLoader.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/components/CloudLoader.tsx b/src/components/CloudLoader.tsx index 6b82903..e3e399f 100644 --- a/src/components/CloudLoader.tsx +++ b/src/components/CloudLoader.tsx @@ -119,6 +119,14 @@ export function CloudLoader({ compact = false, variant = 'outline', className }: const response = await smartFetch(rawUrl); const buffer = await response.arrayBuffer(); + // Valida tamanho + assinatura antes de converter + const sig = validateModelFile(fileName, buffer.byteLength, ext, buffer); + if (!sig.ok) { + toast.error(sig.reason!); + if (sig.detail) console.warn('[validateModelFile]', sig.detail); + return; + } + let blob: Blob, outName = fileName, outSize = buffer.byteLength; if (ext === 'glb') { blob = new Blob([buffer], { type: 'model/gltf-binary' }); From ee3a907450e73f646ef6e2ed980c57a4d91dd679 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 11:49:52 +0000 Subject: [PATCH 10/10] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/lib/generateReport.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/lib/generateReport.ts b/src/lib/generateReport.ts index abf97a0..d876cfb 100644 --- a/src/lib/generateReport.ts +++ b/src/lib/generateReport.ts @@ -227,10 +227,14 @@ export async function generateInspectionReport(data: ReportData) { for (let i = 0; i < data.screenshots.length; i++) { checkSpace(100); + const src = data.screenshots[i]; try { + if (typeof src !== 'string' || !src.startsWith('data:image/')) { + throw new Error('formato de imagem inválido (esperado data URL)'); + } const imgW = contentW; const imgH = imgW * 0.56; // ~16:9 - pdf.addImage(data.screenshots[i], 'PNG', margin, y, imgW, imgH); + pdf.addImage(src, 'PNG', margin, y, imgW, imgH); y += imgH + 3; pdf.setFontSize(7); @@ -238,10 +242,12 @@ export async function generateInspectionReport(data: ReportData) { pdf.setTextColor(148, 163, 184); pdf.text(`Captura ${i + 1} de ${data.screenshots.length}`, margin, y); y += 8; - } catch { + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + console.warn(`[generateReport] screenshot ${i + 1} falhou:`, msg); pdf.setFontSize(8); pdf.setTextColor(200, 50, 50); - pdf.text(`[Erro ao inserir screenshot ${i + 1}]`, margin, y); + pdf.text(`[Erro ao inserir screenshot ${i + 1}: ${msg}]`, margin, y); y += 8; } }