export const MAX_ATTACHMENTS_PER_MESSAGE = 10; export const MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024; export const allowedAttachmentTypes = new Set([ "image/png", "image/jpeg", "image/webp", "application/pdf", "text/plain", "text/markdown", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", ]); export class AttachmentValidationError extends Error { readonly status = 400; constructor() { super("Недопустимое вложение"); this.name = "AttachmentValidationError"; } } export type ValidatedAttachment = { name: string; contentType: string; size: number; }; export async function validateAttachmentUpload(file: File): Promise { if (!allowedAttachmentTypes.has(file.type) || file.size === 0 || file.size > MAX_ATTACHMENT_BYTES) { throw new AttachmentValidationError(); } const bytes = new Uint8Array(await file.slice(0, 16).arrayBuffer()); if (!hasExpectedSignature(file.type, bytes)) throw new AttachmentValidationError(); const name = normalizeAttachmentName(file.name); if (!name) throw new AttachmentValidationError(); return { name, contentType: file.type, size: file.size }; } export function normalizeAttachmentName(name: string) { return name.replaceAll("\\", "/").split("/").at(-1)?.trim().slice(0, 255) ?? ""; } function hasExpectedSignature(contentType: string, bytes: Uint8Array) { if (contentType === "image/png") return startsWith(bytes, [0x89, 0x50, 0x4e, 0x47]); if (contentType === "image/jpeg") return startsWith(bytes, [0xff, 0xd8, 0xff]); if (contentType === "image/webp") return startsWith(bytes, [0x52, 0x49, 0x46, 0x46]) && String.fromCharCode(...bytes.slice(8, 12)) === "WEBP"; if (contentType === "application/pdf") return startsWith(bytes, [0x25, 0x50, 0x44, 0x46, 0x2d]); if (contentType.includes("wordprocessingml")) return startsWith(bytes, [0x50, 0x4b, 0x03, 0x04]); return true; } function startsWith(bytes: Uint8Array, expected: number[]) { return expected.every((value, index) => bytes[index] === value); }