{"version":3,"file":"purl.mjs","names":[],"sources":["../../src/core/purl.ts"],"sourcesContent":["import type { ParsedPURL } from \"./types.ts\";\nimport type { Client } from \"./client.ts\";\nimport { create, type Registry } from \"./registry.ts\";\nimport { InvalidPURLError } from \"./errors.ts\";\n\nconst QUALIFIER_KEY_PATTERN = /^[a-z][a-z0-9._-]*$/;\n\nfunction decodePURLComponent(purlStr: string, value: string): string {\n  try {\n    return decodeURIComponent(value);\n  } catch {\n    throw new InvalidPURLError(purlStr, \"malformed percent-encoding\");\n  }\n}\n\nfunction extractSubpath(purlStr: string, value: string): { remainder: string; subpath: string } {\n  const hashIdx = value.indexOf(\"#\");\n  if (hashIdx === -1) return { remainder: value, subpath: \"\" };\n\n  const subpath = value\n    .slice(hashIdx + 1)\n    .split(\"/\")\n    .map((segment) => decodePURLComponent(purlStr, segment))\n    .join(\"/\");\n  return { remainder: value.slice(0, hashIdx), subpath };\n}\n\nfunction extractQualifiers(\n  purlStr: string,\n  value: string,\n): { qualifiers: Record<string, string>; remainder: string } {\n  const queryIdx = value.indexOf(\"?\");\n  if (queryIdx === -1) return { qualifiers: {}, remainder: value };\n\n  const qualifiers: Record<string, string> = {};\n  for (const pair of value.slice(queryIdx + 1).split(\"&\")) {\n    const eqIdx = pair.indexOf(\"=\");\n    if (eqIdx !== -1) {\n      qualifiers[decodePURLComponent(purlStr, pair.slice(0, eqIdx))] = decodePURLComponent(\n        purlStr,\n        pair.slice(eqIdx + 1),\n      );\n    }\n  }\n  return { qualifiers, remainder: value.slice(0, queryIdx) };\n}\n\nfunction extractVersion(purlStr: string, value: string): { remainder: string; version: string } {\n  const lastSlashIdx = value.lastIndexOf(\"/\");\n  const lastAtIdx = value.lastIndexOf(\"@\");\n  if (lastAtIdx === -1 || lastAtIdx <= lastSlashIdx) return { remainder: value, version: \"\" };\n\n  return {\n    remainder: value.slice(0, lastAtIdx),\n    version: decodePURLComponent(purlStr, value.slice(lastAtIdx + 1)),\n  };\n}\n\n/**\n * Parse a package URL into normalized components.\n *\n * @param purlStr - Package URL to parse.\n * @returns {ParsedPURL} The normalized components.\n * @see https://github.com/package-url/purl-spec (ECMA-427)\n */\nexport function parsePURL(purlStr: string): ParsedPURL {\n  if (!purlStr.startsWith(\"pkg:\")) {\n    throw new InvalidPURLError(purlStr, 'must start with \"pkg:\"');\n  }\n\n  const subpathParts = extractSubpath(purlStr, purlStr.slice(4));\n  const qualifierParts = extractQualifiers(purlStr, subpathParts.remainder);\n  const versionParts = extractVersion(purlStr, qualifierParts.remainder);\n  const remainder = versionParts.remainder;\n\n  // Extract type\n  const slashIdx = remainder.indexOf(\"/\");\n  if (slashIdx === -1) {\n    throw new InvalidPURLError(purlStr, \"missing type/name separator\");\n  }\n\n  const type = remainder.slice(0, slashIdx).toLowerCase();\n  if (!type) {\n    throw new InvalidPURLError(purlStr, \"empty type\");\n  }\n\n  const rest = remainder.slice(slashIdx + 1);\n  if (!rest) {\n    throw new InvalidPURLError(purlStr, \"empty name\");\n  }\n\n  // Extract namespace and name\n  const lastSlashIdx = rest.lastIndexOf(\"/\");\n  let namespace = \"\";\n  let name: string;\n\n  if (lastSlashIdx !== -1) {\n    namespace = rest\n      .slice(0, lastSlashIdx)\n      .split(\"/\")\n      .map((s) => decodePURLComponent(purlStr, s))\n      .join(\"/\");\n    name = decodePURLComponent(purlStr, rest.slice(lastSlashIdx + 1));\n  } else {\n    name = decodePURLComponent(purlStr, rest);\n  }\n\n  // Ecosystem-specific normalization per PURL spec\n  if (type === \"pypi\") {\n    name = name.toLowerCase().replaceAll(/[-_.]+/g, \"-\");\n  }\n\n  if (type === \"alpm\") {\n    name = name.toLowerCase();\n  }\n\n  return {\n    type,\n    namespace,\n    name,\n    version: versionParts.version,\n    qualifiers: qualifierParts.qualifiers,\n    subpath: subpathParts.subpath,\n  };\n}\n\n/**\n * Build a registry package name from PURL components.\n *\n * @param parsed - Parsed package URL components.\n * @returns {string} The namespace-qualified package name.\n */\nexport function fullName(parsed: ParsedPURL): string {\n  if (parsed.namespace) {\n    return `${parsed.namespace}/${parsed.name}`;\n  }\n  return parsed.name;\n}\n\n/**\n * Create a registry from a package URL.\n *\n * @param purlStr - Package URL to resolve.\n * @param client - Optional HTTP client.\n * @returns {Promise<[Registry, string, string]>} The registry, package name, and version.\n */\nexport async function createFromPURL(\n  purlStr: string,\n  client?: Client,\n): Promise<[Registry, string, string]> {\n  const parsed = parsePURL(purlStr);\n  const baseURL = parsed.qualifiers[\"repository_url\"] ?? \"\";\n  const reg = await create(parsed.type, baseURL || undefined, client);\n  return [reg, fullName(parsed), parsed.version];\n}\n\n/**\n * Build a package URL from normalized components.\n *\n * @param parts - Package URL components.\n * @returns {string} The canonical package URL.\n */\nexport function buildPURL(\n  parts: Readonly<{\n    type: string;\n    name: string;\n    version?: string;\n    namespace?: string;\n    qualifiers?: Readonly<Record<string, string>>;\n    subpath?: string;\n  }>,\n): string {\n  let purl = `pkg:${parts.type}/`;\n  if (parts.namespace) {\n    purl += `${parts.namespace\n      .split(\"/\")\n      .map((s) => encodeURIComponent(s))\n      .join(\"/\")}/`;\n  }\n  purl += encodeURIComponent(parts.name);\n  if (parts.version) {\n    purl += `@${encodeURIComponent(parts.version)}`;\n  }\n  if (parts.qualifiers && Object.keys(parts.qualifiers).length > 0) {\n    const qualifiers = new Map<string, string>();\n    for (const [key, value] of Object.entries(parts.qualifiers)) {\n      const canonicalKey = key.toLowerCase();\n      if (!QUALIFIER_KEY_PATTERN.test(canonicalKey)) {\n        throw new InvalidPURLError(purl, `invalid qualifier key \"${key}\"`);\n      }\n      if (qualifiers.has(canonicalKey)) {\n        throw new InvalidPURLError(purl, `duplicate qualifier key \"${canonicalKey}\"`);\n      }\n      qualifiers.set(canonicalKey, value);\n    }\n    const qs = [...qualifiers]\n      .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))\n      .map(([key, value]) => `${key}=${encodeURIComponent(value)}`)\n      .join(\"&\");\n    purl += `?${qs}`;\n  }\n  if (parts.subpath) {\n    purl += `#${parts.subpath\n      .split(\"/\")\n      .map((s) => encodeURIComponent(s))\n      .join(\"/\")}`;\n  }\n  return purl;\n}\n"],"mappings":"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"}