{"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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"}