diff --git a/README.md b/README.md index e0c49f7..173481f 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ following value of DID type. IDStrings:[]string{"q7ckgxeq1lxmra0r"}, Path:"", PathSegments:[]string(nil), + Query:"", Fragment:"" } ``` @@ -66,10 +67,20 @@ which would result in: IDStrings:[]string{"q7ckgxeq1lxmra0r"}, Path:"abc/pqr", PathSegments:[]string{"abc", "pqr"}, + Query:"", Fragment:"" } ``` +or a [DID Reference](https://w3c-ccg.github.io/did-spec/#dfn-did-reference) with a +[DID Path](https://w3c-ccg.github.io/did-spec/#dfn-did-path) and a DID Query: + +```go +d, err := did.Parse("did:example:q7ckgxeq1lxmra0r/abc/pqr?xyz") +fmt.Println(d.Query) +// Output: xyz +``` + or a [DID Reference](https://w3c-ccg.github.io/did-spec/#dfn-did-reference) with a [DID Fragment](https://w3c-ccg.github.io/did-spec/#dfn-did-fragment): @@ -131,17 +142,19 @@ go test -bench=. `did.Parse` included in this package: ``` -BenchmarkParse-8 5000000 345 ns/op -BenchmarkParseWithPath-8 3000000 477 ns/op -BenchmarkParseWithFragment-8 3000000 542 ns/op +BenchmarkParse-8 5000000 365 ns/op +BenchmarkParseWithPath-8 3000000 500 ns/op +BenchmarkParseWithQuery-8 3000000 558 ns/op +BenchmarkParseWithFragment-8 3000000 552 ns/op ``` Go's `url.Parse`: ``` -BenchmarkUrlParse-8 3000000 574 ns/op -BenchmarkUrlParseWithPath-8 3000000 514 ns/op -BenchmarkUrlParseWithFragment-8 5000000 382 ns/op +BenchmarkUrlParse-8 3000000 475 ns/op +BenchmarkUrlParseWithPath-8 3000000 505 ns/op +BenchmarkUrlParseWithQuery-8 5000000 294 ns/op +BenchmarkUrlParseWithFragment-8 5000000 369 ns/op ``` ## Contributing diff --git a/benchmark_test.go b/benchmark_test.go index 2b322dc..8d44fea 100644 --- a/benchmark_test.go +++ b/benchmark_test.go @@ -25,6 +25,14 @@ func BenchmarkParseWithPath(b *testing.B) { parsed = p } +func BenchmarkParseWithQuery(b *testing.B) { + var p *did.DID + for n := 0; n < b.N; n++ { + p, _ = did.Parse("did:ockam:amzbjdl8etgpgwoe841sfi6fc4q9yh82?6pkmkw5pteabvtzm7p6qe106ysiawmo") + } + parsed = p +} + func BenchmarkParseWithFragment(b *testing.B) { var p *did.DID for n := 0; n < b.N; n++ { @@ -53,6 +61,14 @@ func BenchmarkUrlParseWithPath(b *testing.B) { parsedURL = u } +func BenchmarkUrlParseWithQuery(b *testing.B) { + var u *url.URL + for n := 0; n < b.N; n++ { + u, _ = url.Parse("http://amzbjdl8etgpgwoe841sfi6fc4q9yh82.com?6pkmkw5pteabvtzm7p6qe106ysiawm") + } + parsedURL = u +} + func BenchmarkUrlParseWithFragment(b *testing.B) { var u *url.URL for n := 0; n < b.N; n++ { diff --git a/did.abnf b/did.abnf index f085a07..52567ec 100644 --- a/did.abnf +++ b/did.abnf @@ -5,7 +5,7 @@ ; URI Spec: https://tools.ietf.org/html/rfc3986 ; ABNF Spec: https://tools.ietf.org/html/rfc5234 -did-reference = did [ "/" did-path ] [ "#" did-fragment ] +did-reference = did [ "/" did-path ] [ "?" did-query ] [ "#" did-fragment ] did = "did:" method ":" specific-idstring @@ -23,6 +23,10 @@ did-path = segment-nz *( "/" segment ) ; https://tools.ietf.org/html/rfc3986#section-3.5 did-fragment = *( pchar / "/" / "?" ) +; did-query is identical to a URI query and MUST conform to the ABNF of the query ABNF rule in [RFC3986] +; https://tools.ietf.org/html/rfc3986#section-3.4 +did-query = *( pchar / "/" / "?" ) + segment = *pchar segment-nz = 1*pchar pchar = unreserved / pct-encoded / sub-delims / ":" / "@" diff --git a/did.go b/did.go index 3249bee..b87070d 100644 --- a/did.go +++ b/did.go @@ -29,6 +29,11 @@ type DID struct { // did-path = segment-nz *( "/" segment ) PathSegments []string + // DID Query + // https://github.com/w3c-ccg/did-spec/issues/85 + // did-query = *( pchar / "/" / "?" ) + Query string + // DID Fragment, the portion of a DID reference that follows the first hash sign character ("#") // https://w3c-ccg.github.io/did-spec/#dfn-did-fragment Fragment string @@ -45,10 +50,10 @@ type parser struct { // a step in the parser state machine that returns the next step type parserStep func() parserStep -// IsReference returns true if a DID has a Path or a Fragment +// IsReference returns true if a DID has a Path, a Query or a Fragment // https://w3c-ccg.github.io/did-spec/#dfn-did-reference func (d *DID) IsReference() bool { - return (d.Path != "" || len(d.PathSegments) > 0 || d.Fragment != "") + return (d.Path != "" || len(d.PathSegments) > 0 || d.Query != "" || d.Fragment != "") } // String encodes a DID struct into a valid DID string. @@ -85,12 +90,18 @@ func (d *DID) String() string { // write a leading / and then PathSegments joined with / between them buf.WriteByte('/') // nolint, returned error is always nil buf.WriteString(strings.Join(d.PathSegments[:], "/")) // nolint, returned error is always nil - } else { + } + + if d.Query != "" { + // write a leading ? and then Query + buf.WriteByte('?') // nolint, returned error is always nil + buf.WriteString(d.Query) // nolint, returned error is always nil + } + + if d.Fragment != "" && d.Path == "" && len(d.PathSegments) == 0 { // add fragment only when there is no path - if d.Fragment != "" { - buf.WriteByte('#') // nolint, returned error is always nil - buf.WriteString(d.Fragment) // nolint, returned error is always nil - } + buf.WriteByte('#') // nolint, returned error is always nil + buf.WriteString(d.Fragment) // nolint, returned error is always nil } return buf.String() @@ -246,6 +257,12 @@ func (p *parser) parseID() parserStep { break } + if char == '?' { + // encountered ? input may have a query following specific-idstring, parse that next + next = p.parseQuery + break + } + if char == '#' { // encountered # input may have a fragment following specific-idstring, parse that next next = p.parseFragment @@ -312,6 +329,12 @@ func (p *parser) parsePath() parserStep { break } + if char == '?' { + // encountered ? input may have a query following path, parse that next + next = p.parseQuery + break + } + if char == '%' { // a % must be followed by 2 hex digits if (currentIndex+2 >= inputLength) || @@ -352,6 +375,73 @@ func (p *parser) parsePath() parserStep { return next } +// parseQuery is a parserStep that extracts a DID Query from a DID Reference +// from the grammar: +// did-query = *( pchar / "/" / "?" ) +// pchar = unreserved / pct-encoded / sub-delims / ":" / "@" +// unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" +// pct-encoded = "%" HEXDIG HEXDIG +// sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" +func (p *parser) parseQuery() parserStep { + input := p.input + inputLength := len(input) + currentIndex := p.currentIndex + 1 + startIndex := currentIndex + + var indexIncrement int + var next parserStep + var percentEncoded bool + + for { + if currentIndex == inputLength { + // we've reached the end of input + // it's ok for query to be empty, so we don't need a check for that + // did-query = *( pchar / "/" / "?" ) + break + } + + char := input[currentIndex] + + if char == '#' { + // encountered # input may have a fragment following the query, parse that next + next = p.parseFragment + break + } + + if char == '%' { + // a % must be followed by 2 hex digits + if (currentIndex+2 >= inputLength) || + isNotHexDigit(input[currentIndex+1]) || + isNotHexDigit(input[currentIndex+2]) { + return p.errorf(currentIndex, "%% is not followed by 2 hex digits") + } + // if we got here, we're dealing with percent encoded char, jump three chars + percentEncoded = true + indexIncrement = 3 + } else { + // not pecent encoded + percentEncoded = false + indexIncrement = 1 + } + + // did-query = *( pchar / "/" / "?" ) + // pchar = unreserved / pct-encoded / sub-delims / ":" / "@" + // isNotValidQueryOrFragmentChar checks for all the valid chars except pct-encoded + if !percentEncoded && isNotValidQueryOrFragmentChar(char) { + return p.errorf(currentIndex, "character is not allowed in query - %c", char) + } + + // move to the next char + currentIndex = currentIndex + indexIncrement + } + + // update parser state + p.currentIndex = currentIndex + p.out.Query = input[startIndex:currentIndex] + + return next +} + // parseFragment is a parserStep that extracts a DID Fragment from a DID Reference // from the grammar: // did-fragment = *( pchar / "/" / "?" ) @@ -396,9 +486,9 @@ func (p *parser) parseFragment() parserStep { // did-fragment = *( pchar / "/" / "?" ) // pchar = unreserved / pct-encoded / sub-delims / ":" / "@" - // isNotValidFragmentChar checks for all othe valid chars except pct-encoded - if !percentEncoded && isNotValidFragmentChar(char) { - return p.errorf(currentIndex, "character is not allowed in fragment") + // isNotValidQueryOrFragmentChar checks for all the valid chars except pct-encoded + if !percentEncoded && isNotValidQueryOrFragmentChar(char) { + return p.errorf(currentIndex, "character is not allowed in fragment - %c", char) } // move to the next char @@ -434,12 +524,12 @@ func isNotValidIDChar(char byte) bool { return isNotAlpha(char) && isNotDigit(char) && char != '.' && char != '-' } -// isNotValidFragmentChar returns true if a byte is not allowed in a Fragment +// isNotValidQueryOrFragmentChar returns true if a byte is not allowed in a Fragment // from the grammar: // did-fragment = *( pchar / "/" / "?" ) // pchar = unreserved / pct-encoded / sub-delims / ":" / "@" // pct-encoded is not checked in this function -func isNotValidFragmentChar(char byte) bool { +func isNotValidQueryOrFragmentChar(char byte) bool { return isNotValidPathChar(char) && char != '/' && char != '?' } diff --git a/did_test.go b/did_test.go index a3c654f..21ecbad 100644 --- a/did_test.go +++ b/did_test.go @@ -24,6 +24,11 @@ func TestIsReference(t *testing.T) { assert(t, true, d.IsReference()) }) + t.Run("returns true if Query", func(t *testing.T) { + d := &DID{Method: "example", ID: "123", Query: "abc"} + assert(t, true, d.IsReference()) + }) + t.Run("returns true if Fragment", func(t *testing.T) { d := &DID{Method: "example", ID: "123", Fragment: "00000"} assert(t, true, d.IsReference()) @@ -66,6 +71,26 @@ func TestString(t *testing.T) { assert(t, "did:example:123/a/b", d.String()) }) + t.Run("includes Query after IDString", func(t *testing.T) { + d := &DID{Method: "example", ID: "123", Query: "abc"} + assert(t, "did:example:123?abc", d.String()) + }) + + t.Run("includes Query after Path", func(t *testing.T) { + d := &DID{Method: "example", ID: "123", Path: "x/y", Query: "abc"} + assert(t, "did:example:123/x/y?abc", d.String()) + }) + + t.Run("includes Query after before Fragment", func(t *testing.T) { + d := &DID{Method: "example", ID: "123", Fragment: "zyx", Query: "abc"} + assert(t, "did:example:123?abc#zyx", d.String()) + }) + + t.Run("includes Query", func(t *testing.T) { + d := &DID{Method: "example", ID: "123", Query: "abc"} + assert(t, "did:example:123?abc", d.String()) + }) + t.Run("includes Fragment", func(t *testing.T) { d := &DID{Method: "example", ID: "123", Fragment: "00000"} assert(t, "did:example:123#00000", d.String()) @@ -239,6 +264,56 @@ func TestParse(t *testing.T) { assert(t, nil, err) }) + t.Run("succeeds to extract query after idstring", func(t *testing.T) { + d, err := Parse("did:a:123?abc") + assert(t, nil, err) + assert(t, "a", d.Method) + assert(t, "123", d.ID) + assert(t, "abc", d.Query) + }) + + t.Run("succeeds to extract query after path", func(t *testing.T) { + d, err := Parse("did:a:123/a/b/c?abc") + assert(t, nil, err) + assert(t, "a", d.Method) + assert(t, "123", d.ID) + assert(t, "a/b/c", d.Path) + assert(t, "abc", d.Query) + }) + + t.Run("succeeds to extract fragment after query", func(t *testing.T) { + d, err := Parse("did:a:123?abc#xyz") + assert(t, nil, err) + assert(t, "abc", d.Query) + assert(t, "xyz", d.Fragment) + }) + + t.Run("succeeds with percent encoded chars in query", func(t *testing.T) { + d, err := Parse("did:a:123?ab%20c") + assert(t, nil, err) + assert(t, "ab%20c", d.Query) + }) + + t.Run("returns error if % in query is not followed by 2 hex chars", func(t *testing.T) { + dids := []string{ + "did:a:123:456?%", + "did:a:123:456?%a", + "did:a:123:456?%!*", + "did:a:123:456?%A!", + "did:xyz:pqr?%A!", + "did:a:123:456?%A%", + } + for _, did := range dids { + _, err := Parse(did) + assert(t, false, err == nil, "Input: %s", did) + } + }) + + t.Run("returns error if query has invalid char", func(t *testing.T) { + _, err := Parse("did:a:123:456?ssss^sss") + assert(t, false, err == nil) + }) + t.Run("succeeds to extract fragment", func(t *testing.T) { d, err := Parse("did:a:123:456#keys-1") assert(t, nil, err) @@ -303,7 +378,7 @@ func Test_isNotValidIDChar(t *testing.T) { } } -func Test_isNotValidFragmentChar(t *testing.T) { +func Test_isNotValidQueryOrFragmentChar(t *testing.T) { a := []byte{'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', @@ -313,12 +388,12 @@ func Test_isNotValidFragmentChar(t *testing.T) { ':', '@', '/', '?'} for _, c := range a { - assert(t, false, isNotValidFragmentChar(c), "Input: '%c'", c) + assert(t, false, isNotValidQueryOrFragmentChar(c), "Input: '%c'", c) } a = []byte{'%', '^', '#', ' '} for _, c := range a { - assert(t, true, isNotValidFragmentChar(c), "Input: '%c'", c) + assert(t, true, isNotValidQueryOrFragmentChar(c), "Input: '%c'", c) } } diff --git a/example_test.go b/example_test.go index b041752..783fc0f 100644 --- a/example_test.go +++ b/example_test.go @@ -25,6 +25,15 @@ func ExampleParse_withPath() { // Output: Method - example, ID - q7ckgxeq1lxmra0r, Path - a/b } +func ExampleParse_withQuery() { + d, err := did.Parse("did:example:q7ckgxeq1lxmra0r?dskjsdjj") + if err != nil { + log.Fatal(err) + } + fmt.Printf("Method - %s, ID - %s, Query - %s", d.Method, d.ID, d.Query) + // Output: Method - example, ID - q7ckgxeq1lxmra0r, Query - dskjsdjj +} + func ExampleParse_withFragment() { d, err := did.Parse("did:example:q7ckgxeq1lxmra0r#keys-1") if err != nil { @@ -52,6 +61,12 @@ func ExampleDID_String_withPathSegments() { // Output: did:example:q7ckgxeq1lxmra0r/a/b } +func ExampleDID_String_withQuery() { + d := &did.DID{Method: "example", ID: "q7ckgxeq1lxmra0r", Query: "abc"} + fmt.Println(d.String()) + // Output: did:example:q7ckgxeq1lxmra0r?abc +} + func ExampleDID_String_withFragment() { d := &did.DID{Method: "example", ID: "q7ckgxeq1lxmra0r", Fragment: "keys-1"} fmt.Println(d.String())