diff --git a/go.mod b/go.mod index a7bcff4..3ee0b89 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ module github.com/icholy/gomajor -go 1.15 +go 1.18 require ( - golang.org/x/mod v0.14.0 - golang.org/x/sync v0.6.0 + golang.org/x/mod v0.18.0 + golang.org/x/sync v0.7.0 ) diff --git a/go.sum b/go.sum index 1539329..2dc201f 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= +golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -19,6 +21,8 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internal/modproxy/modproxy.go b/internal/modproxy/modproxy.go index 591e78e..3d3a79e 100644 --- a/internal/modproxy/modproxy.go +++ b/internal/modproxy/modproxy.go @@ -10,9 +10,11 @@ import ( "net/http" "os" "path" + "slices" "strconv" "strings" + "golang.org/x/mod/modfile" "golang.org/x/mod/module" "golang.org/x/mod/semver" "golang.org/x/sync/errgroup" @@ -26,6 +28,19 @@ type Module struct { Versions []string } +// MaxVersionModule returns the latest version of the module in the list. +// If pre is false, pre-release versions will are excluded. +// Retracted versions are excluded. +func MaxVersionModule(mods []*Module, pre bool, r Retractions) (*Module, string) { + for i := len(mods); i > 0; i-- { + mod := mods[i-1].Retract(r) + if max := mod.MaxVersion("", pre); max != "" { + return mod, max + } + } + return nil, "" +} + // MaxVersion returns the latest version. // If there are no versions, the empty string is returned. // Prefix can be used to filter the versions based on a prefix. @@ -44,6 +59,15 @@ func (m *Module) MaxVersion(prefix string, pre bool) string { return max } +// Retract returns a copy of m with the retracted versions removed. +func (m *Module) Retract(r Retractions) *Module { + versions := slices.Clone(m.Versions) + return &Module{ + Path: m.Path, + Versions: slices.DeleteFunc(versions, r.Includes), + } +} + // IsNewerVersion returns true if newversion is greater than oldversion in terms of semver. // If major is true, then newversion must be a major version ahead of oldversion to be considered newer. func IsNewerVersion(oldversion, newversion string, major bool) bool { @@ -58,32 +82,44 @@ func IsNewerVersion(oldversion, newversion string, major bool) bool { // Invalid versions are considered lower than valid ones. // If both versions are invalid, the empty string is returned. func MaxVersion(v, w string) string { + // sort by validity + if !semver.IsValid(v) && !semver.IsValid(w) { + return "" + } + if CompareVersion(v, w) == 1 { + return v + } + return w +} + +// CompareVersion returns -1, 0, or 1 if v is less than, equal to, or greater than w. +// Incompatible versions are considered lower than non-incompatible ones. +// Invalid versions are considered lower than valid ones. +// If both versions are invalid, the empty string is returned. +func CompareVersion(v, w string) int { // sort by validity vValid := semver.IsValid(v) wValid := semver.IsValid(w) if !vValid && !wValid { - return "" + return 0 } if vValid != wValid { if vValid { - return v + return 1 } - return w + return -1 } // sort by compatibility vIncompatible := strings.HasSuffix(semver.Build(v), "+incompatible") wIncompatible := strings.HasSuffix(semver.Build(w), "+incompatible") if vIncompatible != wIncompatible { if wIncompatible { - return v + return 1 } - return w + return -1 } // sort by semver - if semver.Compare(v, w) == 1 { - return v - } - return w + return semver.Compare(v, w) } // NextMajor returns the next major version after the provided version @@ -174,13 +210,20 @@ func Latest(modpath string, cached, pre bool) (*Module, error) { if err != nil { return nil, err } - for i := len(mods); i > 0; i-- { - mod := mods[i-1] - if max := mod.MaxVersion("", pre); max != "" { - return mod, nil + // find the retractions + var r Retractions + if mod, _ := MaxVersionModule(mods, false, nil); mod != nil { + var err error + r, err = FetchRetractions(mod) + if err != nil { + return nil, err } } - return nil, ErrNoVersions + mod, _ := MaxVersionModule(mods, pre, r) + if mod == nil { + return nil, ErrNoVersions + } + return mod, nil } // List finds all the major versions of a module @@ -260,6 +303,70 @@ func QueryPackage(pkgpath string, cached bool) (*Module, error) { return nil, fmt.Errorf("failed to find module for package: %s", pkgpath) } +// FetchRetractions fetches the retractions for this module. +func FetchRetractions(mod *Module) (Retractions, error) { + max := mod.MaxVersion("", false) + if max == "" { + return nil, nil + } + escaped, err := module.EscapePath(mod.Path) + if err != nil { + return nil, err + } + req, err := http.NewRequest("GET", "https://proxy.golang.org/"+escaped+"/@v/"+max+".mod", nil) + if err != nil { + return nil, err + } + res, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + body, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + if res.StatusCode != 200 { + msg := string(body) + if msg == "" { + msg = res.Status + } + return nil, fmt.Errorf("proxy: %s", msg) + } + file, err := modfile.ParseLax(mod.Path, body, nil) + if err != nil { + return nil, err + } + var retractions Retractions + for _, r := range file.Retract { + retractions = append(retractions, VersionRange{Low: r.Low, High: r.High}) + } + return retractions, nil +} + +// VersionRange is an inclusive version range. +type VersionRange struct { + Low, High string +} + +// Includes reports whether v is in the inclusive range +func (r VersionRange) Includes(v string) bool { + return CompareVersion(v, r.Low) >= 0 && CompareVersion(v, r.High) <= 0 +} + +// Retractions is a list of retracted versions. +type Retractions []VersionRange + +// Includes reports whether v is retracted +func (rr Retractions) Includes(v string) bool { + for _, r := range rr { + if r.Includes(v) { + return true + } + } + return false +} + // Update reports a newer version of a module. // The Err field will be set if an error occured. type Update struct { diff --git a/internal/modproxy/modproxy_test.go b/internal/modproxy/modproxy_test.go index a9561ca..03fe8d0 100644 --- a/internal/modproxy/modproxy_test.go +++ b/internal/modproxy/modproxy_test.go @@ -250,3 +250,61 @@ func TestIsNewerVersion(t *testing.T) { }) } } + +func TestCompareVersion(t *testing.T) { + tests := []struct { + v, w string + want int + }{ + {v: "v0.0.0", w: "v1.0.0", want: -1}, + {v: "v1.0.0", w: "v0.0.0", want: 1}, + {v: "v0.0.0", w: "v0.0.0", want: 0}, + {v: "v12.0.0+incompatible", w: "v0.0.0", want: -1}, + {v: "", w: "", want: 0}, + {v: "v0.1.0", w: "bad", want: 1}, + {v: "v0.0.0+incompatible", w: "v0.0.0", want: -1}, + } + for _, tt := range tests { + t.Run("", func(t *testing.T) { + if got := CompareVersion(tt.v, tt.w); got != tt.want { + t.Fatalf("CompareVersion(%q, %q) = %v, want %v", tt.v, tt.w, got, tt.want) + } + }) + } +} + +func TestVersionRange(t *testing.T) { + tests := []struct { + r VersionRange + v string + want bool + }{ + { + r: VersionRange{Low: "v0.0.0", High: "v0.0.1"}, + v: "v0.0.0", + want: true, + }, + { + r: VersionRange{Low: "v0.0.0", High: "v0.0.1"}, + v: "v0.0.1", + want: true, + }, + { + r: VersionRange{Low: "v0.0.0", High: "v0.0.1"}, + v: "v0.0.2", + want: false, + }, + { + r: VersionRange{Low: "v0.0.0", High: "v0.0.1"}, + v: "v0.0.0+incompatible", + want: false, + }, + } + for _, tt := range tests { + t.Run("", func(t *testing.T) { + if got := tt.r.Includes(tt.v); got != tt.want { + t.Fatalf("VersionRange{Low: %q, High: %q}.Includes(%q) = %v, want %v", tt.r.Low, tt.r.High, tt.v, got, tt.want) + } + }) + } +}