文件预览

semver.mjs

查看 hermes-attestation-guardian 技能包中的文件内容。

文件内容

lib/semver.mjs

/**
 * @param {string} version
 * @returns {[number, number, number] | null}
 */
export function parseSemver(version) {
  const cleaned = String(version || "")
    .trim()
    .replace(/^v/i, "")
    .split("+")[0]
    .split("-")[0];

  const match = cleaned.match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?$/);
  if (!match) return null;

  const normalized = [
    Number.parseInt(match[1], 10),
    Number.parseInt(match[2] || "0", 10),
    Number.parseInt(match[3] || "0", 10),
  ];

  if (normalized.some((part) => Number.isNaN(part))) return null;
  return /** @type {[number, number, number]} */ (normalized);
}

/**
 * @param {string} left
 * @param {string} right
 * @returns {number | null}
 */
export function compareSemver(left, right) {
  const a = parseSemver(left);
  const b = parseSemver(right);
  if (!a || !b) return null;

  for (let i = 0; i < 3; i += 1) {
    if (a[i] > b[i]) return 1;
    if (a[i] < b[i]) return -1;
  }
  return 0;
}

/**
 * @param {string} value
 * @returns {string}
 */
export function escapeRegex(value) {
  return String(value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

/**
 * @param {string} rawSpecifier
 * @returns {{name: string, versionSpec: string} | null}
 */
export function parseAffectedSpecifier(rawSpecifier) {
  const specifier = String(rawSpecifier || "").trim();
  if (!specifier) return null;

  const atIndex = specifier.lastIndexOf("@");
  if (atIndex <= 0) {
    return null;
  }
  if (atIndex === specifier.length - 1) {
    return null;
  }

  const name = specifier.slice(0, atIndex).trim();
  const versionSpec = specifier.slice(atIndex + 1).trim();
  if (!name || !versionSpec) return null;
  return { name, versionSpec };
}

/**
 * @param {string} reason
 * @param {string} normalized
 * @returns {{supported: false, normalized: string, reason: string}}
 */
function unsupportedSpec(reason, normalized) {
  return { supported: false, normalized, reason };
}

/**
 * @param {string} normalized
 * @returns {{supported: true, normalized: string, reason: null}}
 */
function supportedSpec(normalized) {
  return { supported: true, normalized, reason: null };
}

/**
 * @param {string} rawSpec
 * @returns {{supported: boolean, normalized: string, reason: string | null}}
 */
export function parseVersionSpec(rawSpec) {
  const spec = String(rawSpec || "").trim();
  if (!spec || spec === "*" || spec.toLowerCase() === "any") {
    return supportedSpec("*");
  }

  if (spec.includes("||") || spec.includes("&&") || /\s-\s/.test(spec) || spec.includes(",")) {
    return unsupportedSpec("unsupported logical/composite semver range syntax", spec);
  }

  if (/^(>=|<=|>|<|=).*\s+(>=|<=|>|<|=)/.test(spec)) {
    return unsupportedSpec("unsupported comparator-set semver range syntax", spec);
  }

  if (spec.includes("*")) {
    if (!/^[vV]?[0-9*]+(?:\.[0-9*]+){0,2}$/.test(spec)) {
      return unsupportedSpec("unsupported wildcard semver range syntax", spec);
    }
    return supportedSpec(spec);
  }

  if (/^(>=|<=|>|<|=)\s*([vV]?\d+(?:\.\d+){0,2})$/.test(spec)) {
    return supportedSpec(spec);
  }

  if (spec.startsWith("^")) {
    if (!parseSemver(spec.slice(1))) {
      return unsupportedSpec("invalid caret semver range syntax", spec);
    }
    return supportedSpec(spec);
  }

  if (spec.startsWith("~")) {
    if (!parseSemver(spec.slice(1))) {
      return unsupportedSpec("invalid tilde semver range syntax", spec);
    }
    return supportedSpec(spec);
  }

  if (parseSemver(spec.replace(/^v/i, ""))) {
    return supportedSpec(spec);
  }

  return unsupportedSpec("unsupported semver range syntax", spec);
}

/**
 * @param {string | null} version
 * @param {string} rawSpec
 * @returns {boolean}
 */
export function versionMatches(version, rawSpec) {
  const parsedSpec = parseVersionSpec(rawSpec);
  if (!parsedSpec.supported) return false;

  const spec = parsedSpec.normalized;
  if (spec === "*") return true;
  if (!version || String(version).trim().toLowerCase() === "unknown") return false;

  const normalizedVersion = String(version).trim().replace(/^v/i, "");

  if (spec.includes("*")) {
    const wildcardRegex = new RegExp(`^${escapeRegex(spec).replace(/\\\*/g, ".*")}$`);
    return wildcardRegex.test(normalizedVersion);
  }

  const comparatorMatch = spec.match(/^(>=|<=|>|<|=)\s*([vV]?\d+(?:\.\d+){0,2})$/);
  if (comparatorMatch) {
    const operator = comparatorMatch[1];
    const targetVersion = comparatorMatch[2].trim();
    const compared = compareSemver(normalizedVersion, targetVersion);
    if (compared === null) return false;
    if (operator === ">=") return compared >= 0;
    if (operator === "<=") return compared <= 0;
    if (operator === ">") return compared > 0;
    if (operator === "<") return compared < 0;
    return compared === 0;
  }

  if (spec.startsWith("^")) {
    const target = parseSemver(spec.slice(1));
    const current = parseSemver(normalizedVersion);
    if (!target || !current) return false;

    const lowerBound = `${target[0]}.${target[1]}.${target[2]}`;
    let upperBound;
    if (target[0] > 0) {
      upperBound = `${target[0] + 1}.0.0`;
    } else if (target[1] > 0) {
      upperBound = `0.${target[1] + 1}.0`;
    } else {
      upperBound = `0.0.${target[2] + 1}`;
    }

    const lowerCompared = compareSemver(normalizedVersion, lowerBound);
    const upperCompared = compareSemver(normalizedVersion, upperBound);
    return lowerCompared !== null && upperCompared !== null && lowerCompared >= 0 && upperCompared === -1;
  }

  if (spec.startsWith("~")) {
    const target = parseSemver(spec.slice(1));
    const current = parseSemver(normalizedVersion);
    if (!target || !current) return false;
    return (
      current[0] === target[0] &&
      current[1] === target[1] &&
      compareSemver(normalizedVersion, spec.slice(1)) !== -1
    );
  }

  return normalizedVersion === spec || normalizedVersion === spec.replace(/^v/i, "");
}