From 23463d20bc53b1ce2b68b60fd33fb8cfa493a967 Mon Sep 17 00:00:00 2001 From: Andrew Nesbitt Date: Wed, 19 Aug 2026 11:08:28 +0100 Subject: [PATCH 1/5] Preserve plain versions in PURLs --- makepurl.go | 10 +++++++--- makepurl_test.go | 2 ++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/makepurl.go b/makepurl.go index 37255f2..96444c9 100644 --- a/makepurl.go +++ b/makepurl.go @@ -7,13 +7,17 @@ import ( packageurl "github.com/package-url/packageurl-go" ) -// CleanVersion extracts a version from a version constraint string. -// Uses the vers library to parse the constraint and extract the minimum bound. -// If parsing fails, returns the original string. +// CleanVersion returns plain versions unchanged. For version constraints, it +// uses the vers library to extract the minimum bound. If parsing fails, it +// returns the original string. func CleanVersion(version, scheme string) string { + version = strings.TrimSpace(version) if version == "" { return "" } + if vers.ValidWithScheme(version, scheme) { + return version + } r, err := vers.ParseNative(version, scheme) if err != nil || len(r.Intervals) == 0 { diff --git a/makepurl_test.go b/makepurl_test.go index fbf61c8..b658f34 100644 --- a/makepurl_test.go +++ b/makepurl_test.go @@ -29,6 +29,8 @@ func TestCleanVersion(t *testing.T) { {"^1.0.0", "cargo", "1.0.0"}, // Plain versions pass through + {"1.0", "composer", "1.0"}, + {"1.0", "npm", "1.0"}, {"1.0.0", "npm", "1.0.0"}, {"v1.0.0", "go", "v1.0.0"}, From 0b6d1a072ef2e16b879047ac05ecc6bcef703b71 Mon Sep 17 00:00:00 2001 From: Andrew Nesbitt Date: Wed, 19 Aug 2026 12:28:08 +0100 Subject: [PATCH 2/5] Bump vers to 0.6.0 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index b5341fa..d2e7208 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,6 @@ module github.com/git-pkgs/purl go 1.25.6 require ( - github.com/git-pkgs/vers v0.3.1 + github.com/git-pkgs/vers v0.6.0 github.com/package-url/packageurl-go v0.1.7 ) diff --git a/go.sum b/go.sum index beb0db0..8e48ab8 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,4 @@ -github.com/git-pkgs/vers v0.3.1 h1:jy/ht2wIRJI5zQrccm6GTeYr+hGFwe2z8LV1HOr4Wco= -github.com/git-pkgs/vers v0.3.1/go.mod h1:biTbSQK1qdbrsxDEKnqe3Jzclxz8vW6uDcwKjfUGcOo= +github.com/git-pkgs/vers v0.6.0 h1:droJw8+oSyl8/UoDj/96B9ZPggmxJDTl/JeixzfKzSc= +github.com/git-pkgs/vers v0.6.0/go.mod h1:biTbSQK1qdbrsxDEKnqe3Jzclxz8vW6uDcwKjfUGcOo= github.com/package-url/packageurl-go v0.1.7 h1:iFWg6tzAjLA6F/qX3M5nZaiMHJgc+p2zxVyr/fY+sZY= github.com/package-url/packageurl-go v0.1.7/go.mod h1:nKAWB8E6uk1MHqiS/lQb9pYBGH2+mdJ2PJc2s50dQY0= From 912ff012a0c61b52d1e3ffe14b7987b95c433e6b Mon Sep 17 00:00:00 2001 From: Andrew Nesbitt Date: Wed, 19 Aug 2026 12:43:09 +0100 Subject: [PATCH 3/5] Preserve whitespace in plain versions --- makepurl.go | 1 - makepurl_test.go | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/makepurl.go b/makepurl.go index 96444c9..0708996 100644 --- a/makepurl.go +++ b/makepurl.go @@ -11,7 +11,6 @@ import ( // uses the vers library to extract the minimum bound. If parsing fails, it // returns the original string. func CleanVersion(version, scheme string) string { - version = strings.TrimSpace(version) if version == "" { return "" } diff --git a/makepurl_test.go b/makepurl_test.go index b658f34..83e4ebe 100644 --- a/makepurl_test.go +++ b/makepurl_test.go @@ -33,9 +33,11 @@ func TestCleanVersion(t *testing.T) { {"1.0", "npm", "1.0"}, {"1.0.0", "npm", "1.0.0"}, {"v1.0.0", "go", "v1.0.0"}, + {" 1.0 ", "composer", " 1.0 "}, - // Empty + // Empty and whitespace-only {"", "npm", ""}, + {" ", "npm", " "}, } for _, tt := range tests { From 71efebe40875d9d743a1995223f0e5c7b4c76ea4 Mon Sep 17 00:00:00 2001 From: Andrew Nesbitt Date: Wed, 19 Aug 2026 18:18:20 +0100 Subject: [PATCH 4/5] Preserve native constraint parsing --- makepurl.go | 24 +++++++++++++++++++++++- makepurl_test.go | 6 ++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/makepurl.go b/makepurl.go index 0708996..da3555a 100644 --- a/makepurl.go +++ b/makepurl.go @@ -14,7 +14,7 @@ func CleanVersion(version, scheme string) string { if version == "" { return "" } - if vers.ValidWithScheme(version, scheme) { + if !hasConstraintSyntax(version, scheme) && vers.ValidWithScheme(version, scheme) { return version } @@ -31,6 +31,28 @@ func CleanVersion(version, scheme string) string { return version } +func hasConstraintSyntax(version, scheme string) bool { + constraint := strings.TrimSpace(version) + if constraint == "" { + return false + } + + switch constraint[0] { + case '<', '>', '=', '!', '^', '~': + return true + } + + switch scheme { + case ecosystemMaven: + return constraint[0] == '[' || constraint[0] == '(' + case "conan": + return constraint == "*" || constraint == "*-" || + strings.Contains(constraint, "||") || strings.Contains(constraint, ",") + } + + return false +} + // BuildPURLString builds a PURL string directly from ecosystem-native identifiers // without creating intermediate PURL structs. This is the fast path for manifest // parsing where we just need the string output. It returns an empty string when diff --git a/makepurl_test.go b/makepurl_test.go index 83e4ebe..799522d 100644 --- a/makepurl_test.go +++ b/makepurl_test.go @@ -25,9 +25,15 @@ func TestCleanVersion(t *testing.T) { {">=1.0.0", "pypi", "1.0.0"}, {"~=1.4.2", "pypi", "1.4.2"}, + // maven constraints + {"[1.0,2.0)", "maven", "1.0"}, + // cargo constraints {"^1.0.0", "cargo", "1.0.0"}, + // conan constraints + {"^1.2.3", "conan", "1.2.3"}, + // Plain versions pass through {"1.0", "composer", "1.0"}, {"1.0", "npm", "1.0"}, From 2bb5a59647113430e3ba314493c11fb90a78300b Mon Sep 17 00:00:00 2001 From: Andrew Nesbitt Date: Thu, 27 Aug 2026 10:25:40 +0100 Subject: [PATCH 5/5] Handle Conan prerelease ranges --- makepurl.go | 3 ++- makepurl_test.go | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/makepurl.go b/makepurl.go index da3555a..ac3cb8f 100644 --- a/makepurl.go +++ b/makepurl.go @@ -47,7 +47,8 @@ func hasConstraintSyntax(version, scheme string) bool { return constraint[0] == '[' || constraint[0] == '(' case "conan": return constraint == "*" || constraint == "*-" || - strings.Contains(constraint, "||") || strings.Contains(constraint, ",") + strings.HasSuffix(constraint, "-") || strings.Contains(constraint, "||") || + strings.Contains(constraint, ",") } return false diff --git a/makepurl_test.go b/makepurl_test.go index 799522d..9d68d81 100644 --- a/makepurl_test.go +++ b/makepurl_test.go @@ -33,6 +33,7 @@ func TestCleanVersion(t *testing.T) { // conan constraints {"^1.2.3", "conan", "1.2.3"}, + {"1.2.3-", "conan", "1.2.3"}, // Plain versions pass through {"1.0", "composer", "1.0"}, @@ -74,6 +75,7 @@ func TestBuildPURLString(t *testing.T) { {"with registry", "npm", "lodash", "1.0.0", "https://npm.example.com", "pkg:npm/lodash@1.0.0?repository_url=https:%2F%2Fnpm.example.com"}, {"default registry ignored", "npm", "lodash", "1.0.0", "https://registry.npmjs.org", "pkg:npm/lodash@1.0.0"}, {"composer", "packagist", "vendor/pkg", "1.0", "", "pkg:composer/vendor/pkg@1.0"}, + {"conan range prerelease marker", "conan", "openssl", "1.2.3-", "", "pkg:conan/openssl@1.2.3"}, {"composer normalization", "packagist", "Vendor/Package", "1.0", "", "pkg:composer/vendor/package@1.0"}, {"pypi normalization", "pypi", "Django_REST", "1.0.0", "", "pkg:pypi/django-rest@1.0.0"}, {"golang normalization", "golang", "GitHub.com/Foo/Bar", "v1.0.0", "", "pkg:golang/github.com/foo/bar@v1.0.0"},