Files
buzz-sheet/lib/media/validation.ts
T
2026-09-06 17:16:38 +07:00

81 lines
2.4 KiB
TypeScript

import * as z from "zod";
export const MAX_MEDIA_BYTES = 100 * 1024 * 1024;
export const IMAGE_MIME_TYPES = [
"image/png",
"image/jpeg",
"image/webp",
"image/gif",
] as const;
export const mediaUploadSchema = z.strictObject({
fileName: z.string().trim().min(1).max(255),
mimeType: z.enum(IMAGE_MIME_TYPES),
byteSize: z.number().int().positive().max(MAX_MEDIA_BYTES),
});
export type MediaUploadInput = z.output<typeof mediaUploadSchema>;
export function safeObjectFileName(fileName: string): string {
const normalized = fileName.normalize("NFKC").replace(/[\\/\u0000-\u001f\u007f]+/gu, "-");
const cleaned = normalized.replace(/[^\p{L}\p{M}\p{N}._-]+/gu, "-").replace(/-+/gu, "-");
return cleaned.replace(/^[-.]+|[-.]+$/gu, "").slice(0, 120) || "image";
}
function startsWith(bytes: Uint8Array, signature: number[]): boolean {
return signature.every((value, index) => bytes[index] === value);
}
export function hasValidImageSignature(
bytes: Uint8Array,
mimeType: (typeof IMAGE_MIME_TYPES)[number],
): boolean {
if (mimeType === "image/png") {
return startsWith(bytes, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
}
if (mimeType === "image/jpeg") {
return startsWith(bytes, [0xff, 0xd8, 0xff]);
}
if (mimeType === "image/webp") {
return (
startsWith(bytes, [0x52, 0x49, 0x46, 0x46]) &&
bytes[8] === 0x57 &&
bytes[9] === 0x45 &&
bytes[10] === 0x42 &&
bytes[11] === 0x50
);
}
return (
startsWith(bytes, [0x47, 0x49, 0x46, 0x38, 0x37, 0x61]) ||
startsWith(bytes, [0x47, 0x49, 0x46, 0x38, 0x39, 0x61])
);
}
export function parseByteRange(
rangeHeader: string | null,
size: number,
): { start: number; end: number } | null {
if (!rangeHeader) return null;
const match = /^bytes=(\d*)-(\d*)$/u.exec(rangeHeader.trim());
if (!match) return null;
const [, startText, endText] = match;
if (!startText && !endText) return null;
if (!startText) {
const suffix = Number(endText);
if (!Number.isSafeInteger(suffix) || suffix <= 0) return null;
return { start: Math.max(0, size - suffix), end: size - 1 };
}
const start = Number(startText);
const end = endText ? Number(endText) : size - 1;
if (
!Number.isSafeInteger(start) ||
!Number.isSafeInteger(end) ||
start < 0 ||
end < start ||
start >= size
) {
return null;
}
return { start, end: Math.min(end, size - 1) };
}