// +---------------------------------------------------------------------- // | 语义化版本约束判定(Composer 约束语法子集) // +---------------------------------------------------------------------- declare(strict_types=1); namespace ywxapp\utils; /** * 版本约束工具 * * 支持的约束写法: * 比较符 >=1.2.0 >1.0 <=2.0 ==1.0.0 =1.0.0 !=1.0.0 * 裸版本号 1.2.0(等价 ==1.2.0) * caret ^1.2.3 => >=1.2.3 <2.0.0 ;^0.2.3 => >=0.2.3 <0.3.0(不跨段原则) * tilde ~1.2.3 => >=1.2.3 <1.3.0 ;~1.2 => >=1.2.0 <2.0.0 * 通配符 1.2.* => >=1.2.0 <1.3.0 ;1.* => >=1.0.0 <2.0.0 * 与 AND '>=1.2 <2.0'(也可逗号分隔) * 或 OR '<1.0 || >=2.0'(写作 | 亦可) * * 设计取舍:解析不出合法约束时一律**放行**(返回 true)。 * 依赖声明写错不该让整站无法安装,真实缺失由下游的实际报错兜底——这与上游 * AddonService::versionSatisfies() 的既有行为保持一致。 */ class Version { /** * 判定版本是否满足约束 */ public static function satisfies(string $version, string $constraint): bool { $constraint = trim($constraint); // 版本号缺失(依赖插件没写 version)或约束为空:一律放行,交给下游的真实报错兜底 if (trim($version) === '' || $constraint === '') { return true; } $version = self::clean($version); // PHP 版本这类写法(8.1、8.1.0)与插件版本一致处理 foreach (self::splitOr($constraint) as $orGroup) { if ($orGroup === []) { continue; } $ok = true; foreach ($orGroup as $single) { if (!self::matchSingle($version, $single)) { $ok = false; break; } } if ($ok) { return true; } } return false; } /** * 拆 OR:`||` / `|` 分隔 * * @return array> 每组是 AND 条件数组 */ private static function splitOr(string $constraint): array { $groups = []; foreach (preg_split('/\s*\|\|?\s*/', $constraint) ?: [] as $group) { $group = trim($group); if ($group === '') { continue; } // AND:空白或逗号分隔 $ands = array_values(array_filter(array_map( 'trim', preg_split('/[\s,]+/', $group) ?: [] ))); $groups[] = $ands; } return $groups; } /** * 判定单个条件 */ private static function matchSingle(string $version, string $single): bool { if (!preg_match('/^(<=|>=|==|!=|<|>|=|\^|~)?\s*v?(\d+(?:\.\d+)*(?:\.\*)?)$/i', $single, $m)) { return true; // 非法约束:放行 } $op = strtolower($m[1] ?? ''); $target = self::clean($m[2]); $targetW = str_replace('.*', '', $m[2]); // 通配符段 switch ($op) { case '^': return version_compare($version, $target, '>=') && version_compare($version, self::upperCaret($targetW), '<'); case '~': // ~1.2(两位)=> <2.0.0;~1.2.3(三位)=> <1.3.0 $upper = substr_count($targetW, '.') >= 1 ? self::upperTilde($targetW) : ((string) (((int) $targetW) + 1) . '.0.0'); return version_compare($version, $target, '>=') && version_compare($version, $upper, '<'); case '': // 裸版本号:1.2.0 等价 ==;1.2.* 是通配 if (str_contains($m[2], '*')) { return self::matchWildcard($version, $targetW); } return version_compare($version, $target, '=='); case '=': return version_compare($version, $target, '=='); default: // 通配符与 ^ / ~ 组合意义不明,退化为纯比较 return version_compare($version, $target, $op); } } /** * 通配在新判定:1.2.* => [1.2.0, 1.3.0);1.* => [1.0.0, 2.0.0) */ private static function matchWildcard(string $version, string $base): bool { $parts = explode('.', $base); $lower = self::pad(implode('.', $parts)); $depth = count($parts); $parts[$depth - 1] = (string) (((int) end($parts)) + 1); $upper = self::pad(implode('.', array_slice($parts, 0, $depth))); return version_compare($version, $lower, '>=') && version_compare($version, $upper, '<'); } /** * caret 上界:不改变最左侧非零位 * 1.2.3 -> 2.0.0 ;0.2.3 -> 0.3.0 ;0.0.3 -> 0.0.4 */ private static function upperCaret(string $v): string { $parts = array_map('intval', explode('.', $v)); foreach ($parts as $i => $num) { if ($num !== 0 || $i === count($parts) - 1) { $parts[$i] = $num + 1; $parts = array_slice($parts, 0, $i + 1); return self::pad(implode('.', $parts)); } } return '9999999.0.0'; } /** * tilde 上界:递增最后给出的次末位(~1.2.3 -> 1.3.0) */ private static function upperTilde(string $v): string { $parts = explode('.', $v); $parts[count($parts) - 2] = (string) (((int) $parts[count($parts) - 2]) + 1); return self::pad(implode('.', array_slice($parts, 0, count($parts) - 1) + [])); } /** * 清理版本号:去 v 前缀,补齐到三段便于比较 */ private static function clean(string $v): string { $v = strtolower(trim(ltrim(trim($v), 'vV'))); $v = preg_replace('/[^0-9.]/', '', $v) ?? ''; return self::pad($v); } /** * 补齐 1.2 -> 1.2.0(version_compare 认为 1.2 == 1.2.0,但用于展示/拼接时统一) */ private static function pad(string $v): string { $parts = explode('.', $v === '' ? '0' : $v); while (count($parts) < 3) { $parts[] = '0'; } return implode('.', $parts); } }