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<ValidatedAttachment> {
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);
}