81 lines
2.4 KiB
TypeScript
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) };
|
|
}
|