From 7dfeb381295bc38bafbcd815867176ad25d07148 Mon Sep 17 00:00:00 2001 From: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com> Date: Fri, 10 Oct 2025 12:44:42 -0700 Subject: [PATCH] Use mp4san --- ts/test-electron/backup/integration_test.ts | 2 +- .../backups => }/util/MemoryStream.ts | 5 +-- ts/util/handleVideoAttachment.ts | 44 +++++++++++++++++-- ts/util/processAttachment.ts | 2 +- 4 files changed, 44 insertions(+), 9 deletions(-) rename ts/{services/backups => }/util/MemoryStream.ts (74%) diff --git a/ts/test-electron/backup/integration_test.ts b/ts/test-electron/backup/integration_test.ts index d2d5eccca4..9bcfb8555f 100644 --- a/ts/test-electron/backup/integration_test.ts +++ b/ts/test-electron/backup/integration_test.ts @@ -15,8 +15,8 @@ import { assert } from 'chai'; import { clearData } from './helpers.js'; import { loadAllAndReinitializeRedux } from '../../services/allLoaders.js'; import { backupsService, BackupType } from '../../services/backups/index.js'; -import { MemoryStream } from '../../services/backups/util/MemoryStream.js'; import { initialize as initializeExpiringMessageService } from '../../services/expiringMessagesDeletion.js'; +import { MemoryStream } from '../../util/MemoryStream.js'; const { BACKUP_INTEGRATION_DIR } = process.env; diff --git a/ts/services/backups/util/MemoryStream.ts b/ts/util/MemoryStream.ts similarity index 74% rename from ts/services/backups/util/MemoryStream.ts rename to ts/util/MemoryStream.ts index 260dbe788d..a61f3558fb 100644 --- a/ts/services/backups/util/MemoryStream.ts +++ b/ts/util/MemoryStream.ts @@ -1,17 +1,16 @@ // Copyright 2025 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import { type Buffer } from 'node:buffer'; import { InputStream } from '@signalapp/libsignal-client/dist/io.js'; export class MemoryStream extends InputStream { #offset = 0; - constructor(private readonly buffer: Buffer) { + constructor(private readonly buffer: Uint8Array) { super(); } - public override async read(amount: number): Promise { + public override async read(amount: number): Promise { const result = this.buffer.subarray(this.#offset, this.#offset + amount); this.#offset += amount; return result; diff --git a/ts/util/handleVideoAttachment.ts b/ts/util/handleVideoAttachment.ts index ce79f5c9e9..41c84ef40f 100644 --- a/ts/util/handleVideoAttachment.ts +++ b/ts/util/handleVideoAttachment.ts @@ -2,23 +2,59 @@ // SPDX-License-Identifier: AGPL-3.0-only import { blobToArrayBuffer } from 'blob-util'; +import { sanitize } from '@signalapp/libsignal-client/dist/Mp4Sanitizer.js'; import { v4 as generateUuid } from 'uuid'; import { makeVideoScreenshot } from '../types/VisualAttachment.js'; import { IMAGE_PNG, stringToMIMEType } from '../types/MIME.js'; +import { toLogFormat } from '../types/errors.js'; import type { InMemoryAttachmentDraftType } from '../types/Attachment.js'; +import { createLogger } from '../logging/log.js'; +import { MemoryStream } from './MemoryStream.js'; import { fileToBytes } from './fileToBytes.js'; +const log = createLogger('handleVideoAttachment'); + export async function handleVideoAttachment( file: File, - options?: { generateScreenshot: boolean; flags: number | null } + options: { + generateScreenshot: boolean; + flags: number | null; + } ): Promise { const objectUrl = URL.createObjectURL(file); if (!objectUrl) { throw new Error('Failed to create object url for video!'); } try { - const data = await fileToBytes(file); + let data = await fileToBytes(file); + + if (file.type === 'video/mp4') { + try { + const result = await sanitize( + new MemoryStream(data), + BigInt(data.byteLength) + ); + const metadata = result.getMetadata(); + + // If there is no metadata - mp4 is already in the fast state! + if (metadata != null) { + const dataLen = Number(result.getDataLen()); + const dataOffset = Number(result.getDataOffset()); + const sanitized = new Uint8Array(dataLen + metadata.byteLength); + sanitized.set(metadata, 0); + sanitized.set( + data.subarray(dataOffset, dataOffset + dataLen), + metadata.byteLength + ); + + data = sanitized; + } + } catch (error) { + log.warn(`Failed to mp4san video ${toLogFormat(error)}`); + } + } + const attachment: InMemoryAttachmentDraftType = { contentType: stringToMIMEType(file.type), clientUuid: generateUuid(), @@ -29,7 +65,7 @@ export async function handleVideoAttachment( size: data.byteLength, }; - if (options?.generateScreenshot) { + if (options.generateScreenshot) { const screenshotContentType = IMAGE_PNG; const { blob: screenshotBlob, duration } = await makeVideoScreenshot({ @@ -43,7 +79,7 @@ export async function handleVideoAttachment( attachment.screenshotContentType = screenshotContentType; } - if (options?.flags != null) { + if (options.flags != null) { attachment.flags = options.flags; } diff --git a/ts/util/processAttachment.ts b/ts/util/processAttachment.ts index 8ec276a3f6..ec9690def3 100644 --- a/ts/util/processAttachment.ts +++ b/ts/util/processAttachment.ts @@ -28,7 +28,7 @@ const log = createLogger('processAttachment'); export async function processAttachment( file: File, - options?: { generateScreenshot: boolean; flags: number | null } + options: { generateScreenshot: boolean; flags: number | null } ): Promise { const fileType = stringToMIMEType(file.type);