package pack import ( "fmt" "strconv" "strings" ) func CheckHostCompatibility(manifest Manifest, hostVersion string) error { targetHost := strings.TrimSpace(manifest.TargetHost) if targetHost == "" { return fmt.Errorf("pack manifest target_host is required") } if targetHost != "sub2api" { return fmt.Errorf("pack target_host %q is not supported", targetHost) } normalizedHost, err := parseVersion(strings.TrimSpace(hostVersion)) if err != nil { return fmt.Errorf("parse host version %q: %w", hostVersion, err) } minVersion := strings.TrimSpace(manifest.MinHostVersion) if minVersion != "" { cmp, err := compareVersions(normalizedHost.raw, minVersion) if err != nil { return fmt.Errorf("compare min_host_version: %w", err) } if cmp < 0 { return fmt.Errorf("host version %q is below min_host_version %q", hostVersion, minVersion) } } maxVersion := strings.TrimSpace(manifest.MaxHostVersion) if maxVersion != "" { ok, err := matchesMaxConstraint(normalizedHost.raw, maxVersion) if err != nil { return fmt.Errorf("compare max_host_version: %w", err) } if !ok { return fmt.Errorf("host version %q is above max_host_version %q", hostVersion, maxVersion) } } return nil } type parsedVersion struct { raw string parts [3]int } func compareVersions(a, b string) (int, error) { left, err := parseVersion(a) if err != nil { return 0, err } right, err := parseVersion(b) if err != nil { return 0, err } for i := 0; i < len(left.parts); i++ { if left.parts[i] < right.parts[i] { return -1, nil } if left.parts[i] > right.parts[i] { return 1, nil } } return 0, nil } func matchesMaxConstraint(hostVersion, maxVersion string) (bool, error) { normalizedMax := normalizeVersion(maxVersion) if strings.HasSuffix(normalizedMax, ".x") { prefix := strings.TrimSuffix(normalizedMax, ".x") parts := strings.Split(prefix, ".") if len(parts) != 2 { return false, fmt.Errorf("wildcard max version %q must be in N.N.x format", maxVersion) } host, err := parseVersion(hostVersion) if err != nil { return false, err } major, err := strconv.Atoi(parts[0]) if err != nil { return false, fmt.Errorf("parse major version %q: %w", parts[0], err) } minor, err := strconv.Atoi(parts[1]) if err != nil { return false, fmt.Errorf("parse minor version %q: %w", parts[1], err) } if host.parts[0] < major { return true, nil } if host.parts[0] > major { return false, nil } return host.parts[1] <= minor, nil } cmp, err := compareVersions(hostVersion, maxVersion) if err != nil { return false, err } return cmp <= 0, nil } func parseVersion(value string) (parsedVersion, error) { normalized := normalizeVersion(value) if normalized == "" { return parsedVersion{}, fmt.Errorf("version is required") } parts := strings.Split(normalized, ".") if len(parts) != 3 { return parsedVersion{}, fmt.Errorf("version %q must be in N.N.N format", value) } var parsed parsedVersion parsed.raw = normalized for i, part := range parts { number, err := strconv.Atoi(part) if err != nil { return parsedVersion{}, fmt.Errorf("parse version segment %q: %w", part, err) } parsed.parts[i] = number } return parsed, nil } func normalizeVersion(value string) string { trimmed := strings.TrimSpace(value) trimmed = strings.TrimPrefix(trimmed, "v") trimmed = strings.TrimPrefix(trimmed, "V") return trimmed }