Skip to content
Draft
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
47 changes: 17 additions & 30 deletions pkg/apk/apk/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@ import (
)

var (
parsedVersions sync.Map // map[string]Version
parsedConstraints sync.Map // map[string]ParsedConstraint
parsedVersions sync.Map // map[string]Version
)

// NamedIndex an index that contains all of its packages,
Expand Down Expand Up @@ -272,7 +271,7 @@ func newPkgResolver(ctx context.Context, indexes []NamedIndex) *PkgResolver {
for _, pkgVersions := range allPkgs {
for _, pkg := range pkgVersions {
for _, provide := range pkg.Provides {
name := cachedResolvePackageNameVersionPin(provide).Name
name := constraintName(provide)
pkgNameMap[name] = append(pkgNameMap[name], pkg)
}
}
Expand Down Expand Up @@ -314,7 +313,7 @@ func (p *PkgResolver) nextPackage(packages []string, dq map[*RepositoryPackage]s

// Disqualify anything that provides "constraint". This is used for !foo style constraints.
func (p *PkgResolver) disqualifyProviders(constraint string, dq map[*RepositoryPackage]string) {
parsed := cachedResolvePackageNameVersionPin(constraint)
parsed := ResolvePackageNameVersionPin(constraint)
providers, ok := p.nameMap[parsed.Name]
if !ok {
return
Expand Down Expand Up @@ -346,7 +345,7 @@ func (p *PkgResolver) conflictingVersion(constraint ParsedConstraint, conflict *
}

for _, confProv := range conflict.Provides {
confConstraint := cachedResolvePackageNameVersionPin(confProv)
confConstraint := ResolvePackageNameVersionPin(confProv)
if confConstraint.Name != constraint.Name {
// Not the constraint we're looking for.
continue
Expand All @@ -366,7 +365,7 @@ func (p *PkgResolver) conflictingVersion(constraint ParsedConstraint, conflict *
// Disqualify anything that conflicts with the given pkg.
func (p *PkgResolver) disqualifyConflicts(pkg *RepositoryPackage, dq map[*RepositoryPackage]string) {
for _, prov := range pkg.Provides {
constraint := cachedResolvePackageNameVersionPin(prov)
constraint := ResolvePackageNameVersionPin(prov)
providers, ok := p.nameMap[constraint.Name]
if !ok {
continue
Expand Down Expand Up @@ -405,7 +404,7 @@ func (p *PkgResolver) pick(pkg *RepositoryPackage) error {
p.selected[pkg.Name] = pkg

for _, prov := range pkg.Provides {
constraint := cachedResolvePackageNameVersionPin(prov)
constraint := ResolvePackageNameVersionPin(prov)
if conflict, ok := p.selected[constraint.Name]; ok {
return fmt.Errorf("selecting package %s conflicts with %s on %q", pkg.Filename(), conflict.Filename(), constraint.Name)
}
Expand Down Expand Up @@ -436,7 +435,7 @@ func (p *PkgResolver) constrain(constraints []string, dq map[*RepositoryPackage]
continue
}

parsed := cachedResolvePackageNameVersionPin(constraint)
parsed := ResolvePackageNameVersionPin(constraint)
if parsed.dep == versionAny {
continue
}
Expand Down Expand Up @@ -466,7 +465,7 @@ func (p *PkgResolver) constrain(constraints []string, dq map[*RepositoryPackage]
}
} else {
for _, provides := range provider.Provides {
pp := cachedResolvePackageNameVersionPin(provides)
pp := ResolvePackageNameVersionPin(provides)
if pp.Name != parsed.Name {
continue
}
Expand Down Expand Up @@ -587,7 +586,7 @@ func (p *PkgResolver) GetPackageWithDependencies(ctx context.Context, pkgName st
return nil, nil, nil, &ConstraintError{pkgName, err}
}

pin := cachedResolvePackageNameVersionPin(pkgName).pin
pin := ResolvePackageNameVersionPin(pkgName).pin
deps, conflicts, err := p.getPackageDependencies(ctx, pkg, pin, parents, localExisting, existingOrigins, dq)
if err != nil {
return nil, nil, nil, &DepError{pkg, err}
Expand Down Expand Up @@ -616,7 +615,7 @@ func (p *PkgResolver) GetPackageWithDependencies(ctx context.Context, pkgName st
var matchCount int
for _, subDep := range installIfPkg.InstallIf {
// two possibilities: package name, or name=version
constraint := cachedResolvePackageNameVersionPin(subDep)
constraint := ResolvePackageNameVersionPin(subDep)
name, version := constraint.Name, constraint.Version
// precise match of whatever it is, take it and continue
if _, ok := added[subDep]; ok {
Expand Down Expand Up @@ -646,7 +645,7 @@ func (p *PkgResolver) GetPackageWithDependencies(ctx context.Context, pkgName st
// and decreasing from there. In general, the first one in the list is the best match. This function
// returns multiple in case you need to see all potential matches.
func (p *PkgResolver) ResolvePackage(pkgName string, dq map[*RepositoryPackage]string) ([]*RepositoryPackage, error) {
constraint := cachedResolvePackageNameVersionPin(pkgName)
constraint := ResolvePackageNameVersionPin(pkgName)
name, version, compare, pin := constraint.Name, constraint.Version, constraint.dep, constraint.pin
pkgsWithVersions, ok := p.nameMap[name]
if !ok {
Expand All @@ -672,7 +671,7 @@ func (p *PkgResolver) ResolvePackage(pkgName string, dq map[*RepositoryPackage]s

// This is like ResolvePackage but we only care about the best match and not all matches.
func (p *PkgResolver) resolvePackage(pkgName string, dq map[*RepositoryPackage]string) (*RepositoryPackage, error) {
constraint := cachedResolvePackageNameVersionPin(pkgName)
constraint := ResolvePackageNameVersionPin(pkgName)
name, version, compare, pin := constraint.Name, constraint.Version, constraint.dep, constraint.pin

pkgsWithVersions, ok := p.nameMap[name]
Expand Down Expand Up @@ -728,7 +727,7 @@ func (p *PkgResolver) getPackageDependencies(ctx context.Context, pkg *Repositor
myProvides := make(map[string]bool, 2*len(pkg.Provides))
// see if we provide this
for _, provide := range pkg.Provides {
name := cachedResolvePackageNameVersionPin(provide).Name
name := constraintName(provide)
myProvides[provide] = true
myProvides[name] = true
}
Expand All @@ -755,7 +754,7 @@ func (p *PkgResolver) getPackageDependencies(ctx context.Context, pkg *Repositor
}

// this package might be pinned to a version
constraint := cachedResolvePackageNameVersionPin(dep)
constraint := ResolvePackageNameVersionPin(dep)
name, version, compare := constraint.Name, constraint.Version, constraint.dep
// see if we provide this
if myProvides[name] || myProvides[dep] {
Expand Down Expand Up @@ -800,7 +799,7 @@ func (p *PkgResolver) getPackageDependencies(ctx context.Context, pkg *Repositor
// selected satisfy this constraint.
satisfiedByProvide := false
for _, provide := range picked.Provides {
prostraint := cachedResolvePackageNameVersionPin(provide)
prostraint := ResolvePackageNameVersionPin(provide)
pname, pversion, pcompare := prostraint.Name, prostraint.Version, prostraint.dep
if pname != name {
continue
Expand Down Expand Up @@ -869,7 +868,7 @@ func (p *PkgResolver) getPackageDependencies(ctx context.Context, pkg *Repositor
}

pkgs := options[lowest]
name := cachedResolvePackageNameVersionPin(lowest).Name
name := ResolvePackageNameVersionPin(lowest).Name

// Remove this from our constraints.
constraints = slices.DeleteFunc(constraints, func(s string) bool {
Expand Down Expand Up @@ -930,18 +929,6 @@ func cachedParseVersion(version string) (Version, error) {
return parsed, nil
}

func cachedResolvePackageNameVersionPin(pkgName string) ParsedConstraint {
cached, ok := parsedConstraints.Load(pkgName)
if ok {
return cached.(ParsedConstraint)
}

pin := ResolvePackageNameVersionPin(pkgName)

parsedConstraints.Store(pkgName, pin)
return pin
}

// sortPackages sorts a slice of packages in descending order of preference, based on
// matching origin to a provided comparison package, whether or not one of the packages
// already is installed, the versions, and whether an origin already exists.
Expand Down Expand Up @@ -1118,7 +1105,7 @@ func (p *PkgResolver) getDepVersionForName(pkg *repositoryPackage, name string)
return pkg.Version
}
for _, prov := range pkg.Provides {
constraint := cachedResolvePackageNameVersionPin(prov)
constraint := ResolvePackageNameVersionPin(prov)
if constraint.Name == name {
return constraint.Version
}
Expand Down
144 changes: 144 additions & 0 deletions pkg/apk/apk/resolve_constraint_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// Copyright 2026 Chainguard, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package apk

import (
"regexp"
"strings"
"testing"
)

// regexResolvePackageNameVersionPin is the previous, regex-based
// implementation of ResolvePackageNameVersionPin, kept as the oracle the
// hand-written parser is checked against.
func regexResolvePackageNameVersionPin(pkgName string) ParsedConstraint {
endsWithReleaseStr := regexp.MustCompile(`-r\d+$`)
packageNameRegex := regexp.MustCompile(`^([^@=><~]+)(([=><~]+)([^@]+))?(@([a-zA-Z0-9]+))?$`)
packageNameRegex.Longest()

if strings.HasPrefix(pkgName, "so:") {
onlyPkgName, pkgVersion, found := strings.Cut(pkgName, "=")
if found && !endsWithReleaseStr.MatchString(pkgVersion) {
pkgName = onlyPkgName + "=0." + pkgVersion
}
}

parts := packageNameRegex.FindAllStringSubmatch(pkgName, -1)
if len(parts) == 0 || len(parts[0]) < 2 {
return ParsedConstraint{Name: pkgName, dep: versionAny}
}
p := ParsedConstraint{
Name: parts[0][1],
Version: parts[0][4],
pin: parts[0][6],
dep: versionAny,
}
switch parts[0][3] {
case "=":
p.dep = versionEqual
case ">":
p.dep = versionGreater
case "<":
p.dep = versionLess
case ">=":
p.dep = versionGreaterEqual
case "<=":
p.dep = versionLessEqual
case "~", "=~":
p.dep = versionTilde
}
return p
}

var resolveCases = []string{
"",
"foo",
"foo=1.2.3-r0",
"foo>=1.2",
"foo<=1.2",
"foo>1",
"foo<1",
"foo~1.2",
"foo=~1.2",
"foo=1.2.3-r0@wolfi",
"foo@wolfi",
"foo@",
"foo=",
"foo==",
"foo===",
"foo=@wolfi",
"foo=1@",
"foo=1@pin-with-dash",
"foo=1@p@q",
"foo@p@q",
"=1.2",
"@pin",
"so:libfoo.so.1=1",
"so:libfoo.so.1=1.2.3-r0",
"so:libfoo.so.1=1-r",
"so:libfoo.so.1=1-r5x",
"so:libfoo.so.1=1@wolfi",
"so:libfoo.so.1",
"so:=1",
"cmd:tool=1.0-r0",
"pc:libfoo=1.0",
"cmd:weird@name=1",
"a=>1",
"a=<1",
"a==1",
"a~=1",
"a=1=2",
"a=1>2@pin",
"héllo=1",
"a=1@pïn",
"a>=",
"a>=@x",
}

// lockPackageNameRegex is the copy pkg/build/lock.go used to carry, without
// the leftmost-longest setting and without the shared library tweak.
var lockPackageNameRegex = regexp.MustCompile(`^([^@=><~]+)(([=><~]+)([^@]+))?(@([a-zA-Z0-9]+))?$`)

func checkAgainstRegex(t *testing.T, in string) {
t.Helper()
want := regexResolvePackageNameVersionPin(in)
if got := ResolvePackageNameVersionPin(in); got != want {
t.Errorf("ResolvePackageNameVersionPin(%q) = %+v, regex says %+v", in, got, want)
}
if got := constraintName(in); got != want.Name {
t.Errorf("constraintName(%q) = %q, regex says %q", in, got, want.Name)
}

parts := lockPackageNameRegex.FindStringSubmatch(in)
got, ok := ParseConstraint(in)
if ok != (parts != nil) {
t.Errorf("ParseConstraint(%q) ok = %v, regex matched = %v", in, ok, parts != nil)
} else if ok && got.Name != parts[1] {
t.Errorf("ParseConstraint(%q).Name = %q, regex says %q", in, got.Name, parts[1])
}
}

func TestResolvePackageNameVersionPinMatchesRegex(t *testing.T) {
for _, in := range resolveCases {
checkAgainstRegex(t, in)
}
}

func FuzzResolvePackageNameVersionPinMatchesRegex(f *testing.F) {
for _, in := range resolveCases {
f.Add(in)
}
f.Fuzz(checkAgainstRegex)
}
Loading