Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions constraint.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package vers

import (
"fmt"
"net/url"
"regexp"
"strings"
)
Expand Down Expand Up @@ -40,6 +41,9 @@ func parseConstraintWithScheme(s, scheme string) (*Constraint, error) {
if version == "" {
return nil, fmt.Errorf("invalid constraint format: %s", s)
}
if decoded, err := url.PathUnescape(version); err == nil {
version = decoded
}
if !preserveVPrefix {
version = stripVPrefix(version)
}
Expand All @@ -48,6 +52,9 @@ func parseConstraintWithScheme(s, scheme string) (*Constraint, error) {

// No operator found, treat as exact match
version := s
if decoded, err := url.PathUnescape(version); err == nil {
version = decoded
}
if !preserveVPrefix {
version = stripVPrefix(s)
}
Expand Down
23 changes: 19 additions & 4 deletions parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ func (p *Parser) ToVersString(r *Range, scheme string) string {
if interval.Min == interval.Max && interval.MinInclusive && interval.MaxInclusive && interval.Min != "" {
// Exact version - no operator needed per VERS spec
constraints = append(constraints, constraintWithVersion{
str: normalizeVersion(interval.Min, scheme),
str: encodeVersVersion(normalizeVersion(interval.Min, scheme)),
sortKey: interval.Min,
})
} else {
Expand All @@ -93,7 +93,7 @@ func (p *Parser) ToVersString(r *Range, scheme string) string {
op = ">="
}
constraints = append(constraints, constraintWithVersion{
str: op + normalizeVersion(interval.Min, scheme),
str: op + encodeVersVersion(normalizeVersion(interval.Min, scheme)),
sortKey: interval.Min,
})
}
Expand All @@ -103,7 +103,7 @@ func (p *Parser) ToVersString(r *Range, scheme string) string {
op = "<="
}
constraints = append(constraints, constraintWithVersion{
str: op + normalizeVersion(interval.Max, scheme),
str: op + encodeVersVersion(normalizeVersion(interval.Max, scheme)),
sortKey: interval.Max,
})
}
Expand All @@ -113,7 +113,7 @@ func (p *Parser) ToVersString(r *Range, scheme string) string {
// Add exclusions
for _, exc := range r.Exclusions {
constraints = append(constraints, constraintWithVersion{
str: "!=" + normalizeVersion(exc, scheme),
str: "!=" + encodeVersVersion(normalizeVersion(exc, scheme)),
sortKey: exc,
})
}
Expand Down Expand Up @@ -147,6 +147,21 @@ func sortConstraintsByVersion(constraints []constraintWithVersion) {
}
}

var versMetaEncoder = strings.NewReplacer(
"|", "%7C",
">", "%3E",
"<", "%3C",
"=", "%3D",
"!", "%21",
"/", "%2F",
"*", "%2A",
" ", "%20",
)

func encodeVersVersion(v string) string {
return versMetaEncoder.Replace(v)
}

// normalizeVersion normalizes a version string for output.
// For semver-based schemes, this ensures 3-part versions (1.1 -> 1.1.0).
func normalizeVersion(version, scheme string) string {
Expand Down
20 changes: 20 additions & 0 deletions parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,26 @@ func TestToVersString(t *testing.T) {
}
}

func TestToVersStringEncodesMetacharacters(t *testing.T) {
parser := NewParser()

// A version string containing a VERS separator should be percent-encoded
r := Exact("1.0|2.0")
got := parser.ToVersString(r, "deb")
if strings.Contains(got, "|2.0") && !strings.Contains(got, "%7C") {
t.Errorf("ToVersString should encode | in version, got %q", got)
}

// Roundtrip: parse the encoded output and check containment
parsed, err := parser.Parse(got)
if err != nil {
t.Fatalf("Parse(ToVersString()) failed: %v", err)
}
if !parsed.Contains("1.0|2.0") {
t.Error("roundtrip should preserve exact version with metacharacter")
}
}

func TestPublicAPISatisfies(t *testing.T) {
tests := []struct {
name string
Expand Down