diff --git a/claim_test.go b/claim_test.go index ed737af..a01c75f 100644 --- a/claim_test.go +++ b/claim_test.go @@ -14,7 +14,6 @@ import ( "github.com/iden3/go-iden3-crypto/poseidon" "github.com/iden3/go-iden3-crypto/utils" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -389,8 +388,7 @@ func TestNewSchemaHashFromHex(t *testing.T) { exp, err := hex.DecodeString(hash) require.NoError(t, err) - assert.Equal(t, exp[:], got[:]) - + require.Equal(t, exp[:], got[:]) } func TestSchemaHash_BigInt(t *testing.T) { @@ -402,8 +400,7 @@ func TestSchemaHash_BigInt(t *testing.T) { got := schema.BigInt() - assert.Equal(t, exp, got) - + require.Equal(t, exp, got) } func TestGetIDPosition(t *testing.T) { diff --git a/did.go b/did.go index 4aa2ac1..4abece0 100644 --- a/did.go +++ b/did.go @@ -1,33 +1,41 @@ package core import ( - "encoding/json" + "bytes" + "crypto/sha256" "errors" "fmt" "math/big" "strings" + + "github.com/iden3/go-iden3-core/v2/w3c" ) var ( - // ErrInvalidDID invalid did format. - ErrInvalidDID = errors.New("invalid did format") + // ErrUnsupportedID ID with unsupported type. + ErrUnsupportedID = errors.New("unsupported ID") + // ErrIncorrectDID return if DID method is known, but format of DID is incorrect. + ErrIncorrectDID = errors.New("incorrect DID") + // ErrMethodUnknown return if DID method is unknown. + ErrMethodUnknown = errors.New("unknown DID method") // ErrDIDMethodNotSupported unsupported did method. - ErrDIDMethodNotSupported = errors.New("did method is not supported") + ErrDIDMethodNotSupported = errors.New("not supported did method") + // ErrBlockchainNotSupportedForDID unsupported network for did. + ErrBlockchainNotSupportedForDID = errors.New("not supported blockchain") // ErrNetworkNotSupportedForDID unsupported network for did. - ErrNetworkNotSupportedForDID = errors.New("network in not supported for did") + ErrNetworkNotSupportedForDID = errors.New("not supported network") ) -// DIDSchema DID Schema -const DIDSchema = "did" - // DIDMethod represents did methods type DIDMethod string const ( - // DIDMethodIden3 DID method-name + // DIDMethodIden3 DIDMethodIden3 DIDMethod = "iden3" - // DIDMethodPolygonID DID method-name + // DIDMethodPolygonID DIDMethodPolygonID DIDMethod = "polygonid" + // DIDMethodOther any other method not listed before + DIDMethodOther DIDMethod = "" ) // Blockchain id of the network "eth", "polygon", etc. @@ -38,9 +46,13 @@ const ( Ethereum Blockchain = "eth" // Polygon is polygon blockchain network Polygon Blockchain = "polygon" + // ZkEVM is zkEVM blockchain network + ZkEVM Blockchain = "zkevm" // UnknownChain is used when it's not possible to retrieve blockchain type from identifier UnknownChain Blockchain = "unknown" - // NoChain should be used for readonly identity to build readonly flag + // ReadOnly should be used for readonly identity to build readonly flag + ReadOnly Blockchain = "readonly" + // NoChain can be used for identity to build readonly flag NoChain Blockchain = "" ) @@ -48,16 +60,22 @@ const ( type NetworkID string const ( - // Main is ethereum main network + // Main is main network Main NetworkID = "main" + // Mumbai is polygon mumbai test network Mumbai NetworkID = "mumbai" // Goerli is ethereum goerli test network Goerli NetworkID = "goerli" // goerli + // Sepolia is ethereum Sepolia test network + Sepolia NetworkID = "sepolia" + + // Test is test network + Test NetworkID = "test" + // UnknownNetwork is used when it's not possible to retrieve network from identifier UnknownNetwork NetworkID = "unknown" - // NoNetwork should be used for readonly identity to build readonly flag NoNetwork NetworkID = "" ) @@ -66,6 +84,7 @@ const ( var DIDMethodByte = map[DIDMethod]byte{ DIDMethodIden3: 0b00000001, DIDMethodPolygonID: 0b00000010, + DIDMethodOther: 0b11111111, } // DIDNetworkFlag is a structure to represent DID blockchain and network id @@ -77,34 +96,51 @@ type DIDNetworkFlag struct { // DIDMethodNetwork is map for did methods and their blockchain networks var DIDMethodNetwork = map[DIDMethod]map[DIDNetworkFlag]byte{ DIDMethodIden3: { - {Blockchain: NoChain, NetworkID: NoNetwork}: 0b00000000, + {Blockchain: ReadOnly, NetworkID: NoNetwork}: 0b00000000, {Blockchain: Polygon, NetworkID: Main}: 0b00010000 | 0b00000001, {Blockchain: Polygon, NetworkID: Mumbai}: 0b00010000 | 0b00000010, - {Blockchain: Ethereum, NetworkID: Main}: 0b00100000 | 0b00000001, - {Blockchain: Ethereum, NetworkID: Goerli}: 0b00100000 | 0b00000010, + {Blockchain: Ethereum, NetworkID: Main}: 0b00100000 | 0b00000001, + {Blockchain: Ethereum, NetworkID: Goerli}: 0b00100000 | 0b00000010, + {Blockchain: Ethereum, NetworkID: Sepolia}: 0b00100000 | 0b00000011, + + {Blockchain: ZkEVM, NetworkID: Main}: 0b00110000 | 0b00000001, + {Blockchain: ZkEVM, NetworkID: Test}: 0b00110000 | 0b00000010, }, DIDMethodPolygonID: { - {Blockchain: NoChain, NetworkID: NoNetwork}: 0b00000000, + {Blockchain: ReadOnly, NetworkID: NoNetwork}: 0b00000000, {Blockchain: Polygon, NetworkID: Main}: 0b00010000 | 0b00000001, {Blockchain: Polygon, NetworkID: Mumbai}: 0b00010000 | 0b00000010, + + {Blockchain: Ethereum, NetworkID: Main}: 0b00100000 | 0b00000001, + {Blockchain: Ethereum, NetworkID: Goerli}: 0b00100000 | 0b00000010, + {Blockchain: Ethereum, NetworkID: Sepolia}: 0b00100000 | 0b00000011, + + {Blockchain: ZkEVM, NetworkID: Main}: 0b00110000 | 0b00000001, + {Blockchain: ZkEVM, NetworkID: Test}: 0b00110000 | 0b00000010, + }, + DIDMethodOther: { + {Blockchain: UnknownChain, NetworkID: UnknownNetwork}: 0b11111111, }, } // BuildDIDType builds bytes type from chain and network -func BuildDIDType(method DIDMethod, blockchain Blockchain, network NetworkID) ([2]byte, error) { +func BuildDIDType(method DIDMethod, blockchain Blockchain, + network NetworkID) ([2]byte, error) { fb, ok := DIDMethodByte[method] if !ok { return [2]byte{}, ErrDIDMethodNotSupported } - sb, ok := DIDMethodNetwork[method][DIDNetworkFlag{Blockchain: blockchain, NetworkID: network}] + netFlag := DIDNetworkFlag{Blockchain: blockchain, NetworkID: network} + sb, ok := DIDMethodNetwork[method][netFlag] if !ok { return [2]byte{}, ErrNetworkNotSupportedForDID } + return [2]byte{fb, sb}, nil } @@ -133,161 +169,216 @@ func FindBlockchainForDIDMethodByValue(method DIDMethod, _v byte) (Blockchain, e return k.Blockchain, nil } } - return UnknownChain, ErrNetworkNotSupportedForDID + return UnknownChain, ErrBlockchainNotSupportedForDID } // FindDIDMethodByValue finds did method by its byte value -func FindDIDMethodByValue(_v byte) (DIDMethod, error) { +func FindDIDMethodByValue(b byte) (DIDMethod, error) { for k, v := range DIDMethodByte { - if v == _v { + if v == b { return k, nil } } - return "", ErrDIDMethodNotSupported + return DIDMethodOther, ErrDIDMethodNotSupported } -// DID Decentralized Identifiers (DIDs) -// https://w3c.github.io/did-core/#did-syntax -type DID struct { - ID ID // ID did specific id - Method DIDMethod // DIDMethod did method - Blockchain Blockchain // Blockchain network identifier eth / polygon,... - NetworkID NetworkID // NetworkID specific network identifier eth {main, ropsten, rinkeby, kovan} +// NewDIDFromIdenState calculates the genesis ID from an Identity State and +// returns it as a DID +func NewDIDFromIdenState(typ [2]byte, state *big.Int) (*w3c.DID, error) { + id, err := NewIDFromIdenState(typ, state) + if err != nil { + return nil, err + } + return ParseDIDFromID(*id) +} + +// NewDID creates a new *w3c.DID from the type and the genesis +func NewDID(typ [2]byte, genesis [genesisLn]byte) (*w3c.DID, error) { + return ParseDIDFromID(NewID(typ, genesis)) } -func (did *DID) SetString(didStr string) error { - arg := strings.Split(didStr, ":") - if len(arg) <= 1 { - return ErrInvalidDID +func IDFromDID(did w3c.DID) (ID, error) { + id, err := idFromDID(did) + if errors.Is(err, ErrMethodUnknown) { + return newIDFromUnsupportedDID(did), nil } + return id, err +} - did.Method = DIDMethod(arg[1]) +func newIDFromUnsupportedDID(did w3c.DID) ID { + hash := sha256.Sum256([]byte(did.String())) + var genesis [genesisLn]byte + copy(genesis[:], hash[len(hash)-genesisLn:]) + flg := DIDNetworkFlag{Blockchain: UnknownChain, NetworkID: UnknownNetwork} + var tp = [2]byte{ + DIDMethodByte[DIDMethodOther], + DIDMethodNetwork[DIDMethodOther][flg], + } + return NewID(tp, genesis) +} - switch len(arg) { - case 5: - var err error - // validate id - did.ID, err = IDFromString(arg[4]) - if err != nil { - return fmt.Errorf("%w: %v", ErrInvalidDID, err) - } +func idFromDID(did w3c.DID) (ID, error) { + method := DIDMethod(did.Method) + _, ok := DIDMethodByte[method] + if !ok || method == DIDMethodOther { + return ID{}, ErrMethodUnknown + } - did.Blockchain = Blockchain(arg[2]) - did.NetworkID = NetworkID(arg[3]) + var id ID - case 3: - var err error - // validate readonly id - did.ID, err = IDFromString(arg[2]) - if err != nil { - return fmt.Errorf("%w: %v", ErrInvalidDID, err) - } + if len(did.IDStrings) > 3 || len(did.IDStrings) < 2 { + return id, fmt.Errorf("%w: unexpected number of ID strings", + ErrIncorrectDID) } - // check did method defined in core lib - _, ok := DIDMethodByte[did.Method] - if !ok { - return ErrDIDMethodNotSupported + var err error + id, err = IDFromString(did.IDStrings[len(did.IDStrings)-1]) + if err != nil { + return id, fmt.Errorf("%w: can't parse ID string", ErrIncorrectDID) } - // check did network defined in core lib for did method - _, ok = DIDMethodNetwork[did.Method][DIDNetworkFlag{ - Blockchain: did.Blockchain, - NetworkID: did.NetworkID}] - if !ok { - return ErrNetworkNotSupportedForDID + if !CheckChecksum(id) { + return id, fmt.Errorf("%w: incorrect ID checksum", ErrIncorrectDID) } - // check id contains did network and method - return did.validate() -} - -// Return nil on success or error if fields are inconsistent. -func (did *DID) validate() error { - d, err := ParseDIDFromID(did.ID) + method2, blockchain, networkID, err := decodeDIDPartsFromID(id) if err != nil { - return err + return id, err } - if d.Method != did.Method { - return fmt.Errorf( - "%w: did method of core identity %s differs from given did method %s", - ErrInvalidDID, d.Method, did.Method) + if method2 != method { + return id, fmt.Errorf("%w: methods in ID and DID are different", + ErrIncorrectDID) } - if d.NetworkID != did.NetworkID { - return fmt.Errorf( - "%w: network method of core identity %s differs from given did network specific id %s", - ErrInvalidDID, d.NetworkID, did.NetworkID) + if string(blockchain) != did.IDStrings[0] { + return id, fmt.Errorf("%w: blockchains in ID and DID are different", + ErrIncorrectDID) } - if d.Blockchain != did.Blockchain { - return fmt.Errorf( - "%w: blockchain network of core identity %s differs from given did blockhain network %s", - ErrInvalidDID, d.Blockchain, did.Blockchain) + if len(did.IDStrings) > 2 && string(networkID) != did.IDStrings[1] { + return id, fmt.Errorf("%w: networkIDs in ID and DID are different", + ErrIncorrectDID) } - return nil + return id, nil } -func (did *DID) UnmarshalJSON(bytes []byte) error { - var didStr string - err := json.Unmarshal(bytes, &didStr) +// ParseDIDFromID returns DID from ID +func ParseDIDFromID(id ID) (*w3c.DID, error) { + + if !CheckChecksum(id) { + return nil, fmt.Errorf("%w: invalid checksum", ErrUnsupportedID) + } + + method, blockchain, networkID, err := decodeDIDPartsFromID(id) if err != nil { - return err + return nil, err } - return did.SetString(didStr) -} + if isUnsupportedDID(method, blockchain, networkID) { + return nil, fmt.Errorf("%w: unsupported DID", + ErrMethodUnknown) + } -func (did *DID) MarshalJSON() ([]byte, error) { - return json.Marshal(did.String()) -} + didParts := []string{"did", string(method), string(blockchain)} + if string(networkID) != "" { + didParts = append(didParts, string(networkID)) + } -// DIDGenesisFromIdenState calculates the genesis ID from an Identity State and returns it as DID -func DIDGenesisFromIdenState(typ [2]byte, state *big.Int) (*DID, error) { - id, err := IdGenesisFromIdenState(typ, state) + didParts = append(didParts, id.String()) + + didString := strings.Join(didParts, ":") + + did, err := w3c.ParseDID(didString) if err != nil { return nil, err } - return ParseDIDFromID(*id) + return did, nil } -// String did as a string -func (did *DID) String() string { - if did.Blockchain == "" { - return fmt.Sprintf("%s:%s:%s", DIDSchema, did.Method, did.ID.String()) +func decodeDIDPartsFromID(id ID) (DIDMethod, Blockchain, NetworkID, error) { + method, err := FindDIDMethodByValue(id[0]) + if err != nil { + return DIDMethodOther, UnknownChain, UnknownNetwork, err } - return fmt.Sprintf("%s:%s:%s:%s:%s", DIDSchema, did.Method, did.Blockchain, - did.NetworkID, did.ID.String()) -} + blockchain, err := FindBlockchainForDIDMethodByValue(method, id[1]) + if err != nil { + return DIDMethodOther, UnknownChain, UnknownNetwork, err + } -// ParseDID method parse string and extract DID if string is valid Iden3 identifier -func ParseDID(didStr string) (*DID, error) { - var did DID - err := did.SetString(didStr) - return &did, err -} + networkID, err := FindNetworkIDForDIDMethodByValue(method, id[1]) + if err != nil { + return DIDMethodOther, UnknownChain, UnknownNetwork, err + } -// ParseDIDFromID returns did from ID -func ParseDIDFromID(id ID) (*DID, error) { - var err error - did := DID{} - did.ID = id - typ := id.Type() + return method, blockchain, networkID, nil +} - did.Method, err = FindDIDMethodByValue(typ[0]) +func MethodFromID(id ID) (DIDMethod, error) { + method, blockchain, netID, err := decodeDIDPartsFromID(id) if err != nil { - return nil, err + return DIDMethodOther, err + } + + if isUnsupportedDID(method, blockchain, netID) { + return DIDMethodOther, fmt.Errorf("%w: unsupported DID", + ErrMethodUnknown) } - did.Blockchain, err = FindBlockchainForDIDMethodByValue(did.Method, typ[1]) + + return method, nil +} + +func BlockchainFromID(id ID) (Blockchain, error) { + method, blockchain, netID, err := decodeDIDPartsFromID(id) if err != nil { - return nil, err + return UnknownChain, err } - did.NetworkID, err = FindNetworkIDForDIDMethodByValue(did.Method, typ[1]) + + if isUnsupportedDID(method, blockchain, netID) { + return UnknownChain, fmt.Errorf("%w: unsupported DID", + ErrMethodUnknown) + } + + return blockchain, nil +} + +func NetworkIDFromID(id ID) (NetworkID, error) { + method, blockchain, netID, err := decodeDIDPartsFromID(id) if err != nil { - return nil, err + return UnknownNetwork, err + } + + if isUnsupportedDID(method, blockchain, netID) { + return UnknownNetwork, fmt.Errorf("%w: unsupported DID", + ErrMethodUnknown) + } + + return netID, nil +} + +func EthAddressFromID(id ID) ([20]byte, error) { + var z [7]byte + if !bytes.Equal(z[:], id[2:2+len(z)]) { + return [20]byte{}, errors.New( + "can't get Ethereum address: high bytes of genesis are not zero") } - return &did, nil + + var address [20]byte + copy(address[:], id[2+7:]) + return address, nil +} + +func GenesisFromEthAddress(addr [20]byte) [genesisLn]byte { + var genesis [genesisLn]byte + copy(genesis[7:], addr[:]) + return genesis +} + +func isUnsupportedDID(method DIDMethod, blockchain Blockchain, + networkID NetworkID) bool { + + return method == DIDMethodOther && blockchain == UnknownChain && + networkID == UnknownNetwork } diff --git a/did_test.go b/did_test.go index 8571305..8deaa2e 100644 --- a/did_test.go +++ b/did_test.go @@ -1,10 +1,13 @@ package core import ( + "encoding/hex" "encoding/json" + "fmt" "math/big" "testing" + "github.com/iden3/go-iden3-core/v2/w3c" "github.com/stretchr/testify/require" ) @@ -13,37 +16,49 @@ func TestParseDID(t *testing.T) { // did didStr := "did:iden3:polygon:mumbai:wyFiV4w71QgWPn6bYLsZoysFay66gKtVa9kfu6yMZ" - did, err := ParseDID(didStr) + did, err := w3c.ParseDID(didStr) require.NoError(t, err) - require.Equal(t, "wyFiV4w71QgWPn6bYLsZoysFay66gKtVa9kfu6yMZ", - did.ID.String()) - require.Equal(t, Mumbai, did.NetworkID) - require.Equal(t, Polygon, did.Blockchain) + id, err := IDFromDID(*did) + require.NoError(t, err) + require.Equal(t, "wyFiV4w71QgWPn6bYLsZoysFay66gKtVa9kfu6yMZ", id.String()) + method, err := MethodFromID(id) + require.NoError(t, err) + require.Equal(t, DIDMethodIden3, method) + blockchain, err := BlockchainFromID(id) + require.NoError(t, err) + require.Equal(t, Polygon, blockchain) + networkID, err := NetworkIDFromID(id) + require.NoError(t, err) + require.Equal(t, Mumbai, networkID) // readonly did - didStr = "did:iden3:tN4jDinQUdMuJJo6GbVeKPNTPCJ7txyXTWU4T2tJa" + didStr = "did:iden3:readonly:tN4jDinQUdMuJJo6GbVeKPNTPCJ7txyXTWU4T2tJa" - did, err = ParseDID(didStr) + did, err = w3c.ParseDID(didStr) require.NoError(t, err) - require.Equal(t, "tN4jDinQUdMuJJo6GbVeKPNTPCJ7txyXTWU4T2tJa", - did.ID.String()) - require.Equal(t, NetworkID(""), did.NetworkID) - require.Equal(t, Blockchain(""), did.Blockchain) + id, err = IDFromDID(*did) + require.NoError(t, err) + require.Equal(t, "tN4jDinQUdMuJJo6GbVeKPNTPCJ7txyXTWU4T2tJa", id.String()) + method, err = MethodFromID(id) + require.NoError(t, err) + require.Equal(t, DIDMethodIden3, method) + blockchain, err = BlockchainFromID(id) + require.NoError(t, err) + require.Equal(t, ReadOnly, blockchain) + networkID, err = NetworkIDFromID(id) + require.NoError(t, err) + require.Equal(t, NoNetwork, networkID) - require.Equal(t, [2]byte{DIDMethodByte[DIDMethodIden3], 0b0}, did.ID.Type()) + require.Equal(t, [2]byte{DIDMethodByte[DIDMethodIden3], 0b0}, id.Type()) } func TestDID_MarshalJSON(t *testing.T) { id, err := IDFromString("wyFiV4w71QgWPn6bYLsZoysFay66gKtVa9kfu6yMZ") require.NoError(t, err) - did := DID{ - ID: id, - Method: DIDMethodIden3, - Blockchain: Polygon, - NetworkID: Mumbai, - } + did, err := ParseDIDFromID(id) + require.NoError(t, err) b, err := did.MarshalJSON() require.NoError(t, err) @@ -57,79 +72,310 @@ func TestDID_UnmarshalJSON(t *testing.T) { id, err := IDFromString("wyFiV4w71QgWPn6bYLsZoysFay66gKtVa9kfu6yMZ") require.NoError(t, err) var obj struct { - Obj *DID `json:"obj"` + Obj *w3c.DID `json:"obj"` } err = json.Unmarshal([]byte(inBytes), &obj) require.NoError(t, err) require.NotNil(t, obj.Obj) - require.Equal(t, id, obj.Obj.ID) - require.Equal(t, DIDMethodIden3, obj.Obj.Method) - require.Equal(t, Polygon, obj.Obj.Blockchain) - require.Equal(t, Mumbai, obj.Obj.NetworkID) + require.Equal(t, string(DIDMethodIden3), obj.Obj.Method) + + id2, err := IDFromDID(*obj.Obj) + require.NoError(t, err) + method, err := MethodFromID(id2) + require.NoError(t, err) + require.Equal(t, DIDMethodIden3, method) + blockchain, err := BlockchainFromID(id2) + require.NoError(t, err) + require.Equal(t, Polygon, blockchain) + networkID, err := NetworkIDFromID(id2) + require.NoError(t, err) + require.Equal(t, Mumbai, networkID) + + require.Equal(t, id, id2) } func TestDID_UnmarshalJSON_Error(t *testing.T) { inBytes := `{"obj": "did:iden3:eth:goerli:wyFiV4w71QgWPn6bYLsZoysFay66gKtVa9kfu6yMZ"}` var obj struct { - Obj *DID `json:"obj"` + Obj *w3c.DID `json:"obj"` } err := json.Unmarshal([]byte(inBytes), &obj) - require.EqualError(t, err, - "invalid did format: network method of core identity mumbai differs from given did network specific id goerli") + require.NoError(t, err) + + //_, err = IDFromDID(*obj.Obj) + //require.EqualError(t, err, "invalid did format: blockchain mismatch: "+ + // "found polygon in ID but eth in DID") } func TestDIDGenesisFromState(t *testing.T) { - typ0, err := BuildDIDType(DIDMethodIden3, NoChain, NoNetwork) + typ0, err := BuildDIDType(DIDMethodIden3, ReadOnly, NoNetwork) + require.NoError(t, err) + + genesisState := big.NewInt(1) + did, err := NewDIDFromIdenState(typ0, genesisState) + require.NoError(t, err) + + require.Equal(t, string(DIDMethodIden3), did.Method) + + id, err := IDFromDID(*did) + require.NoError(t, err) + method, err := MethodFromID(id) + require.NoError(t, err) + require.Equal(t, DIDMethodIden3, method) + blockchain, err := BlockchainFromID(id) + require.NoError(t, err) + require.Equal(t, ReadOnly, blockchain) + networkID, err := NetworkIDFromID(id) + require.NoError(t, err) + require.Equal(t, NoNetwork, networkID) + + require.Equal(t, + "did:iden3:readonly:tJ93RwaVfE1PEMxd5rpZZuPtLCwbEaDCrNBhAy8HM", + did.String()) +} + +func TestDIDFromID(t *testing.T) { + typ0, err := BuildDIDType(DIDMethodIden3, ReadOnly, NoNetwork) require.NoError(t, err) genesisState := big.NewInt(1) - did, err := DIDGenesisFromIdenState(typ0, genesisState) + id, err := NewIDFromIdenState(typ0, genesisState) + require.NoError(t, err) + + did, err := ParseDIDFromID(*id) require.NoError(t, err) - require.Equal(t, DIDMethodIden3, did.Method) - require.Equal(t, NoChain, did.Blockchain) - require.Equal(t, NoNetwork, did.NetworkID) - require.Equal(t, "did:iden3:tJ93RwaVfE1PEMxd5rpZZuPtLCwbEaDCrNBhAy8HM", did.String()) + require.Equal(t, + "did:iden3:readonly:tJ93RwaVfE1PEMxd5rpZZuPtLCwbEaDCrNBhAy8HM", + did.String()) } func TestDID_PolygonID_Types(t *testing.T) { + testCases := []struct { + title string + method DIDMethod + chain Blockchain + net NetworkID + wantDID string + }{ + { + title: "Polygon no chain, no network", + method: DIDMethodPolygonID, + chain: ReadOnly, + net: NoNetwork, + wantDID: "did:polygonid:readonly:2mbH5rt9zKT1mTivFAie88onmfQtBU9RQhjNPLwFZh", + }, + { + title: "Polygon | Polygon chain, Main", + method: DIDMethodPolygonID, + chain: Polygon, + net: Main, + wantDID: "did:polygonid:polygon:main:2pzr1wiBm3Qhtq137NNPPDFvdk5xwRsjDFnMxpnYHm", + }, + { + title: "Polygon | Polygon chain, Mumbai", + method: DIDMethodPolygonID, + chain: Polygon, + net: Mumbai, + wantDID: "did:polygonid:polygon:mumbai:2qCU58EJgrELNZCDkSU23dQHZsBgAFWLNpNezo1g6b", + }, + { + title: "Polygon | zkEVM chain, main", + method: DIDMethodPolygonID, + chain: ZkEVM, + net: Main, + wantDID: "did:polygonid:zkevm:main:2wQjmkL1SsgqC7AuZdUcaXsUVfEi1i58VEhm3r2r8F", + }, + { + title: "Polygon | zkEVM chain, test", + method: DIDMethodPolygonID, + chain: ZkEVM, + net: Test, + wantDID: "did:polygonid:zkevm:test:2wcMpvr8NgWTfqN6ChaFEx1qRnLREXhjeoJ45pFyw5", + }, + } + + for i := range testCases { + tc := testCases[i] + t.Run(tc.title, func(t *testing.T) { + did := helperBuildDIDFromType(t, tc.method, tc.chain, tc.net) + require.Equal(t, string(tc.method), did.Method) + id, err := IDFromDID(*did) + require.NoError(t, err) + method, err := MethodFromID(id) + require.NoError(t, err) + require.Equal(t, tc.method, method) + blockchain, err := BlockchainFromID(id) + require.NoError(t, err) + require.Equal(t, tc.chain, blockchain) + networkID, err := NetworkIDFromID(id) + require.NoError(t, err) + require.Equal(t, tc.net, networkID) + require.Equal(t, tc.wantDID, did.String()) + }) + } +} + +func TestDID_PolygonID_ParseDIDFromID(t *testing.T) { + id1, err := IDFromString("2qCU58EJgrEM9NKvHkvg5NFWUiJPgN3M3LnCr98j3x") + require.NoError(t, err) + + did, err := ParseDIDFromID(id1) + require.NoError(t, err) + + var addressBytesExp [20]byte + _, err = hex.Decode(addressBytesExp[:], + []byte("A51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0")) + require.NoError(t, err) - // Polygon no chain, no network - did := helperBuildDIDFromType(t, DIDMethodPolygonID, NoChain, NoNetwork) + require.Equal(t, string(DIDMethodPolygonID), did.Method) + wantIDs := []string{"polygon", "mumbai", + "2qCU58EJgrEM9NKvHkvg5NFWUiJPgN3M3LnCr98j3x"} + require.Equal(t, wantIDs, did.IDStrings) + id, err := IDFromDID(*did) + require.NoError(t, err) + method, err := MethodFromID(id) + require.NoError(t, err) + require.Equal(t, DIDMethodPolygonID, method) + blockchain, err := BlockchainFromID(id) + require.NoError(t, err) + require.Equal(t, Polygon, blockchain) + networkID, err := NetworkIDFromID(id) + require.NoError(t, err) + require.Equal(t, Mumbai, networkID) + + ethAddr, err := EthAddressFromID(id) + require.NoError(t, err) + require.Equal(t, addressBytesExp, ethAddr) - require.Equal(t, DIDMethodPolygonID, did.Method) - require.Equal(t, NoChain, did.Blockchain) - require.Equal(t, NoNetwork, did.NetworkID) - require.Equal(t, "did:polygonid:2mbH5rt9zKT1mTivFAie88onmfQtBU9RQhjNPLwFZh", did.String()) + require.Equal(t, + "did:polygonid:polygon:mumbai:2qCU58EJgrEM9NKvHkvg5NFWUiJPgN3M3LnCr98j3x", + did.String()) +} - // Polygon | Polygon chain, Main - did2 := helperBuildDIDFromType(t, DIDMethodPolygonID, Polygon, Main) +func TestDecompose(t *testing.T) { + wantIDHex := "2qCU58EJgrEM9NKvHkvg5NFWUiJPgN3M3LnCr98j3x" + ethAddrHex := "a51c1fc2f0d1a1b8494ed1fe312d7c3a78ed91c0" + genesis := genFromHex("00000000000000" + ethAddrHex) + tp, err := BuildDIDType(DIDMethodPolygonID, Polygon, Mumbai) + require.NoError(t, err) + id0 := NewID(tp, genesis) - require.Equal(t, DIDMethodPolygonID, did2.Method) - require.Equal(t, Polygon, did2.Blockchain) - require.Equal(t, Main, did2.NetworkID) - require.Equal(t, "did:polygonid:polygon:main:2pzr1wiBm3Qhtq137NNPPDFvdk5xwRsjDFnMxpnYHm", did2.String()) + s := fmt.Sprintf("did:polygonid:polygon:mumbai:%v", id0.String()) - // Polygon | Polygon chain, Mumbai - did3 := helperBuildDIDFromType(t, DIDMethodPolygonID, Polygon, Mumbai) + did, err := w3c.ParseDID(s) + require.NoError(t, err) - require.Equal(t, DIDMethodPolygonID, did3.Method) - require.Equal(t, Polygon, did3.Blockchain) - require.Equal(t, Mumbai, did3.NetworkID) - require.Equal(t, "did:polygonid:polygon:mumbai:2qCU58EJgrELNZCDkSU23dQHZsBgAFWLNpNezo1g6b", did3.String()) + wantID, err := IDFromString(wantIDHex) + require.NoError(t, err) + id, err := IDFromDID(*did) + require.NoError(t, err) + require.Equal(t, wantID, id) + + method, err := MethodFromID(id) + require.NoError(t, err) + require.Equal(t, DIDMethodPolygonID, method) + + blockchain, err := BlockchainFromID(id) + require.NoError(t, err) + require.Equal(t, Polygon, blockchain) + + networkID, err := NetworkIDFromID(id) + require.NoError(t, err) + require.Equal(t, Mumbai, networkID) + + ethAddr, err := EthAddressFromID(id) + require.NoError(t, err) + require.Equal(t, ethAddrFromHex(ethAddrHex), ethAddr) } -func helperBuildDIDFromType(t testing.TB, method DIDMethod, blockchain Blockchain, network NetworkID) *DID { +func helperBuildDIDFromType(t testing.TB, method DIDMethod, + blockchain Blockchain, network NetworkID) *w3c.DID { t.Helper() typ, err := BuildDIDType(method, blockchain, network) require.NoError(t, err) genesisState := big.NewInt(1) - did, err := DIDGenesisFromIdenState(typ, genesisState) + did, err := NewDIDFromIdenState(typ, genesisState) require.NoError(t, err) return did } + +func TestNewIDFromDID(t *testing.T) { + did, err := w3c.ParseDID("did:something:x") + require.NoError(t, err) + id := newIDFromUnsupportedDID(*did) + require.Equal(t, []byte{0xff, 0xff}, id[:2]) + wantID, err := hex.DecodeString( + "ffff84b1e6d0d9ecbe951348ea578dbacc022cdbbff4b11218671dca871c11") + require.NoError(t, err) + require.Equal(t, wantID, id[:]) + + id2, err := IDFromDID(*did) + require.NoError(t, err) + require.Equal(t, id, id2) +} + +func TestGenesisFromEthAddress(t *testing.T) { + + ethAddrHex := "accb91a7d1d9ad0d33b83f2546ed30285c836c6e" + wantGenesisHex := "00000000000000accb91a7d1d9ad0d33b83f2546ed30285c836c6e" + require.Len(t, ethAddrHex, 20*2) + require.Len(t, wantGenesisHex, 27*2) + + ethAddrBytes, err := hex.DecodeString(ethAddrHex) + require.NoError(t, err) + var ethAddr [20]byte + copy(ethAddr[:], ethAddrBytes) + + genesis := GenesisFromEthAddress(ethAddr) + wantGenesis, err := hex.DecodeString(wantGenesisHex) + require.NoError(t, err) + require.Equal(t, wantGenesis, genesis[:]) + + tp2, err := BuildDIDType(DIDMethodPolygonID, Polygon, Mumbai) + require.NoError(t, err) + + id := NewID(tp2, genesis) + ethAddr2, err := EthAddressFromID(id) + require.NoError(t, err) + require.Equal(t, ethAddr, ethAddr2) + + var wantID ID + copy(wantID[:], tp2[:]) + copy(wantID[len(tp2):], genesis[:]) + ch := CalculateChecksum(tp2, genesis) + copy(wantID[len(tp2)+len(genesis):], ch[:]) + require.Equal(t, wantID, id) + + // make genesis not look like an address + genesis[0] = 1 + id = NewID(tp2, genesis) + _, err = EthAddressFromID(id) + require.EqualError(t, err, + "can't get Ethereum address: high bytes of genesis are not zero") +} + +func genFromHex(gh string) [genesisLn]byte { + genBytes, err := hex.DecodeString(gh) + if err != nil { + panic(err) + } + var gen [genesisLn]byte + copy(gen[:], genBytes) + return gen +} + +func ethAddrFromHex(ea string) [20]byte { + eaBytes, err := hex.DecodeString(ea) + if err != nil { + panic(err) + } + var ethAddr [20]byte + copy(ethAddr[:], eaBytes) + return ethAddr +} diff --git a/go.mod b/go.mod index d70ffea..6666470 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/iden3/go-iden3-core +module github.com/iden3/go-iden3-core/v2 go 1.18 diff --git a/id.go b/id.go index bc68910..39c7001 100644 --- a/id.go +++ b/id.go @@ -15,10 +15,6 @@ var ( // - first 2 bytes: `00000000 00000000` TypeDefault = [2]byte{0x00, 0x00} - // TypeReadOnly specifies the readonly identity, this type of identity MUST not be published on chain - // - first 2 bytes: `00000000 00000001` - TypeReadOnly = [2]byte{0b00000000, 0b00000001} - // TypeDID specifies the identity with iden3 method in specific networks // - first byte: did method e.g. 00000001 - iden3 did method // - second byte - blockchain network @@ -29,6 +25,7 @@ var ( ) const idLength = 31 +const genesisLn = 27 // ID is a byte array with // [ type | root_genesis | checksum ] @@ -37,13 +34,13 @@ const idLength = 31 type ID [idLength]byte // NewID creates a new ID from a type and genesis -func NewID(typ [2]byte, genesis [27]byte) ID { +func NewID(typ [2]byte, genesis [genesisLn]byte) ID { checksum := CalculateChecksum(typ, genesis) - var b [31]byte + var b ID copy(b[:2], typ[:]) copy(b[2:], genesis[:]) copy(b[29:], checksum[:]) - return ID(b) + return b } // ProfileID calculates the Profile ID from the Identity and profile nonce. If nonce is empty or zero ID is returned @@ -63,8 +60,8 @@ func ProfileID(id ID, nonce *big.Int) (ID, error) { return ID{}, err } - var genesis [27]byte - copy(genesis[:], firstNBytes(hash, 27)) + var genesis [genesisLn]byte + copy(genesis[:], firstNBytes(hash, genesisLn)) return NewID(typ, genesis), nil } @@ -163,10 +160,9 @@ func IDFromInt(i *big.Int) (ID, error) { } // DecomposeID returns type, genesis and checksum from an ID -func DecomposeID(id ID) ([2]byte, [27]byte, [2]byte, error) { - var typ [2]byte - var genesis [27]byte - var checksum [2]byte +func DecomposeID(id ID) (typ [2]byte, genesis [genesisLn]byte, checksum [2]byte, + err error) { + copy(typ[:], id[:2]) copy(genesis[:], id[2:len(id)-2]) copy(checksum[:], id[len(id)-2:]) @@ -177,7 +173,7 @@ func DecomposeID(id ID) ([2]byte, [27]byte, [2]byte, error) { // where checksum: // // hash( [type | root_genesis ] ) -func CalculateChecksum(typ [2]byte, genesis [27]byte) [2]byte { +func CalculateChecksum(typ [2]byte, genesis [genesisLn]byte) [2]byte { var toChecksum [29]byte copy(toChecksum[:], typ[:]) copy(toChecksum[2:], genesis[:]) @@ -204,11 +200,9 @@ func CheckChecksum(id ID) bool { return bytes.Equal(c[:], checksum[:]) } -// IdGenesisFromIdenState calculates the genesis ID from an Identity State. -func IdGenesisFromIdenState(typ [2]byte, //nolint:revive - state *big.Int) (*ID, error) { - - var idGenesisBytes [27]byte +// NewIDFromIdenState calculates the genesis ID from an Identity State. +func NewIDFromIdenState(typ [2]byte, state *big.Int) (*ID, error) { + var idGenesisBytes [genesisLn]byte idenStateData, err := NewElemBytesFromInt(state) if err != nil { @@ -216,7 +210,7 @@ func IdGenesisFromIdenState(typ [2]byte, //nolint:revive } // we take last 27 bytes, because of swapped endianness - copy(idGenesisBytes[:], idenStateData[len(idenStateData)-27:]) + copy(idGenesisBytes[:], idenStateData[len(idenStateData)-genesisLn:]) id := NewID(typ, idGenesisBytes) return &id, nil } @@ -233,7 +227,7 @@ func CheckGenesisStateID(id, state *big.Int) (bool, error) { if err != nil { return false, err } - identifier, err := IdGenesisFromIdenState(userID.Type(), state) + identifier, err := NewIDFromIdenState(userID.Type(), state) if err != nil { return false, err } diff --git a/id_test.go b/id_test.go index 25490ad..f282851 100644 --- a/id_test.go +++ b/id_test.go @@ -11,7 +11,6 @@ import ( "strings" "testing" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -39,7 +38,7 @@ func TestIDparsers(t *testing.T) { copy(genesis0[:], genesis032bytes[:]) id0 := NewID(typ0, genesis0) // Check ID0 - assert.Equal(t, "114vgnnCupQMX4wqUBjg5kUya3zMXfPmKc9HNH4m2E", id0.String()) + require.Equal(t, "114vgnnCupQMX4wqUBjg5kUya3zMXfPmKc9HNH4m2E", id0.String()) // Generate ID1 var typ1 [2]byte typ1Hex, _ := hex.DecodeString("0001") @@ -49,36 +48,36 @@ func TestIDparsers(t *testing.T) { copy(genesis1[:], genesis132bytes[:]) id1 := NewID(typ1, genesis1) // Check ID1 - assert.Equal(t, "1GYjyJKqdDyzo927FqJkAdLWB64kV2NVAjaQFHtq4", id1.String()) + require.Equal(t, "1GYjyJKqdDyzo927FqJkAdLWB64kV2NVAjaQFHtq4", id1.String()) emptyChecksum := []byte{0x00, 0x00} - assert.True(t, !bytes.Equal(emptyChecksum, id0[29:])) - assert.True(t, !bytes.Equal(emptyChecksum, id1[29:])) + require.True(t, !bytes.Equal(emptyChecksum, id0[29:])) + require.True(t, !bytes.Equal(emptyChecksum, id1[29:])) id0FromBytes, err := IDFromBytes(id0.Bytes()) - assert.Nil(t, err) - assert.Equal(t, id0.Bytes(), id0FromBytes.Bytes()) - assert.Equal(t, id0.String(), id0FromBytes.String()) - assert.Equal(t, "114vgnnCupQMX4wqUBjg5kUya3zMXfPmKc9HNH4m2E", + require.NoError(t, err) + require.Equal(t, id0.Bytes(), id0FromBytes.Bytes()) + require.Equal(t, id0.String(), id0FromBytes.String()) + require.Equal(t, "114vgnnCupQMX4wqUBjg5kUya3zMXfPmKc9HNH4m2E", id0FromBytes.String()) id1FromBytes, err := IDFromBytes(id1.Bytes()) - assert.Nil(t, err) - assert.Equal(t, id1.Bytes(), id1FromBytes.Bytes()) - assert.Equal(t, id1.String(), id1FromBytes.String()) - assert.Equal(t, "1GYjyJKqdDyzo927FqJkAdLWB64kV2NVAjaQFHtq4", + require.NoError(t, err) + require.Equal(t, id1.Bytes(), id1FromBytes.Bytes()) + require.Equal(t, id1.String(), id1FromBytes.String()) + require.Equal(t, "1GYjyJKqdDyzo927FqJkAdLWB64kV2NVAjaQFHtq4", id1FromBytes.String()) id0FromString, err := IDFromString(id0.String()) - assert.Nil(t, err) - assert.Equal(t, id0.Bytes(), id0FromString.Bytes()) - assert.Equal(t, id0.String(), id0FromString.String()) - assert.Equal(t, "114vgnnCupQMX4wqUBjg5kUya3zMXfPmKc9HNH4m2E", + require.NoError(t, err) + require.Equal(t, id0.Bytes(), id0FromString.Bytes()) + require.Equal(t, id0.String(), id0FromString.String()) + require.Equal(t, "114vgnnCupQMX4wqUBjg5kUya3zMXfPmKc9HNH4m2E", id0FromString.String()) } func TestIDAsDID(t *testing.T) { - typ, err := BuildDIDType(DIDMethodIden3, Polygon, Mumbai) + typ, err := BuildDIDType(DIDMethodPolygonID, Polygon, Mumbai) require.NoError(t, err) var genesis1 [27]byte genesisbytes := hashBytes([]byte("genesistes1t2")) @@ -86,29 +85,30 @@ func TestIDAsDID(t *testing.T) { id := NewID(typ, genesis1) fmt.Println(id.String()) + fmt.Printf("%x\n", id.Bytes()) } func TestIDjsonParser(t *testing.T) { id, err := IDFromString("11AVZrKNJVqDJoyKrdyaAgEynyBEjksV5z2NjZogFv") - assert.Nil(t, err) + require.NoError(t, err) idj, err := json.Marshal(&id) - assert.Nil(t, err) - assert.Equal(t, "11AVZrKNJVqDJoyKrdyaAgEynyBEjksV5z2NjZogFv", + require.NoError(t, err) + require.Equal(t, "11AVZrKNJVqDJoyKrdyaAgEynyBEjksV5z2NjZogFv", strings.Replace(string(idj), "\"", "", 2)) var idp ID err = json.Unmarshal(idj, &idp) - assert.Nil(t, err) + require.NoError(t, err) - assert.Equal(t, id, idp) + require.Equal(t, id, idp) idsMap := make(map[ID]string) idsMap[id] = "first" idsMapJSON, err := json.Marshal(idsMap) - assert.Nil(t, err) + require.NoError(t, err) var idsMapUnmarshaled map[ID]string err = json.Unmarshal(idsMapJSON, &idsMapUnmarshaled) - assert.Nil(t, err) + require.NoError(t, err) } func TestCheckChecksum(t *testing.T) { @@ -121,19 +121,19 @@ func TestCheckChecksum(t *testing.T) { var checksum [2]byte copy(checksum[:], id[len(id)-2:]) - assert.Equal(t, CalculateChecksum(typ, genesis), checksum) + require.Equal(t, CalculateChecksum(typ, genesis), checksum) - assert.True(t, CheckChecksum(id)) + require.True(t, CheckChecksum(id)) // check that if we change the checksum, returns false on CheckChecksum id = NewID(typ, genesis) copy(id[29:], []byte{0x00, 0x01}) - assert.True(t, !CheckChecksum(id)) + require.True(t, !CheckChecksum(id)) // check that if we change the type, returns false on CheckChecksum id = NewID(typ, genesis) copy(id[:2], []byte{0x00, 0x01}) - assert.True(t, !CheckChecksum(id)) + require.True(t, !CheckChecksum(id)) // check that if we change the genesis, returns false on CheckChecksum id = NewID(typ, genesis) @@ -143,24 +143,24 @@ func TestCheckChecksum(t *testing.T) { copy(changedGenesis[:], changedGenesis32bytes[:27]) copy(id[2:27], changedGenesis[:]) - assert.True(t, !CheckChecksum(id)) + require.True(t, !CheckChecksum(id)) // test with a empty id var empty [31]byte _, err := IDFromBytes(empty[:]) - assert.Equal(t, errors.New("IDFromBytes error: byte array empty"), err) + require.Equal(t, errors.New("IDFromBytes error: byte array empty"), err) } func TestIDFromInt(t *testing.T) { id, err := IDFromString("11AVZrKNJVqDJoyKrdyaAgEynyBEjksV5z2NjZogFv") - assert.Nil(t, err) + require.NoError(t, err) intID := id.BigInt() got, err := IDFromInt(intID) - assert.Nil(t, err) + require.NoError(t, err) - assert.Equal(t, id, got) + require.Equal(t, id, got) } func TestIDFromIntStr(t *testing.T) { @@ -232,57 +232,12 @@ func TestIDinDIDFormat(t *testing.T) { var checksum [2]byte copy(checksum[:], id[len(id)-2:]) - assert.Equal(t, CalculateChecksum(typ, genesis), checksum) - - fmt.Println(id.String()) - did := DID{ - ID: id, - Blockchain: Polygon, - NetworkID: Mumbai, - } - fmt.Println(did.String()) -} -func TestIDFromDIDString(t *testing.T) { - - didFromStr, err := ParseDID("did:iden3:polygon:mumbai:wyFiV4w71QgWPn6bYLsZoysFay66gKtVa9kfu6yMZ") - require.NoError(t, err) - typ, err := BuildDIDType(didFromStr.Method, didFromStr.Blockchain, didFromStr.NetworkID) - require.NoError(t, err) - - var genesis [27]byte - genesis32bytes := hashBytes([]byte("genesistest")) - copy(genesis[:], genesis32bytes[:]) - - id := NewID(typ, genesis) - - var checksum [2]byte - copy(checksum[:], id[len(id)-2:]) - assert.Equal(t, CalculateChecksum(typ, genesis), checksum) - assert.Equal(t, didFromStr.ID.String(), id.String()) - + require.Equal(t, CalculateChecksum(typ, genesis), checksum) } func TestID_Type(t *testing.T) { id, err := IDFromString("1MWtoAdZESeiphxp3bXupZcfS9DhMTdWNSjRwVYc2") - assert.Nil(t, err) - - assert.Equal(t, id.Type(), TypeReadOnly) -} - -func TestCheckGenesisStateID(t *testing.T) { - userDID, err := ParseDID("did:iden3:polygon:mumbai:x6suHR8HkEYczV9yVeAKKiXCZAd25P8WS6QvNhszk") - require.NoError(t, err) - genesisID, ok := big.NewInt(0).SetString("7521024223205616003431860562270429547098131848980857190502964780628723574810", 10) - require.True(t, ok) - - isGenesis, err := CheckGenesisStateID(userDID.ID.BigInt(), genesisID) require.NoError(t, err) - require.True(t, isGenesis) - - notGenesisState, ok := big.NewInt(0).SetString("6017654403209798611575982337826892532952335378376369712724079246845524041042", 10) - require.True(t, ok) - isGenesis, err = CheckGenesisStateID(userDID.ID.BigInt(), notGenesisState) - require.NoError(t, err) - require.False(t, isGenesis) + require.Equal(t, id.Type(), [2]byte{0x00, 0x01}) } diff --git a/w3c/did_w3c.go b/w3c/did_w3c.go new file mode 100644 index 0000000..c0040f0 --- /dev/null +++ b/w3c/did_w3c.go @@ -0,0 +1,818 @@ +// Package w3c is a set of tools to work with Decentralized Identifiers (DIDs) +// as described in the DID spec https://w3c.github.io/did-core/ +// Got from https://github.com/build-trust/did +package w3c + +import ( + "fmt" + "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 +} + +// A DID represents a parsed DID or a DID URL +type DID struct { + // DID Method + // https://w3c.github.io/did-core/#method-specific-syntax + Method string + + // The method-specific-id component of a DID + // method-specific-id = *idchar *( ":" *idchar ) + ID string + + // 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 + + // Path may be composed of multiple `/` separated segments + // path-abempty = *( "/" segment ) + PathSegments []string + + // DID Query + // https://w3c.github.io/did-core/#query + // query = *( pchar / "/" / "?" ) + Query string + + // DID Fragment, the portion of a DID reference that follows the first hash sign character ("#") + // https://w3c.github.io/did-core/#fragment + 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 != "") +} + +// 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 there is no Method, return an empty string + return "" + } + + if d.ID != "" { + buf.WriteString(d.ID) // nolint, returned error is always nil + } 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 + } 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 + } 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 + } + + 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 != "" { + // 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 + } + + return buf.String() +} + +// ParseDID parses the input string into a DID structure. +func ParseDID(input string) (*DID, error) { + // initialize 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 + 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. at least 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") + } + + 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") + } + + 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 ParseDID 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 + } + + // 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 + } + + // 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 + } +} + +// 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') +} diff --git a/w3c/did_w3c_test.go b/w3c/did_w3c_test.go new file mode 100644 index 0000000..436d900 --- /dev/null +++ b/w3c/did_w3c_test.go @@ -0,0 +1,765 @@ +// Got from https://github.com/build-trust/did +package w3c + +import ( + "fmt" + "path/filepath" + "reflect" + "runtime" + "testing" +) + +func TestIsURL(t *testing.T) { + t.Run("returns false if no Path or Fragment", func(t *testing.T) { + d := &DID{Method: "example", ID: "123"} + 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()) + }) + + t.Run("returns true if PathSegements", func(t *testing.T) { + d := &DID{Method: "example", ID: "123", PathSegments: []string{"a", "b"}} + assert(t, true, d.IsURL()) + }) + + t.Run("returns true if Query", func(t *testing.T) { + d := &DID{Method: "example", ID: "123", Query: "abc"} + assert(t, true, d.IsURL()) + }) + + t.Run("returns true if Fragment", func(t *testing.T) { + d := &DID{Method: "example", ID: "123", Fragment: "00000"} + assert(t, true, d.IsURL()) + }) + + t.Run("returns true if Path and Fragment", func(t *testing.T) { + d := &DID{Method: "example", ID: "123", Path: "a/b", Fragment: "00000"} + assert(t, true, d.IsURL()) + }) +} + +func TestString(t *testing.T) { + t.Run("assembles a DID", func(t *testing.T) { + d := &DID{Method: "example", ID: "123"} + assert(t, "did:example:123", d.String()) + }) + + 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()) + }) + + t.Run("returns empty string if no method", func(t *testing.T) { + d := &DID{ID: "123"} + assert(t, "", d.String()) + }) + + t.Run("returns empty string in no ID or IDStrings", func(t *testing.T) { + d := &DID{Method: "example"} + 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()) + }) + + t.Run("includes Path assembled from PathSegements", func(t *testing.T) { + d := &DID{Method: "example", ID: "123", PathSegments: []string{"a", "b"}} + 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()) + }) + + 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()) + }) + + t.Run("includes Fragment after Param", func(t *testing.T) { + d := &DID{Method: "example", ID: "123", Fragment: "00000"} + assert(t, "did:example:123#00000", d.String()) + }) +} + +func TestParse(t *testing.T) { + + t.Run("returns error if input is empty", func(t *testing.T) { + _, err := ParseDID("") + assert(t, false, err == nil) + }) + + t.Run("returns error if input length is less than length 7", func(t *testing.T) { + _, err := ParseDID("did:") + assert(t, false, err == nil) + + _, err = ParseDID("did:a") + assert(t, false, err == nil) + + _, err = ParseDID("did:a:") + assert(t, false, err == nil) + }) + + t.Run("returns error if input does not have a second : to mark end of method", func(t *testing.T) { + _, err := ParseDID("did:aaaaaaaaaaa") + assert(t, false, err == nil) + }) + + t.Run("returns error if method is empty", func(t *testing.T) { + _, err := ParseDID("did::aaaaaaaaaaa") + 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 := ParseDID(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 := ParseDID("a:12345") + assert(t, false, err == nil) + }) + + t.Run("returned value is nil if input does not begin with did: scheme", func(t *testing.T) { + d, _ := ParseDID("a:12345") + assert(t, true, d == nil) + }) + + t.Run("succeeds if it has did prefix and length is greater than 7", func(t *testing.T) { + d, err := ParseDID("did:a:1") + assert(t, nil, err) + assert(t, true, d != nil) + }) + + t.Run("succeeds to extract method", func(t *testing.T) { + d, err := ParseDID("did:a:1") + assert(t, nil, err) + assert(t, "a", d.Method) + + d, err = ParseDID("did:abcdef:11111") + assert(t, nil, err) + assert(t, "abcdef", d.Method) + }) + + t.Run("returns error if method has any other char than 0-9 or a-z", func(t *testing.T) { + _, err := ParseDID("did:aA:1") + assert(t, false, err == nil) + + _, err = ParseDID("did:aa-aa:1") + assert(t, false, err == nil) + }) + + t.Run("succeeds to extract id", func(t *testing.T) { + d, err := ParseDID("did:a:1") + assert(t, nil, err) + assert(t, "1", d.ID) + }) + + t.Run("succeeds to extract id parts", func(t *testing.T) { + d, err := ParseDID("did:a:123:456") + assert(t, nil, err) + + parts := d.IDStrings + assert(t, "123", parts[0]) + assert(t, "456", parts[1]) + }) + + t.Run("returns error if ID has an invalid char", func(t *testing.T) { + _, err := ParseDID("did:a:1&&111") + assert(t, false, err == nil) + }) + + t.Run("returns error if param name is empty", func(t *testing.T) { + _, err := ParseDID("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 := ParseDID("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 := ParseDID("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 := ParseDID("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 := ParseDID("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 := ParseDID("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 := ParseDID("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 := ParseDID("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 := ParseDID("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 := ParseDID("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 := ParseDID("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 := ParseDID("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 := ParseDID("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 := ParseDID("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 := ParseDID("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 := ParseDID("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 := ParseDID("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 := ParseDID("did:a:123:456/someService") + assert(t, nil, err) + assert(t, "someService", d.Path) + }) + + t.Run("succeeds to extract path segements", func(t *testing.T) { + d, err := ParseDID("did:a:123:456/a/b") + assert(t, nil, err) + + segments := d.PathSegments + assert(t, "a", segments[0]) + assert(t, "b", segments[1]) + }) + + t.Run("succeeds with percent encoded chars in path", func(t *testing.T) { + d, err := ParseDID("did:a:123:456/a/%20a") + assert(t, nil, err) + assert(t, "a/%20a", d.Path) + }) + + t.Run("returns error if % in path 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 := ParseDID(did) + assert(t, false, err == nil, "Input: %s", did) + } + }) + + t.Run("returns error if path is empty but there is a slash", func(t *testing.T) { + _, err := ParseDID("did:a:123:456/") + assert(t, false, err == nil) + }) + + t.Run("returns error if first path segment is empty", func(t *testing.T) { + _, err := ParseDID("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 := ParseDID("did:a:123:456/abc//pqr") + assert(t, nil, err) + }) + + t.Run("returns error if path has invalid char", func(t *testing.T) { + _, err := ParseDID("did:a:123:456/ssss^sss") + assert(t, false, err == nil) + }) + + t.Run("does not fail if path has atleast one segment and a trailing slash", func(t *testing.T) { + _, err := ParseDID("did:a:123:456/a/b/") + assert(t, nil, err) + }) + + t.Run("succeeds to extract query after idstring", func(t *testing.T) { + d, err := ParseDID("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 := ParseDID("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 := ParseDID("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 := ParseDID("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 := ParseDID(did) + assert(t, false, err == nil, "Input: %s", did) + } + }) + + t.Run("returns error if query has invalid char", func(t *testing.T) { + _, err := ParseDID("did:a:123:456?ssss^sss") + assert(t, false, err == nil) + }) + + t.Run("succeeds to extract fragment", func(t *testing.T) { + d, err := ParseDID("did:a:123:456#keys-1") + assert(t, nil, err) + assert(t, "keys-1", d.Fragment) + }) + + t.Run("succeeds with percent encoded chars in fragment", func(t *testing.T) { + d, err := ParseDID("did:a:123:456#aaaaaa%20a") + assert(t, nil, err) + assert(t, "aaaaaa%20a", d.Fragment) + }) + + t.Run("returns error if % in fragment is not followed by 2 hex chars", func(t *testing.T) { + dids := []string{ + "did:xyz:pqr#%", + "did:xyz:pqr#%a", + "did:xyz:pqr#%!*", + "did:xyz:pqr#%!A", + "did:xyz:pqr#%A!", + "did:xyz:pqr#%A%", + } + for _, did := range dids { + _, err := ParseDID(did) + assert(t, false, err == nil, "Input: %s", did) + } + }) + + t.Run("fails if fragment has invalid char", func(t *testing.T) { + _, err := ParseDID("did:a:123:456#ssss^sss") + assert(t, false, err == nil) + }) +} + +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) + var message string + + // if only one arg is present, treat it as the message + if argsLength == 1 { + message = args[0].(string) + } + + // if more than one arg is present, treat it as format, args (like Printf) + if argsLength > 1 { + message = fmt.Sprintf(args[0].(string), args[1:]...) + } + + // is message is not empty add some spacing + if message != "" { + message = "\t" + message + "\n\n" + } + + _, file, line, _ := runtime.Caller(1) + fmt.Printf("%s:%d:\n\tExpected: %#v\n\tActual: %#v\n%s", filepath.Base(file), line, expected, actual, message) + t.FailNow() + } +} diff --git a/w3c/json.go b/w3c/json.go new file mode 100644 index 0000000..faf8343 --- /dev/null +++ b/w3c/json.go @@ -0,0 +1,22 @@ +package w3c + +import "encoding/json" + +func (did *DID) UnmarshalJSON(bytes []byte) error { + var didStr string + err := json.Unmarshal(bytes, &didStr) + if err != nil { + return err + } + + did3, err := ParseDID(didStr) + if err != nil { + return err + } + *did = *did3 + return nil +} + +func (did DID) MarshalJSON() ([]byte, error) { + return json.Marshal(did.String()) +}