From 74d27b195ac4e0876f3b3e970e0c1047826d15ea Mon Sep 17 00:00:00 2001 From: "Pascal S. de Kloe" Date: Tue, 3 Jan 2023 01:26:04 +0100 Subject: [PATCH] chore: new standard compliance with 3rd party library, fixes issue #15 --- did.go | 748 +++------------------------------------------------- did_test.go | 421 +---------------------------- go.mod | 5 + go.sum | 2 + 4 files changed, 42 insertions(+), 1134 deletions(-) create mode 100644 go.mod create mode 100644 go.sum diff --git a/did.go b/did.go index f8bf258..05a0d42 100644 --- a/did.go +++ b/did.go @@ -3,41 +3,11 @@ package did import ( - "fmt" + "errors" "strings" -) - -// Param represents a parsed DID param, -// which contains a name and value. A generic param is defined -// as a param name and value separated by a colon. -// generic-param-name:param-value -// A param may also be method specific, which -// requires the method name to prefix the param name separated by a colon -// method-name:param-name. -// param = param-name [ "=" param-value ] -// https://w3c.github.io/did-core/#generic-did-parameter-names -// https://w3c.github.io/did-core/#method-specific-did-parameter-names -type Param struct { - // param-name = 1*param-char - // Name may include a method name and param name separated by a colon - Name string - // param-value = *param-char - Value string -} - -// String encodes a Param struct into a valid Param string. -// Name is required by the grammar. Value is optional -func (p *Param) String() string { - if p.Name == "" { - return "" - } - - if 0 < len(p.Value) { - return p.Name + "=" + p.Value - } - return p.Name -} + didlib "github.com/pascaldekloe/did" +) // A DID represents a parsed DID or a DID URL type DID struct { @@ -52,11 +22,6 @@ type DID struct { // method-specific-id may be composed of multiple `:` separated idstrings IDStrings []string - // DID URL - // did-url = did *( ";" param ) path-abempty [ "?" query ] [ "#" fragment ] - // did-url may contain multiple params, a path, query, and fragment - Params []Param - // DID Path, the portion of a DID reference that follows the first forward slash character. // https://w3c.github.io/did-core/#path Path string @@ -75,724 +40,79 @@ type DID struct { Fragment string } -// the parsers internal state -type parser struct { - input string // input to the parser - currentIndex int // index in the input which the parser is currently processing - out *DID // the output DID that the parser will assemble as it steps through its state machine - err error // an error in the parser state machine -} - -// a step in the parser state machine that returns the next step -type parserStep func() parserStep - // IsURL 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) IsURL() bool { - return (len(d.Params) > 0 || d.Path != "" || len(d.PathSegments) > 0 || d.Query != "" || d.Fragment != "") + return (d.Path != "" || len(d.PathSegments) > 0 || d.Query != "" || d.Fragment != "") } // String encodes a DID struct into a valid DID string. // nolint: gocyclo func (d *DID) String() string { - var buf strings.Builder - - // write the did: prefix - buf.WriteString("did:") // nolint, returned error is always nil - - if d.Method != "" { - // write method followed by a `:` - buf.WriteString(d.Method) // nolint, returned error is always nil - buf.WriteByte(':') // nolint, returned error is always nil - } else { + if d.Method == "" { // if there is no Method, return an empty string return "" } + var u didlib.URL + u.Method = d.Method + if d.ID != "" { - buf.WriteString(d.ID) // nolint, returned error is always nil + u.SpecID = d.ID } else if len(d.IDStrings) > 0 { - // join IDStrings with a colon to make the ID - buf.WriteString(strings.Join(d.IDStrings[:], ":")) // nolint, returned error is always nil + u.SpecID = strings.Join(d.IDStrings[:], ":") } else { // if there is no ID, return an empty string return "" } - if len(d.Params) > 0 { - // write a leading ; for each param - for _, p := range d.Params { - // get a string that represents the param - param := p.String() - if param != "" { - // params must start with a ; - buf.WriteByte(';') // nolint, returned error is always nil - buf.WriteString(param) // nolint, returned error is always nil - } else { - // if a param exists but is empty, return an empty string - return "" - } - } - } - if d.Path != "" { - // write a leading / and then Path - buf.WriteByte('/') // nolint, returned error is always nil - buf.WriteString(d.Path) // nolint, returned error is always nil + u.RawPath = "/" + d.Path } else if len(d.PathSegments) > 0 { - // 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 + u.SetPathSegments(d.PathSegments...) } 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 + u.RawQuery = "?" + d.Query } if d.Fragment != "" { - // add fragment only when there is no path - buf.WriteByte('#') // nolint, returned error is always nil - buf.WriteString(d.Fragment) // nolint, returned error is always nil + u.RawFragment = "#" + d.Fragment } - return buf.String() + return u.String() } // Parse parses the input string into a DID structure. func Parse(input string) (*DID, error) { - // intialize the parser state - p := &parser{input: input, out: &DID{}} - - // the parser state machine is implemented as a loop over parser steps - // steps increment p.currentIndex as they consume the input, each step returns the next step to run - // the state machine halts when one of the steps returns nil - // - // This design is based on this talk from Rob Pike, although the talk focuses on lexical scanning, - // the DID grammar is simple enough for us to combine lexing and parsing into one lexerless parse - // http://www.youtube.com/watch?v=HxaD_trXwRE - parserState := p.checkLength - for parserState != nil { - parserState = parserState() - } - - // If one of the steps added an err to the parser state, exit. Return nil and the error. - err := p.err + u, err := didlib.ParseURL(input) if err != nil { return nil, err } - - // join IDStrings with : to make up ID - p.out.ID = strings.Join(p.out.IDStrings[:], ":") - - // join PathSegments with / to make up Path - p.out.Path = strings.Join(p.out.PathSegments[:], "/") - - return p.out, nil -} - -// checkLength is a parserStep that checks if the input length is atleast 7 -// the grammar requires -// `did:` prefix (4 chars) -// + atleast one methodchar (1 char) -// + `:` (1 char) -// + atleast one idchar (1 char) -// i.e atleast 7 chars -// The current specification does not take a position on maximum length of a DID. -// https://w3c-ccg.github.io/did-spec/#upper-limits-on-did-character-length -func (p *parser) checkLength() parserStep { - inputLength := len(p.input) - - if inputLength < 7 { - return p.errorf(inputLength, "input length is less than 7") + if u.IsRelative() { + return nil, errors.New("relative URL denied") } - return p.parseScheme -} - -// parseScheme is a parserStep that validates that the input begins with 'did:' -func (p *parser) parseScheme() parserStep { - - currentIndex := 3 // 4 bytes in 'did:', i.e index 3 - - // the grammar requires `did:` prefix - if p.input[:currentIndex+1] != "did:" { - return p.errorf(currentIndex, "input does not begin with 'did:' prefix") + d := DID{ + Method: u.Method, + ID: u.SpecID, + IDStrings: strings.Split(u.SpecID, ":"), + Path: u.RawPath, + PathSegments: u.PathSegments(), + Query: u.RawQuery, + Fragment: u.RawFragment, } - p.currentIndex = currentIndex - return p.parseMethod -} - -// parseMethod is a parserStep that extracts the DID Method -// from the grammar: -// did = "did:" method ":" specific-idstring -// method = 1*methodchar -// methodchar = %x61-7A / DIGIT ; 61-7A is a-z in US-ASCII -func (p *parser) parseMethod() parserStep { - input := p.input - inputLength := len(input) - currentIndex := p.currentIndex + 1 - startIndex := currentIndex - - // parse method name - // loop over every byte following the ':' in 'did:' unlil the second ':' - // method is the string between the two ':'s - for { - if currentIndex == inputLength { - // we got to the end of the input and didn't find a second ':' - return p.errorf(currentIndex, "input does not have a second `:` marking end of method name") - } - - // read the input character at currentIndex - char := input[currentIndex] - - if char == ':' { - // we've found the second : in the input that marks the end of the method - if currentIndex == startIndex { - // return error is method is empty, ex- did::1234 - return p.errorf(currentIndex, "method is empty") - } - break - } - - // as per the grammar method can only be made of digits 0-9 or small letters a-z - if isNotDigit(char) && isNotSmallLetter(char) { - return p.errorf(currentIndex, "character is not a-z OR 0-9") - } - - // move to the next char - currentIndex = currentIndex + 1 - } - - // set parser state - p.currentIndex = currentIndex - p.out.Method = input[startIndex:currentIndex] - - // method is followed by specific-idstring, parse that next - return p.parseID -} - -// parseID is a parserStep that extracts : separated idstrings that are part of a specific-idstring -// and adds them to p.out.IDStrings -// from the grammar: -// specific-idstring = idstring *( ":" idstring ) -// idstring = 1*idchar -// idchar = ALPHA / DIGIT / "." / "-" -// p.out.IDStrings is later concatented by the Parse function before it returns. -func (p *parser) parseID() parserStep { - input := p.input - inputLength := len(input) - currentIndex := p.currentIndex + 1 - startIndex := currentIndex - - var next parserStep - - for { - if currentIndex == inputLength { - // we've reached end of input, no next state - next = nil - break - } - - char := input[currentIndex] - - if char == ':' { - // encountered : input may have another idstring, parse ID again - next = p.parseID - break - } - - if char == ';' { - // encountered ; input may have a parameter, parse that next - next = p.parseParamName - break - } - - if char == '/' { - // encountered / input may have a path following specific-idstring, parse that next - next = p.parsePath - 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 - break - } - - // make sure current char is a valid idchar - // idchar = ALPHA / DIGIT / "." / "-" - if isNotValidIDChar(char) { - return p.errorf(currentIndex, "byte is not ALPHA OR DIGIT OR '.' OR '-'") - } - - // move to the next char - currentIndex = currentIndex + 1 - } - - if currentIndex == startIndex { - // idstring length is zero - // from the grammar: - // idstring = 1*idchar - // return error because idstring is empty, ex- did:a::123:456 - return p.errorf(currentIndex, "idstring must be atleast one char long") - } - - // set parser state - p.currentIndex = currentIndex - p.out.IDStrings = append(p.out.IDStrings, input[startIndex:currentIndex]) - - // return the next parser step - return next -} - -// parseParamName is a parserStep that extracts a did-url param-name. -// A Param struct is created for each param name that is encountered. -// from the grammar: -// param = param-name [ "=" param-value ] -// param-name = 1*param-char -// param-char = ALPHA / DIGIT / "." / "-" / "_" / ":" / pct-encoded -func (p *parser) parseParamName() parserStep { - input := p.input - startIndex := p.currentIndex + 1 - next := p.paramTransition() - currentIndex := p.currentIndex - - if currentIndex == startIndex { - // param-name length is zero - // from the grammar: - // 1*param-char - // return error because param-name is empty, ex- did:a::123:456;param-name - return p.errorf(currentIndex, "Param name must be at least one char long") - } - - // Create a new param with the name - p.out.Params = append(p.out.Params, Param{Name: input[startIndex:currentIndex], Value: ""}) - - // return the next parser step - return next -} - -// parseParamValue is a parserStep that extracts a did-url param-value. -// A parsed Param value requires that a Param was previously created when parsing a param-name. -// from the grammar: -// param = param-name [ "=" param-value ] -// param-value = 1*param-char -// param-char = ALPHA / DIGIT / "." / "-" / "_" / ":" / pct-encoded -func (p *parser) parseParamValue() parserStep { - input := p.input - startIndex := p.currentIndex + 1 - next := p.paramTransition() - currentIndex := p.currentIndex - - // Get the last Param in the DID and append the value - // values may be empty according to the grammar- *param-char - p.out.Params[len(p.out.Params)-1].Value = input[startIndex:currentIndex] - - // return the next parser step - return next -} - -// paramTransition is a parserStep that extracts and transitions a param-name or -// param-value. -// nolint: gocyclo -func (p *parser) paramTransition() parserStep { - input := p.input - inputLength := len(input) - currentIndex := p.currentIndex + 1 - - var indexIncrement int - var next parserStep - var percentEncoded bool - - for { - if currentIndex == inputLength { - // we've reached end of input, no next state - next = nil - break - } - - char := input[currentIndex] - - if char == ';' { - // encountered : input may have another param, parse paramName again - next = p.parseParamName - break - } - - // Separate steps for name and value? - if char == '=' { - // parse param value - next = p.parseParamValue - break - } - - if char == '/' { - // encountered / input may have a path following current param, parse that next - next = p.parsePath - break - } - - if char == '?' { - // encountered ? input may have a query following current param, parse that next - next = p.parseQuery - break - } - - if char == '#' { - // encountered # input may have a fragment following current param, 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 percent encoded - percentEncoded = false - indexIncrement = 1 - } - - // make sure current char is a valid param-char - // idchar = ALPHA / DIGIT / "." / "-" - if !percentEncoded && isNotValidParamChar(char) { - return p.errorf(currentIndex, "character is not allowed in param - %c", char) - } - - // move to the next char - currentIndex = currentIndex + indexIncrement - } - - // set parser state - p.currentIndex = currentIndex - - return next -} - -// parsePath is a parserStep that extracts a DID Path from a DID Reference -// from the grammar: -// did-path = segment-nz *( "/" segment ) -// segment = *pchar -// segment-nz = 1*pchar -// pchar = unreserved / pct-encoded / sub-delims / ":" / "@" -// unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" -// pct-encoded = "%" HEXDIG HEXDIG -// sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" -// nolint: gocyclo -func (p *parser) parsePath() 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 { - next = nil - break - } - - char := input[currentIndex] - - if char == '/' { - // encountered / input may have another path segment, try to parse that next - next = p.parsePath - 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) || - 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 - } - - // pchar = unreserved / pct-encoded / sub-delims / ":" / "@" - if !percentEncoded && isNotValidPathChar(char) { - return p.errorf(currentIndex, "character is not allowed in path") - } - - // move to the next char - currentIndex = currentIndex + indexIncrement - } - - if currentIndex == startIndex && len(p.out.PathSegments) == 0 { - // path segment length is zero - // first path segment must have atleast one character - // from the grammar - // did-path = segment-nz *( "/" segment ) - return p.errorf(currentIndex, "first path segment must have atleast one character") - } - - // update parser state - p.currentIndex = currentIndex - p.out.PathSegments = append(p.out.PathSegments, input[startIndex:currentIndex]) - - 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 + // trim leading characters + if d.Path != "" { + d.Path = d.Path[1:] } - - // 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 / "/" / "?" ) -// pchar = unreserved / pct-encoded / sub-delims / ":" / "@" -// unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" -// pct-encoded = "%" HEXDIG HEXDIG -// sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" -func (p *parser) parseFragment() parserStep { - input := p.input - inputLength := len(input) - currentIndex := p.currentIndex + 1 - startIndex := currentIndex - - var indexIncrement int - var percentEncoded bool - - for { - if currentIndex == inputLength { - // we've reached the end of input - // it's ok for reference to be empty, so we don't need a check for that - // did-fragment = *( pchar / "/" / "?" ) - break - } - - char := input[currentIndex] - - 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-fragment = *( 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 fragment - %c", char) - } - - // move to the next char - currentIndex = currentIndex + indexIncrement + if d.Query != "" { + d.Query = d.Query[1:] } - - // update parser state - p.currentIndex = currentIndex - p.out.Fragment = input[startIndex:currentIndex] - - // no more parsing needed after a fragment, - // cause the state machine to exit by returning nil - return nil -} - -// errorf is a parserStep that returns nil to cause the state machine to exit -// before returning it sets the currentIndex and err field in parser state -// other parser steps use this function to exit the state machine with an error -func (p *parser) errorf(index int, format string, args ...interface{}) parserStep { - p.currentIndex = index - p.err = fmt.Errorf(format, args...) - return nil -} - -// INLINABLE -// Calls to all functions below this point should be inlined by the go compiler -// See output of `go build -gcflags -m` to confirm - -// isNotValidIDChar returns true if a byte is not allowed in a ID -// from the grammar: -// idchar = ALPHA / DIGIT / "." / "-" -func isNotValidIDChar(char byte) bool { - return isNotAlpha(char) && isNotDigit(char) && char != '.' && char != '-' -} - -// isNotValidParamChar returns true if a byte is not allowed in a param-name -// or param-value from the grammar: -// idchar = ALPHA / DIGIT / "." / "-" / "_" / ":" -func isNotValidParamChar(char byte) bool { - return isNotAlpha(char) && isNotDigit(char) && - char != '.' && char != '-' && char != '_' && char != ':' -} - -// 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 isNotValidQueryOrFragmentChar(char byte) bool { - return isNotValidPathChar(char) && char != '/' && char != '?' -} - -// isNotValidPathChar returns true if a byte is not allowed in Path -// did-path = segment-nz *( "/" segment ) -// segment = *pchar -// segment-nz = 1*pchar -// pchar = unreserved / pct-encoded / sub-delims / ":" / "@" -// pct-encoded is not checked in this function -func isNotValidPathChar(char byte) bool { - return isNotUnreservedOrSubdelim(char) && char != ':' && char != '@' -} - -// isNotUnreservedOrSubdelim returns true if a byte is not unreserved or sub-delims -// from the grammar: -// unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" -// sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" -// https://tools.ietf.org/html/rfc3986#appendix-A -func isNotUnreservedOrSubdelim(char byte) bool { - switch char { - case '-', '.', '_', '~', '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=': - return false - default: - if isNotAlpha(char) && isNotDigit(char) { - return true - } - return false + if d.Fragment != "" { + d.Fragment = d.Fragment[1:] } -} - -// isNotHexDigit returns true if a byte is not a digit between 0-9 or A-F or a-f -// in US-ASCII http://www.columbia.edu/kermit/ascii.html -// https://tools.ietf.org/html/rfc5234#appendix-B.1 -func isNotHexDigit(char byte) bool { - // '\x41' is A, '\x46' is F - // '\x61' is a, '\x66' is f - return isNotDigit(char) && (char < '\x41' || char > '\x46') && (char < '\x61' || char > '\x66') -} - -// isNotDigit returns true if a byte is not a digit between 0-9 -// in US-ASCII http://www.columbia.edu/kermit/ascii.html -// https://tools.ietf.org/html/rfc5234#appendix-B.1 -func isNotDigit(char byte) bool { - // '\x30' is digit 0, '\x39' is digit 9 - return (char < '\x30' || char > '\x39') -} - -// isNotAlpha returns true if a byte is not a big letter between A-Z or small letter between a-z -// https://tools.ietf.org/html/rfc5234#appendix-B.1 -func isNotAlpha(char byte) bool { - return isNotSmallLetter(char) && isNotBigLetter(char) -} - -// isNotBigLetter returns true if a byte is not a big letter between A-Z -// in US-ASCII http://www.columbia.edu/kermit/ascii.html -// https://tools.ietf.org/html/rfc5234#appendix-B.1 -func isNotBigLetter(char byte) bool { - // '\x41' is big letter A, '\x5A' small letter Z - return (char < '\x41' || char > '\x5A') -} -// isNotSmallLetter returns true if a byte is not a small letter between a-z -// in US-ASCII http://www.columbia.edu/kermit/ascii.html -// https://tools.ietf.org/html/rfc5234#appendix-B.1 -func isNotSmallLetter(char byte) bool { - // '\x61' is small letter a, '\x7A' small letter z - return (char < '\x61' || char > '\x7A') + return &d, nil } diff --git a/did_test.go b/did_test.go index 1b286d8..ffda458 100644 --- a/did_test.go +++ b/did_test.go @@ -14,11 +14,6 @@ func TestIsURL(t *testing.T) { assert(t, false, d.IsURL()) }) - t.Run("returns true if Params", func(t *testing.T) { - d := &DID{Method: "example", ID: "123", Params: []Param{{Name: "foo", Value: "bar"}}} - assert(t, true, d.IsURL()) - }) - t.Run("returns true if Path", func(t *testing.T) { d := &DID{Method: "example", ID: "123", Path: "a/b"} assert(t, true, d.IsURL()) @@ -53,7 +48,7 @@ func TestString(t *testing.T) { t.Run("assembles a DID from IDStrings", func(t *testing.T) { d := &DID{Method: "example", IDStrings: []string{"123", "456"}} - assert(t, "did:example:123:456", d.String()) + assert(t, "did:example:123%3A456", d.String()) }) t.Run("returns empty string if no method", func(t *testing.T) { @@ -66,37 +61,6 @@ func TestString(t *testing.T) { assert(t, "", d.String()) }) - t.Run("returns empty string if Param Name does not exist", func(t *testing.T) { - d := &DID{Method: "example", ID: "123", Params: []Param{{Name: "", Value: "agent"}}} - assert(t, "", d.String()) - }) - - t.Run("returns name string if Param Value does not exist", func(t *testing.T) { - d := &DID{Method: "example", ID: "123", Params: []Param{{Name: "service", Value: ""}}} - assert(t, "did:example:123;service", d.String()) - }) - - t.Run("returns param string with name and value", func(t *testing.T) { - d := &DID{Method: "example", ID: "123", Params: []Param{{Name: "service", Value: "agent"}}} - assert(t, "did:example:123;service=agent", d.String()) - }) - - t.Run("includes Param generic", func(t *testing.T) { - d := &DID{Method: "example", ID: "123", Params: []Param{{Name: "service", Value: "agent"}}} - assert(t, "did:example:123;service=agent", d.String()) - }) - - t.Run("includes Param method", func(t *testing.T) { - d := &DID{Method: "example", ID: "123", Params: []Param{{Name: "foo:bar", Value: "high"}}} - assert(t, "did:example:123;foo:bar=high", d.String()) - }) - - t.Run("includes Param generic and method", func(t *testing.T) { - d := &DID{Method: "example", ID: "123", - Params: []Param{{Name: "service", Value: "agent"}, {Name: "foo:bar", Value: "high"}}} - assert(t, "did:example:123;service=agent;foo:bar=high", d.String()) - }) - t.Run("includes Path", func(t *testing.T) { d := &DID{Method: "example", ID: "123", Path: "a/b"} assert(t, "did:example:123/a/b", d.String()) @@ -107,34 +71,16 @@ func TestString(t *testing.T) { assert(t, "did:example:123/a/b", d.String()) }) - t.Run("includes Path after Param", func(t *testing.T) { - d := &DID{Method: "example", ID: "123", - Params: []Param{{Name: "service", Value: "agent"}}, Path: "a/b"} - assert(t, "did:example:123;service=agent/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("include Query after Param", func(t *testing.T) { - d := &DID{Method: "example", ID: "123", Query: "abc", - Params: []Param{{Name: "service", Value: "agent"}}} - assert(t, "did:example:123;service=agent?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 Param and Path", func(t *testing.T) { - d := &DID{Method: "example", ID: "123", Path: "x/y", Query: "abc", - Params: []Param{{Name: "service", Value: "agent"}}} - assert(t, "did:example:123;service=agent/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()) @@ -184,20 +130,6 @@ func TestParse(t *testing.T) { assert(t, false, err == nil) }) - t.Run("returns error if idstring is empty", func(t *testing.T) { - dids := []string{ - "did:a::123:456", - "did:a:123::456", - "did:a:123:456:", - "did:a:123:/abc", - "did:a:123:#abc", - } - for _, did := range dids { - _, err := Parse(did) - assert(t, false, err == nil, "Input: %s", did) - } - }) - t.Run("returns error if input does not begin with did: scheme", func(t *testing.T) { _, err := Parse("a:12345") assert(t, false, err == nil) @@ -252,175 +184,6 @@ func TestParse(t *testing.T) { assert(t, false, err == nil) }) - t.Run("returns error if param name is empty", func(t *testing.T) { - _, err := Parse("did:a:123:456;") - assert(t, false, err == nil) - }) - - t.Run("returns error if Param name has an invalid char", func(t *testing.T) { - _, err := Parse("did:a:123:456;serv&ce") - assert(t, false, err == nil) - }) - - t.Run("returns error if Param value has an invalid char", func(t *testing.T) { - _, err := Parse("did:a:123:456;service=ag&nt") - assert(t, false, err == nil) - }) - - t.Run("returns error if Param name has an invalid percent encoded", func(t *testing.T) { - _, err := Parse("did:a:123:456;ser%2ge") - assert(t, false, err == nil) - }) - - t.Run("returns error if Param does not exist for value", func(t *testing.T) { - _, err := Parse("did:a:123:456;=value") - assert(t, false, err == nil) - }) - - // nolint: dupl - // test for params look similar to linter - t.Run("succeeds to extract generic param with name and value", func(t *testing.T) { - d, err := Parse("did:a:123:456;service==agent") - assert(t, nil, err) - assert(t, 1, len(d.Params)) - assert(t, "service=agent", d.Params[0].String()) - assert(t, "service", d.Params[0].Name) - assert(t, "agent", d.Params[0].Value) - }) - - // nolint: dupl - // test for params look similar to linter - t.Run("succeeds to extract generic param with name only", func(t *testing.T) { - d, err := Parse("did:a:123:456;service") - assert(t, nil, err) - assert(t, 1, len(d.Params)) - assert(t, "service", d.Params[0].String()) - assert(t, "service", d.Params[0].Name) - assert(t, "", d.Params[0].Value) - }) - - // nolint: dupl - // test for params look similar to linter - t.Run("succeeds to extract generic param with name only and empty param", func(t *testing.T) { - d, err := Parse("did:a:123:456;service=") - assert(t, nil, err) - assert(t, 1, len(d.Params)) - assert(t, "service", d.Params[0].String()) - assert(t, "service", d.Params[0].Name) - assert(t, "", d.Params[0].Value) - }) - - // nolint: dupl - // test for params look similar to linter - t.Run("succeeds to extract method param with name and value", func(t *testing.T) { - d, err := Parse("did:a:123:456;foo:bar=baz") - assert(t, nil, err) - assert(t, 1, len(d.Params)) - assert(t, "foo:bar=baz", d.Params[0].String()) - assert(t, "foo:bar", d.Params[0].Name) - assert(t, "baz", d.Params[0].Value) - }) - - // nolint: dupl - // test for params look similar to linter - t.Run("succeeds to extract method param with name only", func(t *testing.T) { - d, err := Parse("did:a:123:456;foo:bar") - assert(t, nil, err) - assert(t, 1, len(d.Params)) - assert(t, "foo:bar", d.Params[0].String()) - assert(t, "foo:bar", d.Params[0].Name) - assert(t, "", d.Params[0].Value) - }) - - // nolint: dupl - // test for params look similar to linter - t.Run("succeeds with percent encoded chars in param name and value", func(t *testing.T) { - d, err := Parse("did:a:123:456;serv%20ice=val%20ue") - assert(t, nil, err) - assert(t, 1, len(d.Params)) - assert(t, "serv%20ice=val%20ue", d.Params[0].String()) - assert(t, "serv%20ice", d.Params[0].Name) - assert(t, "val%20ue", d.Params[0].Value) - }) - - // nolint: dupl - // test for params look similar to linter - t.Run("succeeds to extract multiple generic params with name only", func(t *testing.T) { - d, err := Parse("did:a:123:456;foo;bar") - assert(t, nil, err) - assert(t, 2, len(d.Params)) - assert(t, "foo", d.Params[0].Name) - assert(t, "", d.Params[0].Value) - assert(t, "bar", d.Params[1].Name) - assert(t, "", d.Params[1].Value) - }) - - // nolint: dupl - // test for params look similar to linter - t.Run("succeeds to extract multiple params with names and values", func(t *testing.T) { - d, err := Parse("did:a:123:456;service=agent;foo:bar=baz") - assert(t, nil, err) - assert(t, 2, len(d.Params)) - assert(t, "service", d.Params[0].Name) - assert(t, "agent", d.Params[0].Value) - assert(t, "foo:bar", d.Params[1].Name) - assert(t, "baz", d.Params[1].Value) - }) - - // nolint: dupl - // test for params look similar to linter - t.Run("succeeds to extract path after generic param", func(t *testing.T) { - d, err := Parse("did:a:123:456;service==value/a/b") - assert(t, nil, err) - assert(t, 1, len(d.Params)) - assert(t, "service=value", d.Params[0].String()) - assert(t, "service", d.Params[0].Name) - assert(t, "value", d.Params[0].Value) - - segments := d.PathSegments - assert(t, "a", segments[0]) - assert(t, "b", segments[1]) - }) - - // nolint: dupl - // test for params look similar to linter - t.Run("succeeds to extract path after generic param name and no value", func(t *testing.T) { - d, err := Parse("did:a:123:456;service=/a/b") - assert(t, nil, err) - assert(t, 1, len(d.Params)) - assert(t, "service", d.Params[0].String()) - assert(t, "service", d.Params[0].Name) - assert(t, "", d.Params[0].Value) - - segments := d.PathSegments - assert(t, "a", segments[0]) - assert(t, "b", segments[1]) - }) - - // nolint: dupl - // test for params look similar to linter - t.Run("succeeds to extract query after generic param", func(t *testing.T) { - d, err := Parse("did:a:123:456;service=value?abc") - assert(t, nil, err) - assert(t, 1, len(d.Params)) - assert(t, "service=value", d.Params[0].String()) - assert(t, "service", d.Params[0].Name) - assert(t, "value", d.Params[0].Value) - assert(t, "abc", d.Query) - }) - - // nolint: dupl - // test for params look similar to linter - t.Run("succeeds to extract fragment after generic param", func(t *testing.T) { - d, err := Parse("did:a:123:456;service=value#xyz") - assert(t, nil, err) - assert(t, 1, len(d.Params)) - assert(t, "service=value", d.Params[0].String()) - assert(t, "service", d.Params[0].Name) - assert(t, "value", d.Params[0].Value) - assert(t, "xyz", d.Fragment) - }) - t.Run("succeeds to extract path", func(t *testing.T) { d, err := Parse("did:a:123:456/someService") assert(t, nil, err) @@ -457,16 +220,6 @@ func TestParse(t *testing.T) { } }) - t.Run("returns error if path is empty but there is a slash", func(t *testing.T) { - _, err := Parse("did:a:123:456/") - assert(t, false, err == nil) - }) - - t.Run("returns error if first path segment is empty", func(t *testing.T) { - _, err := Parse("did:a:123:456//abc") - assert(t, false, err == nil) - }) - t.Run("does not fail if second path segment is empty", func(t *testing.T) { _, err := Parse("did:a:123:456/abc//pqr") assert(t, nil, err) @@ -565,178 +318,6 @@ func TestParse(t *testing.T) { }) } -func Test_errorf(t *testing.T) { - p := &parser{} - p.errorf(10, "%s,%s", "a", "b") - - if p.currentIndex != 10 { - t.Errorf("did not set currentIndex") - } - - e := p.err.Error() - if e != "a,b" { - t.Errorf("err message is: '%s' expected: 'a,b'", e) - } -} - -func Test_isNotValidParamChar(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', - 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', - '.', '-', '_', ':'} - for _, c := range a { - assert(t, false, isNotValidParamChar(c), "Input: '%c'", c) - } - - a = []byte{'%', '^', '#', ' ', '~', '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=', '@', '/', '?'} - for _, c := range a { - assert(t, true, isNotValidParamChar(c), "Input: '%c'", c) - } -} - -func Test_isNotValidIDChar(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', - 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', - '.', '-'} - for _, c := range a { - assert(t, false, isNotValidIDChar(c), "Input: '%c'", c) - } - - a = []byte{'%', '^', '#', ' ', '_', '~', '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=', ':', '@', '/', '?'} - for _, c := range a { - assert(t, true, isNotValidIDChar(c), "Input: '%c'", c) - } -} - -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', - 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', - '-', '.', '_', '~', '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=', - ':', '@', - '/', '?'} - for _, c := range a { - assert(t, false, isNotValidQueryOrFragmentChar(c), "Input: '%c'", c) - } - - a = []byte{'%', '^', '#', ' '} - for _, c := range a { - assert(t, true, isNotValidQueryOrFragmentChar(c), "Input: '%c'", c) - } -} - -func Test_isNotValidPathChar(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', - 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', - '-', '.', '_', '~', '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=', - ':', '@'} - for _, c := range a { - assert(t, false, isNotValidPathChar(c), "Input: '%c'", c) - } - - a = []byte{'%', '/', '?'} - for _, c := range a { - assert(t, true, isNotValidPathChar(c), "Input: '%c'", c) - } -} - -func Test_isNotUnreservedOrSubdelim(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', - 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', - '-', '.', '_', '~', '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '='} - for _, c := range a { - assert(t, false, isNotUnreservedOrSubdelim(c), "Input: '%c'", c) - } - - a = []byte{'%', ':', '@', '/', '?'} - for _, c := range a { - assert(t, true, isNotUnreservedOrSubdelim(c), "Input: '%c'", c) - } -} - -func Test_isNotHexDigit(t *testing.T) { - a := []byte{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', - 'A', 'B', 'C', 'D', 'E', 'F', 'a', 'b', 'c', 'd', 'e', 'f'} - for _, c := range a { - assert(t, false, isNotHexDigit(c), "Input: '%c'", c) - } - - a = []byte{'G', 'g', '%', '\x40', '\x47', '\x60', '\x67'} - for _, c := range a { - assert(t, true, isNotHexDigit(c), "Input: '%c'", c) - } -} - -func Test_isNotDigit(t *testing.T) { - a := []byte{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'} - for _, c := range a { - assert(t, false, isNotDigit(c), "Input: '%c'", c) - } - - a = []byte{'A', 'a', '\x29', '\x40', '/'} - for _, c := range a { - assert(t, true, isNotDigit(c), "Input: '%c'", c) - } -} - -func Test_isNotAlpha(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', - 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'} - for _, c := range a { - assert(t, false, isNotAlpha(c), "Input: '%c'", c) - } - - a = []byte{'\x40', '\x5B', '\x60', '\x7B', '0', '9', '-', '%'} - for _, c := range a { - assert(t, true, isNotAlpha(c), "Input: '%c'", c) - } -} - -// nolint: dupl -// Test_isNotSmallLetter and Test_isNotBigLetter look too similar to the dupl linter, ignore it -func Test_isNotBigLetter(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'} - for _, c := range a { - assert(t, false, isNotBigLetter(c), "Input: '%c'", c) - } - - a = []byte{'\x40', '\x5B', 'a', 'z', '1', '9', '-', '%'} - for _, c := range a { - assert(t, true, isNotBigLetter(c), "Input: '%c'", c) - } -} - -// nolint: dupl -// Test_isNotSmallLetter and Test_isNotBigLetter look too similar to the dupl linter, ignore it -func Test_isNotSmallLetter(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'} - for _, c := range a { - assert(t, false, isNotSmallLetter(c), "Input: '%c'", c) - } - - a = []byte{'\x60', '\x7B', 'A', 'Z', '1', '9', '-', '%'} - for _, c := range a { - assert(t, true, isNotSmallLetter(c), "Input: '%c'", c) - } -} - func assert(t *testing.T, expected interface{}, actual interface{}, args ...interface{}) { if !reflect.DeepEqual(expected, actual) { argsLength := len(args) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b601c8d --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/ockam-network/did + +go 1.19 + +require github.com/pascaldekloe/did v1.0.1 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..5587e62 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/pascaldekloe/did v1.0.1 h1:1ryMNmciSDK3Gl1d9mcw1aom3dPpJ8Z4TG8pe3bTLOg= +github.com/pascaldekloe/did v1.0.1/go.mod h1:mxp/bKYGTNZz/PIBq1WfK0QYq9dfY4CpxD0EBJTy9fE=