aegida-console / lib / chat / attachments.ts
attachments.ts
Raw
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);
}