export interface IfCondition { not: boolean; kind: "lock-token" | "state-token" | "etag"; value: string; } export interface IfList { resourceHref?: string; conditions: IfCondition[]; } export interface ParsedIfHeader { lists: IfList[]; } export type IfHeaderResult = { ok: true; header: ParsedIfHeader } | { ok: false; message: string }; class IfCursor { index = 0; constructor(readonly value: string) {} done(): boolean { return this.index >= this.value.length; } peek(): string { return this.value[this.index] ?? ""; } skipWs(): void { while (/\s/.test(this.peek())) this.index += 1; } consume(char: string): boolean { this.skipWs(); if (this.peek() !== char) return false; this.index += 1; return true; } readUntil(end: string): string | null { const start = this.index; const endIndex = this.value.indexOf(end, start); if (endIndex === -1) return null; this.index = endIndex + end.length; return this.value.slice(start, endIndex); } readWord(): string { const start = this.index; while (/[A-Za-z]/.test(this.peek())) this.index += 1; return this.value.slice(start, this.index); } } function parseCondition(cursor: IfCursor): IfCondition | null { cursor.skipWs(); let not = false; const wordStart = cursor.index; const word = cursor.readWord(); if (word.toLowerCase() === "not") { not = true; cursor.skipWs(); } else { cursor.index = wordStart; } const opener = cursor.peek(); if (opener === "<") { cursor.index += 1; const value = cursor.readUntil(">"); if (!value) return null; return { not, kind: value.startsWith("opaquelocktoken:") ? "lock-token" : "state-token", value }; } if (opener === "[") { cursor.index += 1; const value = cursor.readUntil("]"); if (!value) return null; return { not, kind: "etag", value }; } return null; } export function parseIfHeader(value: string | null): IfHeaderResult { if (!value) return { ok: true, header: { lists: [] } }; const cursor = new IfCursor(value); const lists: IfList[] = []; let currentResource: string | undefined; while (!cursor.done()) { cursor.skipWs(); if (cursor.done()) break; if (cursor.peek() === "<") { cursor.index += 1; const href = cursor.readUntil(">"); if (!href) return { ok: false, message: "Invalid tagged If header" }; currentResource = href; continue; } if (!cursor.consume("(")) return { ok: false, message: "Invalid If header list" }; const conditions: IfCondition[] = []; while (!cursor.done()) { cursor.skipWs(); if (cursor.consume(")")) break; const condition = parseCondition(cursor); if (!condition) return { ok: false, message: "Invalid If header condition" }; conditions.push(condition); } if (conditions.length === 0) return { ok: false, message: "Empty If header list" }; lists.push({ resourceHref: currentResource, conditions }); } return { ok: true, header: { lists } }; } export function positiveLockTokens(header: ParsedIfHeader): string[] { const tokens = new Set(); for (const list of header.lists) { for (const condition of list.conditions) { if (!condition.not && condition.kind === "lock-token") tokens.add(condition.value); } } return [...tokens]; } export function positiveEtags(header: ParsedIfHeader): string[] { const etags = new Set(); for (const list of header.lists) { for (const condition of list.conditions) { if (!condition.not && condition.kind === "etag") etags.add(condition.value); } } return [...etags]; } export function normalizeIfResourceHref(href: string): string { try { return new URL(href).pathname; } catch { return href; } } export function ifHeaderMatchesStateAndEtags(input: { header: ParsedIfHeader; targetHref: string; targetEtag: string | null; getEtag?: (href: string) => string | null; getStateToken?: (href: string) => string | null; }): boolean { if (input.header.lists.length === 0) return true; for (const list of input.header.lists) { const conditionHref = list.resourceHref ? normalizeIfResourceHref(list.resourceHref) : input.targetHref; const listMatches = list.conditions.every((condition) => { const matches = condition.kind === "etag" ? (conditionHref === input.targetHref ? input.targetEtag : (input.getEtag?.(conditionHref) ?? null)) === condition.value : condition.kind === "state-token" ? (input.getStateToken?.(conditionHref) ?? null) === condition.value : false; return condition.not ? !matches : matches; }); if (listMatches) return true; } return false; }