Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
326 changes: 182 additions & 144 deletions compare.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@
package platforms

import (
"strconv"
"strings"
"slices"

specs "github.com/opencontainers/image-spec/specs-go/v1"
)
Expand All @@ -31,125 +30,199 @@ type MatchComparer interface {
Less(specs.Platform, specs.Platform) bool
}

type platformVersions struct {
major []int
minor []int
// Only returns a match comparer for a single platform using default
// resolution logic for the platform.
//
// Match and Less answer two different questions and are implemented
// independently: Match asks "can this run here at all", and holds all the
// compatibility logic — feature requirements, the arm64 cross-generation
// offset, architecture fallbacks, and (on Windows) the stable-ABI version
// window between host and container OS versions (see
// checkWindowsHostAndContainerCompat) — computed directly (see variant.go)
// rather than by generating and searching the set of every platform that
// could be compatible, which would be unbounded for some architectures'
// variant schemes. Less asks "how would these sort, biased towards this
// host's own OS and architecture" and is a plain, match-independent sort
// key.
//
// Callers that want "the best runnable match" should filter with Match
// before ranking with Less — the same way images.Manifests filters an
// index's manifests before sorting the survivors — rather than sorting
// first and scanning for the first match: these lists are small enough
// that filtering first is never more expensive, and it avoids sorting
// entries that are just going to be discarded.
//
// For arm64/v9.x, will also match arm64/v9.{0..x-1} and arm64/v8.{0..x+5}
// For arm64/v8.x, will also match arm64/v8.{0..x-1}
// For arm/v8, will also match arm/v7, arm/v6 and arm/v5
// For arm/v7, will also match arm/v6 and arm/v5
// For arm/v6, will also match arm/v5
// For amd64, will also match 386
// For any other architecture with a "<prefix><number><suffix>"-shaped
// variant (e.g. ppc64le's "powerN", riscv64's "rvaNNu64"), will also match
// lower numbered variants sharing the same prefix/suffix, as well as
// images that declare no variant at all.
// For windows, will also match any container OS version within the current
// Windows Server stable-ABI compatibility window, not just an exact
// OS version match.
func Only(platform specs.Platform) MatchComparer {
return &onlyComparer{
platform: Normalize(platform),
}
}

var arm64variantToVersion = map[string]platformVersions{
"v8": {[]int{8}, []int{0}},
"v8.0": {[]int{8}, []int{0}},
"v8.1": {[]int{8}, []int{1}},
"v8.2": {[]int{8}, []int{2}},
"v8.3": {[]int{8}, []int{3}},
"v8.4": {[]int{8}, []int{4}},
"v8.5": {[]int{8}, []int{5}},
"v8.6": {[]int{8}, []int{6}},
"v8.7": {[]int{8}, []int{7}},
"v8.8": {[]int{8}, []int{8}},
"v8.9": {[]int{8}, []int{9}},
"v9": {[]int{9, 8}, []int{0, 5}},
"v9.0": {[]int{9, 8}, []int{0, 5}},
"v9.1": {[]int{9, 8}, []int{1, 6}},
"v9.2": {[]int{9, 8}, []int{2, 7}},
"v9.3": {[]int{9, 8}, []int{3, 8}},
"v9.4": {[]int{9, 8}, []int{4, 9}},
"v9.5": {[]int{9, 8}, []int{5, 9}},
"v9.6": {[]int{9, 8}, []int{6, 9}},
"v9.7": {[]int{9, 8}, []int{7, 9}},
// onlyComparer implements the matching and ranking behavior of Only.
type onlyComparer struct {
platform specs.Platform // normalized reference platform
}

// platformVector returns an (ordered) vector of appropriate specs.Platform
// objects to try matching for the given platform object (see platforms.Only).
func platformVector(platform specs.Platform) []specs.Platform {
vector := []specs.Platform{platform}
// osIdentity reports whether p's OS name and OS version are ones
// c.platform could run. There's no "native vs. fallback OS" concept in Only
// (that would be for something like running Linux containers on Windows or
// FreeBSD), so an OS identity mismatch is an absolute disqualifier.
func (c *onlyComparer) osIdentity(p specs.Platform) bool {
normalized := Normalize(p)
if c.platform.OS != normalized.OS {
return false
}
return osVersionMatch(c.platform.OS, c.platform.OSVersion, p.OSVersion)
}

switch platform.Architecture {
case "amd64":
if amd64Version, err := strconv.Atoi(strings.TrimPrefix(platform.Variant, "v")); err == nil && amd64Version > 1 {
for amd64Version--; amd64Version >= 1; amd64Version-- {
vector = append(vector, specs.Platform{
Architecture: platform.Architecture,
OS: platform.OS,
OSVersion: platform.OSVersion,
OSFeatures: platform.OSFeatures,
Variant: "v" + strconv.Itoa(amd64Version),
})
}
}
vector = append(vector, specs.Platform{
Architecture: "386",
OS: platform.OS,
OSVersion: platform.OSVersion,
OSFeatures: platform.OSFeatures,
})
case "arm":
if armVersion, err := strconv.Atoi(strings.TrimPrefix(platform.Variant, "v")); err == nil && armVersion > 5 {
for armVersion--; armVersion >= 5; armVersion-- {
vector = append(vector, specs.Platform{
Architecture: platform.Architecture,
OS: platform.OS,
OSVersion: platform.OSVersion,
OSFeatures: platform.OSFeatures,
Variant: "v" + strconv.Itoa(armVersion),
})
}
// featuresOK reports whether p's OS features (ignoring win32k on Windows,
// which is missing on Nano Server) are a subset of c.platform's.
func (c *onlyComparer) featuresOK(p specs.Platform) bool {
features := c.stripIgnoredFeatures(Normalize(p).OSFeatures)
return osFeaturesSubset(features, c.platform.OSFeatures)
}

// featureOverlap returns how many of p's OS features are also declared by
// c.platform. Used only by Less, to rank two candidates that are otherwise
// tied: a candidate's extra features should only count in its favor to the
// extent the host actually declares them.
func (c *onlyComparer) featureOverlap(p specs.Platform) int {
features := c.stripIgnoredFeatures(Normalize(p).OSFeatures)
have := c.platform.OSFeatures
n, j := 0, 0
for _, f := range features {
for j < len(have) && have[j] < f {
j++
}
case "arm64":
variant := platform.Variant
if variant == "" {
variant = "v8"
if j < len(have) && have[j] == f {
n++
j++
}
}
return n
}

vector = []specs.Platform{} // Reset vector, the first variant will be added in loop.
arm64Versions, ok := arm64variantToVersion[variant]
if !ok {
break
}
for i, major := range arm64Versions.major {
for minor := arm64Versions.minor[i]; minor >= 0; minor-- {
arm64Variant := "v" + strconv.Itoa(major) + "." + strconv.Itoa(minor)
if minor == 0 {
arm64Variant = "v" + strconv.Itoa(major)
}
vector = append(vector, specs.Platform{
Architecture: "arm64",
OS: platform.OS,
OSVersion: platform.OSVersion,
OSFeatures: platform.OSFeatures,
Variant: arm64Variant,
})
}
func (c *onlyComparer) stripIgnoredFeatures(features []string) []string {
if c.platform.OS == "windows" {
// win32k is missing on Nano Server; ignore it for matching purposes.
if i := slices.Index(features, "win32k"); i >= 0 {
return slices.Delete(slices.Clone(features), i, i+1)
}
}
return features
}

// All arm64/v8.x and arm64/v9.x are compatible with arm/v8 (32-bits) and below.
// There's no arm64 v9 variant, so it's normalized to v8.
if strings.HasPrefix(variant, "v8") || strings.HasPrefix(variant, "v9") {
variant = "v8"
// fallbackArch returns the recognized cross-architecture fallback for
// hostArch (386 for amd64, arm for arm64), or "" if hostArch has none. Both
// archMatch and archRank need this same pairing, one for compatibility and
// one for ranking, so it's kept in one place.
func fallbackArch(hostArch string) string {
switch hostArch {
case "amd64":
return "386"
case "arm64":
return "arm"
}
return ""
}

// archMatch reports whether p's architecture and variant are compatible
// with c.platform's: either the same architecture with a compatible
// variant, or a recognized cross-architecture fallback (see fallbackArch)
// with a compatible variant.
func (c *onlyComparer) archMatch(p specs.Platform) bool {
normalized := Normalize(p)
if normalized.Architecture == c.platform.Architecture {
switch c.platform.Architecture {
case "amd64":
return numberedVariantMatch(c.platform.Variant, normalized.Variant, "v1")
case "arm":
return numberedVariantMatch(c.platform.Variant, normalized.Variant, "v5")
case "arm64":
return arm64VariantMatch(c.platform.Variant, normalized.Variant)
default:
return genericVariantMatch(c.platform.Variant, normalized.Variant)
}
vector = append(vector, platformVector(specs.Platform{
Architecture: "arm",
OS: platform.OS,
OSVersion: platform.OSVersion,
OSFeatures: platform.OSFeatures,
Variant: variant,
})...)
}
if normalized.Architecture != fallbackArch(c.platform.Architecture) {
return false
}

switch c.platform.Architecture {
case "amd64":
return true // 386 has no variant to check; it's a match-or-nothing fallback.
default: // arm64
return numberedVariantMatch("v8", normalized.Variant, "v5")
}
}

return vector
func (c *onlyComparer) Match(p specs.Platform) bool {
return c.osIdentity(p) && c.featuresOK(p) && c.archMatch(p)
}

// Only returns a match comparer for a single platform
// using default resolution logic for the platform.
//
// For arm64/v9.x, will also match arm64/v9.{0..x-1} and arm64/v8.{0..x+5}
// For arm64/v8.x, will also match arm64/v8.{0..x-1}
// For arm/v8, will also match arm/v7, arm/v6 and arm/v5
// For arm/v7, will also match arm/v6 and arm/v5
// For arm/v6, will also match arm/v5
// For amd64, will also match 386
func Only(platform specs.Platform) MatchComparer {
return Ordered(platformVector(Normalize(platform))...)
// archRank returns how preferable arch is, for a host declaring
// hostArch: 2 for the host's own architecture, 1 for its recognized
// cross-architecture fallback (see fallbackArch), or 0 otherwise (ties are
// broken alphabetically by Less). Unlike archMatch, this doesn't check
// variant compatibility at all — it's a plain preference between
// architecture names, used only for ranking.
func archRank(hostArch, arch string) int {
switch {
case arch == hostArch:
return 2
case arch != "" && arch == fallbackArch(hostArch):
return 1
default:
return 0
}
}

func (c *onlyComparer) Less(p1, p2 specs.Platform) bool {
n1, n2 := Normalize(p1), Normalize(p2)

// Prefer the host's own OS over any other; otherwise alphabetically.
native1, native2 := n1.OS == c.platform.OS, n2.OS == c.platform.OS
if native1 != native2 {
return native1
}
if n1.OS != n2.OS {
return naturalLess(n1.OS, n2.OS)
}

// Prefer the host's own architecture, then its recognized fallback
// architecture, then alphabetically.
a1, a2 := archRank(c.platform.Architecture, n1.Architecture), archRank(c.platform.Architecture, n2.Architecture)
if a1 != a2 {
return a1 > a2
}
if n1.Architecture != n2.Architecture {
return naturalLess(n1.Architecture, n2.Architecture)
}

// Then variant and OS version, newest/highest first.
if n1.Variant != n2.Variant {
return naturalLess(n2.Variant, n1.Variant)
}
if p1.OSVersion != p2.OSVersion {
return naturalLess(p2.OSVersion, p1.OSVersion)
}

// Tied on everything above: prefer the one whose OS features overlap
// more with what the host actually declares.
return c.featureOverlap(p1) > c.featureOverlap(p2)
}

// OnlyOS returns a match comparer that matches only platforms with the same
Expand All @@ -160,25 +233,14 @@ func OnlyOS(platform specs.Platform) MatchComparer {
normalized := Normalize(platform)
return onlyOSComparer{
platform: normalized,
osvM: newOSVersionMatcher(normalized),
archOrder: orderedPlatformComparer{
matchers: []Matcher{NewMatcher(normalized)},
},
}
}

func newOSVersionMatcher(platform specs.Platform) osVerMatcher {
if platform.OS == "windows" {
return &windowsVersionMatcher{
windowsOSVersion: getWindowsOSVersion(platform.OSVersion),
}
}
return nil
}

type onlyOSComparer struct {
platform specs.Platform
osvM osVerMatcher
archOrder orderedPlatformComparer
}

Expand All @@ -187,34 +249,10 @@ func (c onlyOSComparer) matchOS(platform specs.Platform) bool {
if c.platform.OS != normalized.OS {
return false
}
if c.osvM != nil {
if !c.osvM.Match(platform.OSVersion) {
return false
}
}
if len(normalized.OSFeatures) > 0 {
if len(c.platform.OSFeatures) < len(normalized.OSFeatures) {
return false
}
j := 0
for _, feature := range normalized.OSFeatures {
found := false
for ; j < len(c.platform.OSFeatures); j++ {
if feature == c.platform.OSFeatures[j] {
found = true
j++
break
}
if feature < c.platform.OSFeatures[j] {
return false
}
}
if !found {
return false
}
}
if !osVersionMatch(c.platform.OS, c.platform.OSVersion, platform.OSVersion) {
return false
}
return true
return osFeaturesSubset(normalized.OSFeatures, c.platform.OSFeatures)
}

func (c onlyOSComparer) Match(platform specs.Platform) bool {
Expand Down
Loading
Loading