import { HttpException, HttpStatus } from "@nestjs/common"; /** Carries the plaintext size for the controller’s unsatisfied Content-Range header. */ export class FileRangeNotSatisfiableException extends HttpException { constructor(readonly total: number) { super("requested range is not satisfiable", HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE); } } /** Inclusive HTTP byte bounds, clipped to the plaintext EOF. */ export function playbackRange(total: number, start: number, end: number): { start: number; end: number } { if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 0 || start >= total || end < start) { throw new FileRangeNotSatisfiableException(total); } return { start, end: Math.min(end, total - 1) }; } /** Query bounds take precedence; also accept the single Range browsers send. */ export function requestedPlaybackRange(total: number, start: unknown, end: unknown, range?: string): { start: number; end: number } { const fail = (): never => { throw new FileRangeNotSatisfiableException(total); }; const integer = (value: unknown): number => { if (typeof value !== "string" || !/^\d+$/.test(value) || !Number.isSafeInteger(Number(value))) return fail(); return Number(value); }; if (start !== undefined || end !== undefined) { return playbackRange(total, start === undefined ? 0 : integer(start), end === undefined ? total - 1 : integer(end)); } if (range === undefined) return playbackRange(total, 0, total - 1); const match = /^bytes=(\d*)-(\d*)$/.exec(range); if (match === null || (match[1] === "" && match[2] === "")) return fail(); if (match[1] === "") { const suffix = integer(match[2]); if (suffix === 0) return fail(); return playbackRange(total, Math.max(0, total - suffix), total - 1); } return playbackRange(total, integer(match[1]), match[2] === "" ? total - 1 : integer(match[2])); }