type AssetKind = 'document' | 'spreadsheet' | 'archive'; type AssetDefinition = { kind: AssetKind; extension: string; needsZipSignature?: boolean; needsOleSignature?: boolean; needsPdfSignature?: boolean; needsTextContent?: boolean; }; const allowedAssetTypes = new Map([ ['application/pdf', { kind: 'document', extension: 'pdf', needsPdfSignature: true }], ['application/vnd.openxmlformats-officedocument.wordprocessingml.document', { kind: 'document', extension: 'docx', needsZipSignature: true }], ['application/msword', { kind: 'document', extension: 'doc', needsOleSignature: true }], ['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', { kind: 'spreadsheet', extension: 'xlsx', needsZipSignature: true }], ['application/vnd.ms-excel', { kind: 'spreadsheet', extension: 'xls', needsOleSignature: true }], ['text/csv', { kind: 'spreadsheet', extension: 'csv', needsTextContent: true }], ['application/zip', { kind: 'archive', extension: 'zip', needsZipSignature: true }], ]); export class AssetUploadValidationError extends Error { constructor(message: string, public readonly statusCode: 400 | 413 | 415) { super(message); } } const zipSignature = Buffer.from([0x50, 0x4b, 0x03, 0x04]); const oleSignature = Buffer.from([0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1]); export function validateAssetUpload(body: Buffer, contentType: string | undefined, maxBytes: number) { const normalizedType = contentType?.split(';')[0]?.trim().toLowerCase(); const definition = normalizedType ? allowedAssetTypes.get(normalizedType) : undefined; if (!definition) { throw new AssetUploadValidationError('Envie apenas arquivos PDF, DOCX, XLSX, CSV ou ZIP.', 415); } if (body.length === 0) throw new AssetUploadValidationError('Escolha um arquivo para enviar.', 400); if (body.length > maxBytes) throw new AssetUploadValidationError(`O arquivo excede o limite de ${Math.floor(maxBytes / 1024 / 1024)} MB.`, 413); if (definition.needsPdfSignature && !body.subarray(0, 5).equals(Buffer.from('%PDF-'))) { throw new AssetUploadValidationError('O arquivo enviado não é um PDF válido.', 415); } if (definition.needsZipSignature && !body.subarray(0, 4).equals(zipSignature)) { throw new AssetUploadValidationError('O arquivo enviado não possui o formato esperado.', 415); } if (definition.needsOleSignature && !body.subarray(0, 8).equals(oleSignature)) { throw new AssetUploadValidationError('O arquivo enviado não possui o formato esperado.', 415); } if (definition.needsTextContent && body.subarray(0, Math.min(body.length, 4096)).includes(0)) { throw new AssetUploadValidationError('O arquivo CSV deve conter texto.', 415); } return { kind: definition.kind, extension: definition.extension, contentType: normalizedType as string }; }