export type DeadPropInstruction = | { kind: "set"; nsUri: string; localName: string; xmlValue: string } | { kind: "remove"; nsUri: string; localName: string }; export function validDavCollectionName(name: string): boolean { return name.length > 0 && name.length <= 128 && name !== "." && name !== ".." && !/[\\/]/.test(name); } export function davSyncToken(seq: number): string { return `sync:${seq}`; } export function parseDavSyncToken(token: string | null): number { if (!token) return 0; const match = /^sync:(\d+)$/.exec(token); return match ? Number.parseInt(match[1]!, 10) : 0; } export function parseDavSyncTokenStrict(token: string | null): number | null { if (!token) return 0; const match = /^sync:(\d+)$/.exec(token); return match ? Number.parseInt(match[1]!, 10) : null; } export function objectNameFromCollectionHref( collectionHref: string, href: string, acceptedOrigins?: string[], ): string | null { let pathname: string; const absolute = href.startsWith("http://") || href.startsWith("https://"); try { if (absolute) { const parsed = new URL(href); // RFC 4791 7.9 / RFC 6352 8.7: absolute hrefs in multiget must resolve to // the same subject host. Reject foreign authorities outright so a request // cannot leak local objects under a different origin's name. if (acceptedOrigins && acceptedOrigins.length > 0 && !acceptedOrigins.includes(parsed.origin)) { return null; } pathname = parsed.pathname; } else { pathname = href; } } catch { return null; } if (!pathname.startsWith(collectionHref)) return null; const rawName = pathname.slice(collectionHref.length); if (!rawName || rawName.includes("/")) return null; try { return decodeURIComponent(rawName); } catch { return null; } } export function applyDeadPropInstructions( instructions: DeadPropInstruction[], handlers: { set: (instruction: Extract) => void; remove: (instruction: Extract) => void; }, ): void { for (const instruction of instructions) { if (instruction.kind === "set") handlers.set(instruction); else handlers.remove(instruction); } } // Extract text content from a serialized prop element such as // "Family". The serializer preserves nested // markup, so we strip the outermost element and decode common entities. export function extractPropTextValue(xmlValue: string): string { const match = /^<[^>]+>([\s\S]*)<\/[^>]+>$/.exec(xmlValue.trim()); const inner = match ? (match[1] ?? "") : xmlValue; return inner .replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">") .replace(/"/g, '"') .replace(/'/g, "'"); }