github.com/google/osv-scalibr@v0.4.1/guidedremediation/internal/strategy/relax/relaxer/python.go (about) 1 // Copyright 2025 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package relaxer 16 17 import ( 18 "context" 19 "fmt" 20 "slices" 21 "strings" 22 23 "deps.dev/util/resolve" 24 "deps.dev/util/semver" 25 "github.com/google/osv-scalibr/guidedremediation/upgrade" 26 "github.com/google/osv-scalibr/log" 27 ) 28 29 // PythonRelaxer implements RequirementRelaxer for Python. 30 type PythonRelaxer struct{} 31 32 // Relax attempts to relax an requirement for Python packages. 33 // It returns the newly relaxed requirement and true if successful. 34 // If unsuccessful, it returns the original requirement and false. 35 func (r PythonRelaxer) Relax(ctx context.Context, cl resolve.Client, req resolve.RequirementVersion, config upgrade.Config) (resolve.RequirementVersion, bool) { 36 configLevel := config.Get(req.Name) 37 if configLevel == upgrade.None { 38 return req, false 39 } 40 41 c, err := semver.PyPI.ParseConstraint(req.Version) 42 if err != nil { 43 // The specified version is not a valid semver constraint 44 log.Warnf("Failed to parse Python requirement %s: %v", req.Version, err) 45 return req, false 46 } 47 48 // Get all the concrete versions of the package 49 allVKs, err := cl.Versions(ctx, req.PackageKey) 50 if err != nil { 51 return req, false 52 } 53 var vers []*semver.Version 54 for _, vk := range allVKs { 55 if vk.VersionType != resolve.Concrete { 56 continue 57 } 58 sv, err := semver.PyPI.Parse(vk.Version) 59 if err != nil { 60 log.Warnf("Failed to parse Python requirement %s: %v", vk.Version, err) 61 continue 62 } 63 vers = append(vers, sv) 64 } 65 slices.SortFunc(vers, func(a, b *semver.Version) int { 66 return a.Compare(b) 67 }) 68 69 // Find the versions on either side of the upper boundary of the requirement 70 var lastIdx int // highest version matching constraint 71 nextIdx := -1 // next version outside of range, preferring non-prerelease 72 nextIsPre := true // if the next version is a prerelease version 73 for lastIdx = len(vers) - 1; lastIdx >= 0; lastIdx-- { 74 v := vers[lastIdx] 75 if c.MatchVersion(v) { // found the upper bound, stop iterating 76 break 77 } 78 79 // Want to prefer non-prerelease versions, so only select one if we haven't seen any non-prerelease versions 80 if !v.IsPrerelease() || nextIsPre { 81 nextIdx = lastIdx 82 nextIsPre = v.IsPrerelease() 83 } 84 } 85 86 // Didn't find any higher versions of the package 87 if nextIdx == -1 { 88 return req, false 89 } 90 91 // No versions match the existing constraint, something is wrong 92 if lastIdx == -1 { 93 return req, false 94 } 95 96 // Our desired relaxation ordering is 97 // 1.2.3 -> 1.2.* -> 1.*.* -> 2.*.* -> 3.*.* -> ... 98 cmpVer := vers[lastIdx] 99 _, diff := cmpVer.Difference(vers[nextIdx]) 100 if !configLevel.Allows(diff) { 101 return req, false 102 } 103 if diff == semver.DiffMajor { 104 // Want to step only one major version at a time 105 // Instead of looking for a difference larger than major, 106 // we want to look for a major version bump from the first next version 107 cmpVer = vers[nextIdx] 108 diff = semver.DiffMinor 109 } 110 111 // Find the highest version with the same difference 112 best := vers[nextIdx] 113 for i := nextIdx + 1; i < len(vers); i++ { 114 _, d := cmpVer.Difference(vers[i]) 115 // If we've exceeded our allowed upgrade level, stop looking. 116 if !configLevel.Allows(d) { 117 break 118 } 119 120 // DiffMajor < DiffMinor < DiffPatch < DiffPrerelease 121 // So if d is less than the original diff, it represents a larger change 122 if d < diff { 123 break 124 } 125 if !vers[i].IsPrerelease() || nextIsPre { 126 best = vers[i] 127 } 128 } 129 130 // For a pinned version requirement, we pin it to the best version. 131 if strings.Contains(req.Version, "==") { 132 req.Version = "==" + best.String() 133 return req, true 134 } 135 136 // Find the next version that is not pre-release 137 var next *semver.Version 138 for i := nextIdx; i < len(vers); i++ { 139 _, d := cmpVer.Difference(vers[i]) 140 // If we've exceeded our allowed upgrade level, stop looking. 141 if !configLevel.Allows(d) { 142 break 143 } 144 if !vers[i].IsPrerelease() { 145 next = vers[i] 146 break 147 } 148 } 149 if next == nil { 150 // There is no newer version that is not pre-release, so use the latest pre-release. 151 next = vers[len(vers)-1] 152 } 153 154 if diff == semver.DiffPatch { 155 req.Version = "~=" + next.String() 156 } else { 157 // For a minor/major diff, we use the next version as the minimum requirement, 158 // and use the next major as the upper limit. 159 m, _ := next.Major() 160 req.Version = fmt.Sprintf(">=%s,<%d.0.0", next.String(), m+1) 161 } 162 return req, true 163 }