Repository: G-Rath/osv-detector
Pull request #74 (2022-03-18 02:44:52)

fix: trim off leading "v" in version strings when parsing
All versions in the OSV advisories are strictly semver, so have no leading "v" - however `composer` packages can have an optional leading v, which currently causes the whole version string to be treated as having no components and just a build string.

This changes that to trim out the leading "v" if present - I've implemented this to actually track if the version had a leading "v" and undo the trim + track if the string turns out to have no components, because it was straightforward to do and so a nice optional extra.


Patch:
diff --git a/internal/semantic/parse.go b/internal/semantic/parse.go
index ddd2424..3754850 100644
--- a/internal/semantic/parse.go
+++ b/internal/semantic/parse.go
@@ -3,6 +3,7 @@ package semantic
 import (
 	"regexp"
 	"strconv"
+	"strings"
 )
 
 func Parse(line string) Version {
@@ -14,6 +15,9 @@ func Parse(line string) Version {
 	foundBuild := false
 	emptyComponent := false
 
+	leadingV := strings.HasPrefix(line, "v")
+	line = strings.TrimPrefix(line, "v")
+
 	for _, c := range line {
 		if foundBuild {
 			currentCom += string(c)
@@ -76,7 +80,14 @@ func Parse(line string) Version {
 		currentCom = "." + currentCom
 	}
 
+	// if we found no components, then the v wasn't actually leading
+	if len(components) == 0 && leadingV {
+		leadingV = false
+		currentCom = "v" + currentCom
+	}
+
 	return Version{
+		LeadingV:   leadingV,
 		Components: components,
 		Build:      currentCom,
 	}
diff --git a/internal/semantic/version.go b/internal/semantic/version.go
index 42baa2b..50cf7d5 100644
--- a/internal/semantic/version.go
+++ b/internal/semantic/version.go
@@ -8,6 +8,7 @@ import (
 type Components []int
 
 type Version struct {
+	LeadingV   bool
 	Components Components
 	Build      string
 }
@@ -23,6 +24,10 @@ func (components *Components) Fetch(n int) int {
 func (v Version) ToString() string {
 	str := ""
 
+	if v.LeadingV {
+		str += "v"
+	}
+
 	for _, component := range v.Components {
 		str += fmt.Sprintf("%d.", component)
 	}


Test patch:
diff --git a/internal/semantic/compare_test.go b/internal/semantic/compare_test.go
index 0aa2a63..afa3eff 100644
--- a/internal/semantic/compare_test.go
+++ b/internal/semantic/compare_test.go
@@ -420,3 +420,38 @@ func TestVersion_Compare_MixedWithAndWithoutBuild(t *testing.T) {
 		-1,
 	)
 }
+
+// leading "v" is just cosmetic, and shouldn't change the comparing
+func TestVersion_Compare_BasicWithLeadingV(t *testing.T) {
+	t.Parallel()
+
+	expectCompareResult(t,
+		semantic.Version{LeadingV: false, Components: []int{1}, Build: ""},
+		semantic.Version{LeadingV: false, Components: []int{1}, Build: ""},
+		0,
+	)
+
+	expectCompareResult(t,
+		semantic.Version{LeadingV: true, Components: []int{1}, Build: ""},
+		semantic.Version{LeadingV: false, Components: []int{1}, Build: ""},
+		0,
+	)
+
+	expectCompareResult(t,
+		semantic.Version{LeadingV: false, Components: []int{1}, Build: ""},
+		semantic.Version{LeadingV: true, Components: []int{1}, Build: ""},
+		0,
+	)
+
+	expectCompareResult(t,
+		semantic.Version{LeadingV: true, Components: []int{1}, Build: ""},
+		semantic.Version{LeadingV: true, Components: []int{1}, Build: ""},
+		0,
+	)
+
+	expectCompareResult(t,
+		semantic.Version{LeadingV: true, Components: []int{2}, Build: ""},
+		semantic.Version{LeadingV: true, Components: []int{1}, Build: ""},
+		1,
+	)
+}
diff --git a/internal/semantic/parse_test.go b/internal/semantic/parse_test.go
index e66d108..208bc33 100644
--- a/internal/semantic/parse_test.go
+++ b/internal/semantic/parse_test.go
@@ -102,36 +102,43 @@ func TestParse_Standard(t *testing.T) {
 	t.Parallel()
 
 	expectParsedAsVersion(t, "0.0.0.0", semantic.Version{
+		LeadingV:   false,
 		Components: []int{0, 0, 0, 0},
 		Build:      "",
 	})
 
 	expectParsedAsVersion(t, "1.0.0.0", semantic.Version{
+		LeadingV:   false,
 		Components: []int{1, 0, 0, 0},
 		Build:      "",
 	})
 
 	expectParsedAsVersion(t, "1.2.0.0", semantic.Version{
+		LeadingV:   false,
 		Components: []int{1, 2, 0, 0},
 		Build:      "",
 	})
 
 	expectParsedAsVersion(t, "1.2.3.0", semantic.Version{
+		LeadingV:   false,
 		Components: []int{1, 2, 3, 0},
 		Build:      "",
 	})
 
 	expectParsedAsVersion(t, "1.2.3.4", semantic.Version{
+		LeadingV:   false,
 		Components: []int{1, 2, 3, 4},
 		Build:      "",
 	})
 
 	expectParsedAsVersion(t, "9.2.55826.0", semantic.Version{
+		LeadingV:   false,
 		Components: []int{9, 2, 55826, 0},
 		Build:      "",
 	})
 
 	expectParsedAsVersion(t, "3.2.22.3", semantic.Version{
+		LeadingV:   false,
 		Components: []int{3, 2, 22, 3},
 		Build:      "",
 	})
@@ -141,21 +148,25 @@ func TestParse_Omitted(t *testing.T) {
 	t.Parallel()
 
 	expectParsedAsVersion(t, "1", semantic.Version{
+		LeadingV:   false,
 		Components: []int{1},
 		Build:      "",
 	})
 
 	expectParsedAsVersion(t, "1.2", semantic.Version{
+		LeadingV:   false,
 		Components: []int{1, 2},
 		Build:      "",
 	})
 
 	expectParsedAsVersion(t, "1.2.3", semantic.Version{
+		LeadingV:   false,
 		Components: []int{1, 2, 3},
 		Build:      "",
 	})
 
 	expectParsedAsVersion(t, "1.2.3.", semantic.Version{
+		LeadingV:   false,
 		Components: []int{1, 2, 3},
 		Build:      ".",
 	})
@@ -165,41 +176,49 @@ func TestParse_WithBuildString(t *testing.T) {
 	t.Parallel()
 
 	expectParsedAsVersion(t, "10.0.0.beta1", semantic.Version{
+		LeadingV:   false,
 		Components: []int{10, 0, 0},
 		Build:      ".beta1",
 	})
 
 	expectParsedAsVersion(t, "1.0.0a20", semantic.Version{
+		LeadingV:   false,
 		Components: []int{1, 0, 0},
 		Build:      "a20",
 	})
 
 	expectParsedAsVersion(t, "9.0.0.pre1", semantic.Version{
+		LeadingV:   false,
 		Components: []int{9, 0, 0},
 		Build:      ".pre1",
 	})
 
 	expectParsedAsVersion(t, "9.4.16.v20190411", semantic.Version{
+		LeadingV:   false,
 		Components: []int{9, 4, 16},
 		Build:      ".v20190411",
 	})
 
 	expectParsedAsVersion(t, "0.3.0-beta.83", semantic.Version{
+		LeadingV:   false,
 		Components: []int{0, 3, 0},
 		Build:      "-beta.83",
 	})
 
 	expectParsedAsVersion(t, "3.0.0-beta.17.5", semantic.Version{
+		LeadingV:   false,
 		Components: []int{3, 0, 0},
 		Build:      "-beta.17.5",
 	})
 
 	expectParsedAsVersion(t, "4.0.0-milestone3", semantic.Version{
+		LeadingV:   false,
 		Components: []int{4, 0, 0},
 		Build:      "-milestone3",
 	})
 
 	expectParsedAsVersion(t, "13.6RC1", semantic.Version{
+		LeadingV:   false,
 		Components: []int{13, 6},
 		Build:      "RC1",
 	})
@@ -239,15 +258,39 @@ func TestParse_NoComponents(t *testing.T) {
 	expectParsedVersionToMatchOriginalString(t, "hello world!")
 }
 
+func TestParse_LeadingV(t *testing.T) {
+	t.Parallel()
+
+	expectParsedVersionToMatchString(t, "v1.0.0", "v1.0.0", semantic.Version{
+		LeadingV:   true,
+		Components: []int{1, 0, 0},
+		Build:      "",
+	})
+
+	expectParsedVersionToMatchString(t, "v1.2.3-beta1", "v1.2.3-beta1", semantic.Version{
+		LeadingV:   true,
+		Components: []int{1, 2, 3},
+		Build:      "-beta1",
+	})
+
+	expectParsedVersionToMatchString(t, "version210", "version210", semantic.Version{
+		LeadingV:   false,
+		Components: []int{},
+		Build:      "version210",
+	})
+}
+
 func TestParse_LeadingZerosAndDateLike(t *testing.T) {
 	t.Parallel()
 
 	expectParsedVersionToMatchString(t, "20.04.0", "20.4.0", semantic.Version{
+		LeadingV:   false,
 		Components: []int{20, 4, 0},
 		Build:      "",
 	})
 
 	expectParsedVersionToMatchString(t, "4.3.04", "4.3.4", semantic.Version{
+		LeadingV:   false,
 		Components: []int{4, 3, 4},
 		Build:      "",
 	})
@@ -264,31 +307,37 @@ func TestParse_DateLike(t *testing.T) {
 	t.Parallel()
 
 	expectParsedVersionToMatchString(t, "20.04.0", "20.4.0", semantic.Version{
+		LeadingV:   false,
 		Components: []int{20, 4, 0},
 		Build:      "",
 	})
 
 	expectParsedVersionToMatchString(t, "4.3.04alpha01", "4.3.4alpha01", semantic.Version{
+		LeadingV:   false,
 		Components: []int{4, 3, 4},
 		Build:      "alpha01",
 	})
 
 	expectParsedVersionToMatchString(t, "2019.03.6.1", "2019.3.6.1", semantic.Version{
+		LeadingV:   false,
 		Components: []int{2019, 3, 6, 1},
 		Build:      "",
 	})
 
 	expectParsedVersionToMatchString(t, "19.04.15", "19.4.15", semantic.Version{
+		LeadingV:   false,
 		Components: []int{19, 4, 15},
 		Build:      "",
 	})
 
 	expectParsedVersionToMatchString(t, "20.04.13", "20.4.13", semantic.Version{
+		LeadingV:   false,
 		Components: []int{20, 4, 13},
 		Build:      "",
 	})
 
 	expectParsedVersionToMatchString(t, "2019.11.09", "2019.11.9", semantic.Version{
+		LeadingV:   false,
 		Components: []int{2019, 11, 9},
 		Build:      "",
 	})