diff --git a/src/backend/blobHandler.js b/src/backend/blobHandler.js index 0de44ea..cb2c3c2 100644 --- a/src/backend/blobHandler.js +++ b/src/backend/blobHandler.js @@ -85,11 +85,12 @@ const stripJpeg = (buf) => { const out = [buf.subarray(0, 2)]; let orientation = 0; let i = 2; + let scan = false; while (i + 4 <= buf.length) { if (buf[i] !== 0xFF) return null; // no es un JPEG sano: no tocarlo const marker = buf[i + 1]; if (marker === 0xD8 || (marker >= 0xD0 && marker <= 0xD9)) { i += 2; continue; } - if (marker === 0xDA) { out.push(buf.subarray(i)); break; } // datos comprimidos: hasta el final + if (marker === 0xDA) { out.push(buf.subarray(i)); scan = true; break; } // datos comprimidos: hasta el final const len = buf.readUInt16BE(i + 2); if (len < 2 || i + 2 + len > buf.length) return null; const seg = buf.subarray(i + 4, i + 2 + len); @@ -97,6 +98,8 @@ const stripJpeg = (buf) => { if (!JPEG_DROP.has(marker)) out.push(buf.subarray(i, i + 2 + len)); i += 2 + len; } + // sin datos de imagen (SOS) es un JPEG cortado: devolver la cabecera sola subiria una foto vacia + if (!scan) return null; if (orientation > 1) { const exif = exifOnlyOrientation(orientation); const head = Buffer.alloc(4); @@ -150,14 +153,40 @@ const stripWebp = (buf) => { return Buffer.concat([head, body]); }; -/** Limpia segun el formato; si algo no cuadra, devuelve la imagen tal cual. */ -const stripMetadataPure = (buffer) => { - try { - const out = stripJpeg(buffer) || stripPng(buffer) || stripWebp(buffer); - return out && out.length ? out : buffer; - } catch { - return buffer; +// Formatos que pueden llevar EXIF (GPS incluido) y que aqui no se saben limpiar: +// HEIC/HEIF/AVIF (caja ftyp) y TIFF. Antes subian tal cual, con la ubicacion. +const isUncleanable = (buf) => { + if (buf.length >= 12 && buf.toString("latin1", 4, 8) === "ftyp") { + return /^(heic|heix|hevc|hevx|heim|heis|mif1|msf1|avif|avis)$/.test(buf.toString("latin1", 8, 12)); } + return buf.length >= 4 && (buf.toString("latin1", 0, 4) === "II*\0" || buf.toString("latin1", 0, 4) === "MM\0*"); +}; +const looksCleanable = (buf) => + (buf.length >= 2 && buf[0] === 0xFF && buf[1] === 0xD8) || + (buf.length >= 8 && buf.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]))) || + (buf.length >= 12 && buf.toString("latin1", 0, 4) === "RIFF" && buf.toString("latin1", 8, 12) === "WEBP"); + +const unsafe = () => { + let msg = "This image could not be cleaned of its metadata, so it has not been uploaded."; + try { msg = require("../views/main_views").i18n.imageMetadataUnsafe || msg; } catch (_) {} + const err = new Error(msg); + err.status = 400; + return err; +}; + +/** + * Limpia segun el formato. Si es JPEG/PNG/WebP y no se puede limpiar, o es un + * formato con EXIF que no sabemos limpiar, LANZA: es mejor no subir la foto que + * subirla con la ubicacion. Formatos sin metadatos de ese tipo (BMP, SVG, ICO...) + * pasan tal cual. + */ +const stripMetadataPure = (buffer) => { + if (isUncleanable(buffer)) throw unsafe(); + if (!looksCleanable(buffer)) return buffer; + let out = null; + try { out = stripJpeg(buffer) || stripPng(buffer) || stripWebp(buffer); } catch (_) { out = null; } + if (!out || !out.length) throw unsafe(); + return out; }; const stripImageMetadata = async (buffer) => { diff --git a/src/client/assets/translations/i18n_fork.js b/src/client/assets/translations/i18n_fork.js index 35b2dc4..6eb8379 100644 --- a/src/client/assets/translations/i18n_fork.js +++ b/src/client/assets/translations/i18n_fork.js @@ -17,6 +17,8 @@ module.exports = { en: { + // fotos que no se pueden limpiar (blobHandler.js) + imageMetadataUnsafe: "This image could not be cleaned of its metadata (location, camera…), so it has not been uploaded. Save it as JPEG or PNG and try again.", // Karvan karvanActive: "Active rooms", karvanBlurb: "Rooms that live only in memory and destroy themselves.", @@ -80,6 +82,8 @@ module.exports = { spreadMore: "more" }, es: { + // fotos que no se pueden limpiar (blobHandler.js) + imageMetadataUnsafe: "No se han podido quitar los metadatos de esta imagen (ubicación, cámara…), así que no se ha subido. Guárdala como JPEG o PNG y vuelve a intentarlo.", // Karvan karvanActive: "Salas activas", karvanBlurb: "Salas que viven solo en memoria y se destruyen solas.", @@ -149,6 +153,8 @@ module.exports = { totalspreads: "Réplicas totales" }, fr: { + // fotos que no se pueden limpiar (blobHandler.js) + imageMetadataUnsafe: "Impossible de retirer les métadonnées de cette image (position, appareil…), elle n'a donc pas été envoyée. Enregistrez-la en JPEG ou PNG et réessayez.", // Karvan karvanActive: "Salons actifs", karvanBlurb: "Des salons qui ne vivent qu'en mémoire et se détruisent d'eux-mêmes.", @@ -212,6 +218,8 @@ module.exports = { spreadMore: "plus" }, eu: { + // fotos que no se pueden limpiar (blobHandler.js) + imageMetadataUnsafe: "Ezin izan dira irudi honen metadatuak kendu (kokapena, kamera…), beraz ez da igo. Gorde JPEG edo PNG gisa eta saiatu berriro.", // Karvan karvanActive: "Gela aktiboak", karvanBlurb: "Memorian bakarrik bizi diren eta beren kabuz suntsitzen diren gelak.", @@ -275,6 +283,8 @@ module.exports = { spreadMore: "gehiago" }, de: { + // fotos que no se pueden limpiar (blobHandler.js) + imageMetadataUnsafe: "Die Metadaten dieses Bildes (Standort, Kamera…) konnten nicht entfernt werden, daher wurde es nicht hochgeladen. Speichere es als JPEG oder PNG und versuche es erneut.", // Karvan karvanActive: "Aktive Räume", karvanBlurb: "Räume, die nur im Speicher leben und sich von selbst zerstören.", @@ -338,6 +348,8 @@ module.exports = { spreadMore: "mehr" }, it: { + // fotos que no se pueden limpiar (blobHandler.js) + imageMetadataUnsafe: "Non è stato possibile rimuovere i metadati di questa immagine (posizione, fotocamera…), quindi non è stata caricata. Salvala come JPEG o PNG e riprova.", // Karvan karvanActive: "Stanze attive", karvanBlurb: "Stanze che vivono solo in memoria e si distruggono da sole.", @@ -401,6 +413,8 @@ module.exports = { spreadMore: "altro" }, pt: { + // fotos que no se pueden limpiar (blobHandler.js) + imageMetadataUnsafe: "Não foi possível remover os metadados desta imagem (localização, câmara…), por isso não foi enviada. Guarda-a como JPEG ou PNG e tenta de novo.", // Karvan karvanActive: "Salas ativas", karvanBlurb: "Salas que vivem apenas em memória e destroem-se sozinhas.", @@ -464,6 +478,8 @@ module.exports = { spreadMore: "mais" }, zh: { + // fotos que no se pueden limpiar (blobHandler.js) + imageMetadataUnsafe: "无法清除这张图片的元数据(位置、相机等),因此没有上传。请另存为 JPEG 或 PNG 后重试。", // Karvan karvanActive: "活跃房间", karvanBlurb: "只存在于内存中、会自行销毁的房间。", @@ -527,6 +543,8 @@ module.exports = { spreadMore: "更多" }, ar: { + // fotos que no se pueden limpiar (blobHandler.js) + imageMetadataUnsafe: "تعذّر إزالة البيانات الوصفية من هذه الصورة (الموقع، الكاميرا…)، لذلك لم تُرفع. احفظها بصيغة JPEG أو PNG وحاول مجددًا.", // Karvan karvanActive: "الغرف النشطة", karvanBlurb: "غرف تعيش في الذاكرة فقط وتدمّر نفسها بنفسها.", @@ -590,6 +608,8 @@ module.exports = { spreadMore: "المزيد" }, hi: { + // fotos que no se pueden limpiar (blobHandler.js) + imageMetadataUnsafe: "इस छवि का मेटाडेटा (स्थान, कैमरा…) हटाया नहीं जा सका, इसलिए इसे अपलोड नहीं किया गया। इसे JPEG या PNG के रूप में सहेजें और फिर कोशिश करें।", // Karvan karvanActive: "सक्रिय कक्ष", karvanBlurb: "ऐसे कक्ष जो केवल मेमोरी में रहते हैं और स्वयं नष्ट हो जाते हैं।", @@ -653,6 +673,8 @@ module.exports = { spreadMore: "और" }, ru: { + // fotos que no se pueden limpiar (blobHandler.js) + imageMetadataUnsafe: "Не удалось удалить метаданные этого изображения (местоположение, камера…), поэтому оно не загружено. Сохраните его как JPEG или PNG и попробуйте снова.", // Karvan karvanActive: "Активные комнаты", karvanBlurb: "Комнаты, которые живут только в памяти и уничтожаются сами.",