import fs from 'fs/promises' import path from 'path' import { InternalError, ValidationError } from '../../shared/errors/index.js' export const getUniqueFilename = async (imagesDirFullPath: string, baseName: string, ext: string) => { const maxAttempts = 1000 for (let i = 0; i < maxAttempts; i++) { const suffix = i === 0 ? '' : `-${i + 1}` const filename = `${baseName}${suffix}${ext}` const fullPath = path.join(imagesDirFullPath, filename) try { await fs.access(fullPath) } catch { return filename } } throw new InternalError('Failed to generate unique filename') } export const mimeToExt: Record = { 'image/png': '.png', 'image/jpeg': '.jpg', 'image/jpg': '.jpg', 'image/gif': '.gif', 'image/webp': '.webp', } const IMAGE_MAGIC_BYTES: Record = { 'image/png': { bytes: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a] }, 'image/jpeg': { bytes: [0xff, 0xd8, 0xff] }, 'image/gif': { bytes: [0x47, 0x49, 0x46, 0x38] }, 'image/webp': { bytes: [0x52, 0x49, 0x46, 0x46], offset: 0 }, } const WEBP_RIFF_HEADER = [0x52, 0x49, 0x46, 0x46] const WEBP_WEBP_MARKER = [0x57, 0x45, 0x42, 0x50] const MIN_IMAGE_SIZE = 16 const MAX_IMAGE_SIZE = 8 * 1024 * 1024 export const validateImageBuffer = (buffer: Buffer, claimedMimeType: string): void => { if (buffer.byteLength < MIN_IMAGE_SIZE) { throw new ValidationError('Image file is too small or corrupted') } if (buffer.byteLength > MAX_IMAGE_SIZE) { throw new ValidationError('Image file is too large') } const magicInfo = IMAGE_MAGIC_BYTES[claimedMimeType] if (!magicInfo) { throw new ValidationError('Unsupported image type for content validation') } const offset = magicInfo.offset || 0 const expectedBytes = magicInfo.bytes for (let i = 0; i < expectedBytes.length; i++) { if (buffer[offset + i] !== expectedBytes[i]) { throw new ValidationError('Image content does not match the claimed file type') } } if (claimedMimeType === 'image/webp') { if (buffer.byteLength < 12) { throw new ValidationError('WebP image is corrupted') } for (let i = 0; i < WEBP_WEBP_MARKER.length; i++) { if (buffer[8 + i] !== WEBP_WEBP_MARKER[i]) { throw new ValidationError('WebP image content is invalid') } } } } export const detectImageMimeType = (buffer: Buffer): string | null => { if (buffer.byteLength < 8) return null if ( buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47 && buffer[4] === 0x0d && buffer[5] === 0x0a && buffer[6] === 0x1a && buffer[7] === 0x0a ) { return 'image/png' } if (buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) { return 'image/jpeg' } if (buffer[0] === 0x47 && buffer[1] === 0x49 && buffer[2] === 0x46 && buffer[3] === 0x38) { return 'image/gif' } if ( buffer[0] === 0x52 && buffer[1] === 0x49 && buffer[2] === 0x46 && buffer[3] === 0x46 && buffer[8] === 0x57 && buffer[9] === 0x45 && buffer[10] === 0x42 && buffer[11] === 0x50 ) { return 'image/webp' } return null } export const sanitizeFilename = (filename: string): string => { let sanitized = filename.replace(/[<>:"/\\|?*\x00-\x1f]/g, '_') sanitized = sanitized.replace(/^\.+|\.+$/g, '') sanitized = sanitized.replace(/\.{2,}/g, '.') if (sanitized.length > 200) { const ext = path.extname(filename) const baseName = path.basename(filename, ext) sanitized = baseName.substring(0, 200 - ext.length) + ext } return sanitized || 'unnamed' }