import fs from 'node:fs'; import path from 'node:path'; import crypto from 'node:crypto'; import { TextDecoder } from 'node:util'; import multer from 'multer'; import { fileTypeFromFile } from 'file-type'; import { detectAv } from '@file-type/av'; import { config } from '../config.js'; import { HttpError } from './errors.js'; const MAX_GPX_PREFIX_BYTES = 131072; const BASE64_PATTERN = /^[A-Za-z0-9+/]*={0,2}$/; const IGNORED_XML_MARKUP = Object.freeze([ Object.freeze({ opening: '' }), Object.freeze({ opening: '' }), Object.freeze({ opening: '' }) ]); const ALLOWED_TYPES = Object.freeze({ picture: Object.freeze({ mime: new Set(['image/jpeg', 'image/png', 'image/webp']), ext: new Set(['jpg', 'png', 'webp']) }), audio: Object.freeze({ mime: new Set(['audio/mpeg', 'audio/mp4', 'audio/aac', 'audio/ogg', 'audio/wav', 'audio/webm']), ext: new Set(['mp3', 'm4a', 'aac', 'ogg', 'opus', 'spx', 'wav', 'webm']) }) }); function sanitizeFilename(value) { return String(value).replaceAll(/[^a-zA-Z0-9._-]/g, '_') || 'upload'; } function stripBase64Padding(value) { let end = value.length; while (end > 0 && value[end - 1] === '=') end -= 1; return value.slice(0, end); } function base64FromDataUrl(value, fieldName) { const dataUrl = value.trim(); const marker = ';base64,'; const markerIndex = dataUrl.indexOf(marker); if (!dataUrl.startsWith('data:') || markerIndex < 5) { throw new HttpError(400, `${fieldName}.dataUrl muss eine Base64-Data-URL sein.`); } return dataUrl.slice(markerIndex + marker.length); } function isXmlNameCharacter(character, first = false) { const code = character.codePointAt(0); const letter = (code >= 65 && code <= 90) || (code >= 97 && code <= 122); if (letter || character === '_') return true; return !first && ((code >= 48 && code <= 57) || character === '.' || character === '-' || character === ':'); } function ignoredMarkupEnd(xml, opening) { const markup = IGNORED_XML_MARKUP.find(item => xml.startsWith(item.opening, opening)); if (!markup) return null; const closing = xml.indexOf(markup.closing, opening + markup.opening.length); return closing < 0 ? -1 : closing + markup.closing.length; } function readXmlName(xml, start) { if (start >= xml.length || !isXmlNameCharacter(xml[start], true)) return null; let end = start + 1; while (end < xml.length && isXmlNameCharacter(xml[end])) end += 1; return xml.slice(start, end); } function rootElementName(xml) { let cursor = 0; while (cursor < xml.length) { const opening = xml.indexOf('<', cursor); if (opening < 0 || opening + 1 >= xml.length) return null; const nextCursor = ignoredMarkupEnd(xml, opening); if (nextCursor === -1) return null; if (nextCursor != null) { cursor = nextCursor; continue; } if (xml.startsWith('