196 lines
5.4 KiB
TypeScript
196 lines
5.4 KiB
TypeScript
import {
|
|
CADDY_IMGPROXY_CODES,
|
|
CaddyImgproxyError,
|
|
} from "../error-codes/error-codes.js";
|
|
import { recordParseOutcome } from "../telemetry/telemetry.js";
|
|
import type {
|
|
GravityCode,
|
|
HintType,
|
|
ImgproxyOperation,
|
|
ParsedImgproxyUrl,
|
|
} from "../types/types.js";
|
|
|
|
/**
|
|
* Matches the compact imgproxy URL grammar. Capture groups:
|
|
* 1: signed/unsigned (`s`|`u`)
|
|
* 2: operation (`v`|`c`|`f`|`s`|`r`) — optional
|
|
* 3: gravity (`tl`|`tr`|`bl`|`br`|`t`|`b`|`l`|`r`|`c`) — optional
|
|
* 4: aspect W (>=1)
|
|
* 5: aspect H (>=1)
|
|
* 6: pixel W (>=1, when followed by xH)
|
|
* 7: pixel H (>=0)
|
|
* 8: pixel W (=0)
|
|
* 9: pixel H (>=1, when W=0)
|
|
* 10: square dim
|
|
* 11: modifiers
|
|
* 12: path
|
|
* 13: format
|
|
*/
|
|
const URL_PATTERN =
|
|
/^\/([su])([vcfsr])?(tl|tr|bl|br|[tblrc])?(?:([1-9]\d*):([1-9]\d*)|([1-9]\d*)x(\d+)|(0)x([1-9]\d*)|([1-9]\d*))?((?:[wh][1-9]\d*|q(?:100|[1-9]\d?))*)\/(.+)\.([a-z]+)$/;
|
|
|
|
/** Extracts individual `w<int>`, `h<int>`, `q<int>` modifiers. */
|
|
const MODIFIER_PATTERN = /([whq])(\d+)/g;
|
|
|
|
const OP_MAP: Record<string, ImgproxyOperation> = {
|
|
v: "cover",
|
|
c: "crop",
|
|
f: "fit",
|
|
s: "stretch",
|
|
r: "resize",
|
|
};
|
|
|
|
/**
|
|
* Parses an imgproxy URL into its typed components.
|
|
*
|
|
* Verbatim port of the upstream Go parseURL. Returns a frozen
|
|
* ParsedImgproxyUrl on success and throws CaddyImgproxyError on
|
|
* every failure. Increments caddy_imgproxy_parse_total once per call.
|
|
*/
|
|
export function parseImgproxyUrl(path: string): ParsedImgproxyUrl {
|
|
const matches = URL_PATTERN.exec(path);
|
|
if (matches === null) {
|
|
recordParseOutcome("invalid");
|
|
throw new CaddyImgproxyError(
|
|
CADDY_IMGPROXY_CODES.INVALID_URL,
|
|
"imgproxy URL does not match the expected grammar",
|
|
{ context: { path } }
|
|
);
|
|
}
|
|
|
|
const opToken = matches[2] ?? "";
|
|
const operation =
|
|
opToken === "" ? "" : (OP_MAP[opToken] as ImgproxyOperation);
|
|
const gravity = resolveGravity(matches[3]);
|
|
const { aspectW, aspectH, pixelW, pixelH, isAspect, hasDims } =
|
|
resolveDimensions(matches);
|
|
const { hintType, hintValue, quality } = parseModifiers(
|
|
matches[11] as string
|
|
);
|
|
|
|
if (operation !== "" && !hasDims) {
|
|
recordParseOutcome("missing_dims");
|
|
throw new CaddyImgproxyError(
|
|
CADDY_IMGPROXY_CODES.MISSING_DIMENSIONS,
|
|
`dimensions required for operation: ${operation}`,
|
|
{ context: { path, operation } }
|
|
);
|
|
}
|
|
|
|
recordParseOutcome("ok");
|
|
return Object.freeze({
|
|
signed: matches[1] === "s",
|
|
operation,
|
|
gravity,
|
|
aspectW,
|
|
aspectH,
|
|
pixelW,
|
|
pixelH,
|
|
hintType,
|
|
hintValue,
|
|
quality,
|
|
path: matches[12] as string,
|
|
format: matches[13] as string,
|
|
isAspect,
|
|
hasDims,
|
|
});
|
|
}
|
|
|
|
function resolveGravity(token: string | undefined): GravityCode {
|
|
if (token === undefined || token === "") return "c";
|
|
return token as GravityCode;
|
|
}
|
|
|
|
interface DimensionParts {
|
|
readonly aspectW: number;
|
|
readonly aspectH: number;
|
|
readonly pixelW: number;
|
|
readonly pixelH: number;
|
|
readonly isAspect: boolean;
|
|
readonly hasDims: boolean;
|
|
}
|
|
|
|
function resolveDimensions(matches: RegExpExecArray): DimensionParts {
|
|
if (matches[4] !== undefined && matches[5] !== undefined) {
|
|
return {
|
|
aspectW: Number.parseInt(matches[4], 10),
|
|
aspectH: Number.parseInt(matches[5], 10),
|
|
pixelW: 0,
|
|
pixelH: 0,
|
|
isAspect: true,
|
|
hasDims: true,
|
|
};
|
|
}
|
|
if (matches[6] !== undefined) {
|
|
return {
|
|
aspectW: 0,
|
|
aspectH: 0,
|
|
pixelW: Number.parseInt(matches[6], 10),
|
|
pixelH: Number.parseInt(matches[7] as string, 10),
|
|
isAspect: false,
|
|
hasDims: true,
|
|
};
|
|
}
|
|
if (matches[8] !== undefined) {
|
|
return {
|
|
aspectW: 0,
|
|
aspectH: 0,
|
|
pixelW: 0,
|
|
pixelH: Number.parseInt(matches[9] as string, 10),
|
|
isAspect: false,
|
|
hasDims: true,
|
|
};
|
|
}
|
|
if (matches[10] !== undefined) {
|
|
const dim = Number.parseInt(matches[10], 10);
|
|
return {
|
|
aspectW: 0,
|
|
aspectH: 0,
|
|
pixelW: dim,
|
|
pixelH: dim,
|
|
isAspect: false,
|
|
hasDims: true,
|
|
};
|
|
}
|
|
return {
|
|
aspectW: 0,
|
|
aspectH: 0,
|
|
pixelW: 0,
|
|
pixelH: 0,
|
|
isAspect: false,
|
|
hasDims: false,
|
|
};
|
|
}
|
|
|
|
interface ModifierParts {
|
|
readonly hintType: HintType;
|
|
readonly hintValue: number;
|
|
readonly quality: number;
|
|
}
|
|
|
|
function parseModifiers(token: string): ModifierParts {
|
|
let hintType: HintType = "";
|
|
let hintValue = 0;
|
|
let quality = 0;
|
|
if (token === "") {
|
|
return { hintType, hintValue, quality };
|
|
}
|
|
MODIFIER_PATTERN.lastIndex = 0;
|
|
let match: RegExpExecArray | null = MODIFIER_PATTERN.exec(token);
|
|
while (match !== null) {
|
|
const kind = match[1];
|
|
const value = Number.parseInt(match[2] as string, 10);
|
|
if (kind === "w") {
|
|
hintType = "w";
|
|
hintValue = value;
|
|
} else if (kind === "h") {
|
|
hintType = "h";
|
|
hintValue = value;
|
|
} else {
|
|
quality = value;
|
|
}
|
|
match = MODIFIER_PATTERN.exec(token);
|
|
}
|
|
return { hintType, hintValue, quality };
|
|
}
|