Initial Commit
This commit is contained in:
195
packages/media/image/caddy-imgproxy/src/parse/parse-url.ts
Normal file
195
packages/media/image/caddy-imgproxy/src/parse/parse-url.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
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 };
|
||||
}
|
||||
Reference in New Issue
Block a user