Skip to content

Commit f5a2b66

Browse files
committed
add resolve by version check for debian
1 parent 012a518 commit f5a2b66

2 files changed

Lines changed: 195 additions & 11 deletions

File tree

internal/debutils/resolver.go

Lines changed: 194 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,17 @@ import (
1919
// returns the minimal closure of PackageInfos needed to satisfy all Requires.
2020
func ResolvePackageInfos(requested []provider.PackageInfo, all []provider.PackageInfo) ([]provider.PackageInfo, error) {
2121

22-
// Build a map for fast lookup by package name
22+
// Build a map for fast lookup by package name and version
23+
byNameVer := make(map[string]provider.PackageInfo, len(all))
2324
byName := make(map[string]provider.PackageInfo, len(all))
24-
// Build a map for lookup by provided virtual package name
2525
byProvides := make(map[string]provider.PackageInfo)
2626
for _, pi := range all {
27+
key := pi.Name
28+
if pi.Version != "" {
29+
// contruct key as "name=version" for exact matches
30+
key = fmt.Sprintf("%s=%s", pi.Name, pi.Version)
31+
byNameVer[key] = pi
32+
}
2733
byName[pi.Name] = pi
2834
for _, prov := range pi.Provides {
2935
byProvides[prov] = pi
@@ -35,15 +41,23 @@ func ResolvePackageInfos(requested []provider.PackageInfo, all []provider.Packag
3541
// Start with the requested packages
3642
queue := make([]provider.PackageInfo, 0, len(requested))
3743
for _, pi := range requested {
38-
if _, ok := byName[pi.Name]; !ok {
39-
// Try to resolve via Provides
40-
if provPkg, ok := byProvides[pi.Name]; ok {
41-
queue = append(queue, provPkg)
44+
key := pi.Name
45+
if pi.Version != "" {
46+
key = fmt.Sprintf("%s=%s", pi.Name, pi.Version)
47+
if pkg, ok := byNameVer[key]; ok {
48+
queue = append(queue, pkg)
4249
continue
4350
}
44-
return nil, fmt.Errorf("requested package %q not in repo listing", pi.Name)
4551
}
46-
queue = append(queue, pi)
52+
if pkg, ok := byName[pi.Name]; ok {
53+
queue = append(queue, pkg)
54+
continue
55+
}
56+
if provPkg, ok := byProvides[pi.Name]; ok {
57+
queue = append(queue, provPkg)
58+
continue
59+
}
60+
return nil, fmt.Errorf("requested package %q not in repo listing", pi.Name)
4761
}
4862

4963
result := make([]provider.PackageInfo, 0)
@@ -60,22 +74,83 @@ func ResolvePackageInfos(requested []provider.PackageInfo, all []provider.Packag
6074

6175
// Traverse dependencies (Requires)
6276
for _, dep := range cur.Requires {
63-
// Remove version constraints if present, e.g. "foo (>= 1.2)" -> "foo"
77+
// dep may be "foo (>= 1.2)" or "foo (= 1.2)" or just "foo"
6478
depName := dep
65-
if idx := strings.Index(dep, " "); idx > 0 {
66-
depName = dep[:idx]
79+
depVersion := ""
80+
// handles "|" in package names, e.g. "perl | perl-base"
81+
// currently only the first part is used, TODO: handle multiple parts
82+
if idx := strings.Index(depName, "|"); idx > 0 {
83+
depName = strings.TrimSpace(depName[:idx])
6784
}
6885
// Remove architecture qualifiers, e.g. "perl:any" -> "perl"
6986
if idx := strings.Index(depName, ":"); idx > 0 {
7087
depName = depName[:idx]
7188
}
89+
// Check for version constraint
90+
if idx := strings.Index(depName, "("); idx > 0 {
91+
name := strings.TrimSpace(depName[:idx])
92+
verPart := strings.TrimSpace(depName[idx:])
93+
verPart = strings.Trim(verPart, "() ")
94+
// Only enforce exact version if constraint is "="
95+
if strings.HasPrefix(verPart, "=") {
96+
depVersion = strings.TrimSpace(strings.TrimPrefix(verPart, "="))
97+
depName = name
98+
} else {
99+
depName = name
100+
}
101+
} else if idx := strings.Index(depName, " "); idx > 0 {
102+
depName = depName[:idx]
103+
}
72104
depName = strings.TrimSpace(depName)
73105
if depName == "" {
74106
continue
75107
}
76108
if _, seen := neededSet[depName]; seen {
77109
continue
78110
}
111+
// If version is enforced, match by name+version
112+
if depVersion != "" {
113+
// Try exact version first
114+
key := fmt.Sprintf("%s=%s", depName, depVersion)
115+
if depPkg, ok := byNameVer[key]; ok {
116+
// exact version match found
117+
queue = append(queue, depPkg)
118+
continue
119+
}
120+
// Try to find any package with higher version using Debian version semantics
121+
var found *provider.PackageInfo
122+
for _, pi := range all {
123+
if pi.Name == depName {
124+
// Use Debian version comparison
125+
cmp, err := compareDebianVersions(pi.Version, depVersion)
126+
if err != nil {
127+
return nil, fmt.Errorf("failed to compare versions: %v", err)
128+
}
129+
if cmp >= 0 {
130+
// sorting by version, pick the lowest one that satisfies the constraint
131+
if found == nil {
132+
tmp := pi
133+
found = &tmp
134+
} else {
135+
// Pick the lowest version that satisfies the constraint
136+
cmp2, err := compareDebianVersions(pi.Version, found.Version)
137+
if err != nil {
138+
return nil, fmt.Errorf("failed to compare versions: %v", err)
139+
}
140+
if cmp2 < 0 {
141+
tmp := pi
142+
found = &tmp
143+
}
144+
}
145+
}
146+
}
147+
}
148+
if found != nil {
149+
queue = append(queue, *found)
150+
continue
151+
}
152+
return nil, fmt.Errorf("dependency %q (version %q or higher) required by %q not found in repo", depName, depVersion, cur.Name)
153+
}
79154
if depPkg, ok := byName[depName]; ok {
80155
queue = append(queue, depPkg)
81156
} else if provPkg, ok := byProvides[depName]; ok {
@@ -203,6 +278,8 @@ func ParsePrimary(baseURL string, pkggz string, releaseFile string, releaseSign
203278
switch key {
204279
case "Package":
205280
pkg.Name = val
281+
case "Version":
282+
pkg.Version = val
206283
case "Depends":
207284
// Split dependencies by comma and trim spaces
208285
deps := strings.Split(val, ",")
@@ -251,3 +328,109 @@ func getFullUrl(filePath string, baseUrl string) (string, error) {
251328
fullURL := fmt.Sprintf("%s/%s", strings.TrimSuffix(baseUrl, "/"), filePath)
252329
return fullURL, nil
253330
}
331+
332+
// compareDebianVersions compares two Debian version strings.
333+
// Returns -1 if a < b, 0 if a == b, 1 if a > b.
334+
func compareDebianVersions(a, b string) (int, error) {
335+
// Helper to split epoch
336+
splitEpoch := func(ver string) (epoch int, rest string) {
337+
parts := strings.SplitN(ver, ":", 2)
338+
if len(parts) == 2 {
339+
fmt.Sscanf(parts[0], "%d", &epoch)
340+
rest = parts[1]
341+
} else {
342+
epoch = 0
343+
rest = ver
344+
}
345+
return
346+
}
347+
348+
// Helper to get next segment (numeric or non-numeric)
349+
nextSegment := func(s string) (seg string, rest string, numeric bool) {
350+
if s == "" {
351+
return "", "", false
352+
}
353+
if s[0] >= '0' && s[0] <= '9' {
354+
i := 0
355+
for i < len(s) && s[i] >= '0' && s[i] <= '9' {
356+
i++
357+
}
358+
return s[:i], s[i:], true
359+
}
360+
i := 0
361+
for i < len(s) && (s[i] < '0' || s[i] > '9') {
362+
i++
363+
}
364+
return s[:i], s[i:], false
365+
}
366+
367+
// Handle epoch
368+
epochA, restA := splitEpoch(a)
369+
epochB, restB := splitEpoch(b)
370+
if epochA < epochB {
371+
return -1, nil
372+
}
373+
if epochA > epochB {
374+
return 1, nil
375+
}
376+
377+
// Compare the rest
378+
sa, sb := restA, restB
379+
for sa != "" || sb != "" {
380+
// Handle tilde (~)
381+
if len(sa) > 0 && sa[0] == '~' {
382+
if len(sb) == 0 || sb[0] != '~' {
383+
return -1, nil
384+
}
385+
sa = sa[1:]
386+
sb = sb[1:]
387+
continue
388+
}
389+
if len(sb) > 0 && sb[0] == '~' {
390+
return 1, nil
391+
}
392+
393+
segA, restA, numA := nextSegment(sa)
394+
segB, restB, numB := nextSegment(sb)
395+
396+
if segA == "" && segB == "" {
397+
sa, sb = restA, restB
398+
continue
399+
}
400+
401+
if numA && numB {
402+
// Remove leading zeros
403+
segA = strings.TrimLeft(segA, "0")
404+
segB = strings.TrimLeft(segB, "0")
405+
// Compare by length
406+
if len(segA) > len(segB) {
407+
return 1, nil
408+
}
409+
if len(segA) < len(segB) {
410+
return -1, nil
411+
}
412+
// Compare lexicographically
413+
if segA > segB {
414+
return 1, nil
415+
}
416+
if segA < segB {
417+
return -1, nil
418+
}
419+
} else if !numA && !numB {
420+
if segA > segB {
421+
return 1, nil
422+
}
423+
if segA < segB {
424+
return -1, nil
425+
}
426+
} else {
427+
// Numeric segments are always less than non-numeric
428+
if numA {
429+
return -1, nil
430+
}
431+
return 1, nil
432+
}
433+
sa, sb = restA, restB
434+
}
435+
return 0, nil
436+
}

internal/provider/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
// PackageInfo holds everything you need to fetch + verify one artifact.
88
type PackageInfo struct {
99
Name string // e.g. "abseil-cpp"
10+
Version string // e.g. "7.88.1-10+deb12u5"
1011
URL string // download URL
1112
Checksum string // optional pre-known digest
1213
Provides []string // capabilities this package provides (rpm:entry names)

0 commit comments

Comments
 (0)