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
8 changes: 8 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,11 @@ updates:
open-pull-requests-limit: 5
cooldown:
default-days: 7

- package-ecosystem: gitsubmodule
directory: /
schedule:
interval: weekly
open-pull-requests-limit: 5
cooldown:
default-days: 7
29 changes: 28 additions & 1 deletion parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func (p *Parser) parseVersURI(versURI string, requireCanonicalOrder bool) (*Rang
if !strings.HasPrefix(versURI, prefix) {
return nil, fmt.Errorf("invalid vers URI format: %s", versURI)
}
if strings.ContainsAny(versURI, " \t\r\n") {
if strings.ContainsAny(versURI, "\t\r\n") {
return nil, fmt.Errorf("non-canonical VERS: whitespace is not permitted")
}
remainder := versURI[len(prefix):]
Expand All @@ -40,6 +40,12 @@ func (p *Parser) parseVersURI(versURI string, requireCanonicalOrder bool) (*Rang
scheme := remainder[:slash]
constraintsStr := remainder[slash+1:]

if strings.ContainsRune(scheme, ' ') || (requireCanonicalOrder && strings.ContainsRune(constraintsStr, ' ')) {
return nil, fmt.Errorf("non-canonical VERS: whitespace is not permitted")
}
if constraintsStr == "" && requireCanonicalOrder {
return nil, fmt.Errorf("non-canonical VERS: constraints must not be empty")
}
// Handle wildcard for unbounded range
if constraintsStr == "*" || constraintsStr == "" {
r := Unbounded()
Expand Down Expand Up @@ -67,6 +73,9 @@ func validateVersConstraints(constraints, scheme string, requireCanonicalOrder b
var previous *Constraint
previousRaw := ""
for _, raw := range strings.Split(constraints, "|") {
if requireCanonicalOrder && len(raw) > 0 && raw[0] == '=' {
return fmt.Errorf("non-canonical VERS: explicit equality comparator is not permitted")
}
operator := constraintOperator(raw)
version := raw[len(operator):]
if err := validateVersVersion(version, scheme); err != nil {
Expand Down Expand Up @@ -98,6 +107,9 @@ func validateVersVersion(version, scheme string) error {
if isLowerASCIIHex(version[i+1]) || isLowerASCIIHex(version[i+2]) {
return fmt.Errorf("non-canonical VERS: percent-encoding in version is not canonical")
}
if b := hexByte(version[i+1], version[i+2]); b == '\t' || b == '\r' || b == '\n' {
return fmt.Errorf("non-canonical VERS: whitespace is not permitted")
}
i += 2
}

Expand All @@ -121,6 +133,21 @@ func isASCIIHex(c byte) bool {
return isASCIIDigit(c) || c >= 'A' && c <= 'F' || isLowerASCIIHex(c)
}

func hexByte(hi, lo byte) byte {
return hexNibble(hi)<<4 | hexNibble(lo)
}

func hexNibble(c byte) byte {
switch {
case isASCIIDigit(c):
return c - '0'
case c >= 'A' && c <= 'F':
return c - 'A' + 10
default:
return c - 'a' + 10
}
}

func isLowerASCIIHex(c byte) bool {
return c >= 'a' && c <= 'f'
}
Expand Down
44 changes: 44 additions & 0 deletions parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,50 @@ func TestParseVersURI(t *testing.T) {
}
}

func TestParseVersURIStrict(t *testing.T) {
tests := []struct {
input string
wantErr string
}{
{"vers:npm/", "constraints must not be empty"},
{"vers:npm/=1.0.0", "explicit equality comparator"},
{"vers:npm/==1.0.0", "explicit equality comparator"},
{"vers:lexicographic/1%092", "whitespace is not permitted"},
{"vers:lexicographic/1 2", "whitespace is not permitted"},
{"vers:np m/1.0.0", "whitespace is not permitted"},
{"vers:lexicographic/1%202", ""},
{"vers:npm/1.0.0", ""},
}
for _, tt := range tests {
_, err := defaultParser.parseVersURI(tt.input, true)
if tt.wantErr == "" {
if err != nil {
t.Errorf("parseVersURI(%q, strict) error = %v, want nil", tt.input, err)
}
} else if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
t.Errorf("parseVersURI(%q, strict) error = %v, want substring %q", tt.input, err, tt.wantErr)
}
}
}

func TestParseVersURILenientSpace(t *testing.T) {
r, err := defaultParser.parseVersURI("vers:lexicographic/1 2", false)
if err != nil {
t.Fatalf("lenient parse of literal space failed: %v", err)
}
if got := ToVersString(r, r.Scheme); got != "vers:lexicographic/1%202" {
t.Errorf("roundtrip = %q, want vers:lexicographic/1%%202", got)
}

// Only the constraints may carry a literal space; a scheme with one has no
// canonical form to re-emit.
for _, input := range []string{"vers:np m/1.0.0", "vers: npm/1.0.0"} {
if _, err := defaultParser.parseVersURI(input, false); err == nil {
t.Errorf("lenient parse of %q succeeded, want error", input)
}
}
}

func TestParseNpmRange(t *testing.T) {
tests := []struct {
name string
Expand Down
10 changes: 10 additions & 0 deletions schemes.go
Original file line number Diff line number Diff line change
Expand Up @@ -611,7 +611,17 @@ func compareRPMPart(a, b string) int { //nolint:gocyclo,gocognit
return 0
}

func intDotPrefix(s string) string {
for i := 0; i < len(s); i++ {
if s[i] != '.' && !isASCIIDigit(s[i]) {
return s[:i]
}
}
return s
}

func compareIntDot(a, b string) int {
a, b = intDotPrefix(a), intDotPrefix(b)
pa, pb := strings.Split(a, "."), strings.Split(b, ".")
for i := 0; i < len(pa) || i < len(pb); i++ {
var va, vb string
Expand Down
3 changes: 3 additions & 0 deletions schemes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,9 @@ func TestExistingSchemeComparatorEdges(t *testing.T) {
{"maven", "1.999999999999999999999999", "1.2", 1},
{"lexicographic", "10", "2", -1},
{"intdot", "1.0.0.1", "1.0.0", 1},
{"intdot", "1.2beta", "1.2rc1", 0},
{"intdot", "1.10alpha", "1.3rc1", 1},
{"intdot", "1.2", "1.2.0", 0},
{"gentoo", "01", "1", 0},
}
for _, tt := range tests {
Expand Down
2 changes: 1 addition & 1 deletion testdata/vers-spec
Submodule vers-spec updated 40 files
+0 −0 .github/workflows-archive/validate-docs.yml
+66 −0 .github/workflows/generate-index-and-docs.yml
+1 −0 Makefile
+19 −17 docs/faq.md
+9 −9 docs/problem-solution.md
+163 −141 docs/specification/how-to-parse.md
+29 −0 docs/specification/standard/About.md
+145 −0 docs/specification/standard/Annex-A-VERS-Type-Definition.md
+7 −0 docs/specification/standard/Bibliography.md
+17 −0 docs/specification/standard/Clause-1-Scope.md
+5 −5 docs/specification/standard/Clause-2-Conformance.md
+12 −0 docs/specification/standard/Clause-3-Normative-References.md
+28 −0 docs/specification/standard/Clause-4-Overview.md
+208 −0 docs/specification/standard/Clause-5-VERS-Specification.md
+144 −0 docs/specification/standard/Clause-6-VERS-Type-Definition-Schema.md
+15 −0 docs/specification/standard/Colophon.md
+95 −0 docs/specification/standard/Copyright.md
+2 −9 docs/specification/standard/Introduction.md
+9 −2 docs/specification/standard/specification.md
+1 −1 docs/tests/test-overview.md
+1 −1 docs/tests/test-suite.md
+10 −0 docs/types/definitions/README.md
+22 −0 docs/types/definitions/npm-definition.md
+20 −0 docs/types/definitions/pypi-definition.md
+36 −0 docs/types/details/vers-type-details-template.md
+11 −11 docs/types/vers-types.md
+82 −0 docs/use-cases.md
+86 −0 etc/scripts/generate_index_and_docs.py
+1 −1 schemas/vers-test.schema-0.2.json
+0 −229 schemas/vers-type-definition.schema-0.1.json
+137 −0 schemas/vers-type-definition.schema-1.0.json
+11 −0 schemas/vers-types-index.schema-1.0.json
+1 −1 tests/datetime_version_cmp_test.json
+168 −0 tests/intdot_version_cmp_test.json
+2 −2 tests/lexicographic-test.json
+82 −5 tests/vers_canonical_parse_test.json
+33 −0 types/README.md
+53 −0 types/npm-definition.json
+47 −0 types/pypi-definition.json
+4 −0 vers-types-index.json
Loading