From 3d4fdfb9e3fcfaa287e0ee7205d0e0d2d3a50feb Mon Sep 17 00:00:00 2001 From: Derrick Hammer Date: Wed, 3 Jan 2024 03:47:47 -0500 Subject: [PATCH] feat: initial Multihash --- multihash/multihash.go | 84 ++++++++++++++ multihash/multihash_test.go | 211 ++++++++++++++++++++++++++++++++++++ 2 files changed, 295 insertions(+) create mode 100644 multihash/multihash.go create mode 100644 multihash/multihash_test.go diff --git a/multihash/multihash.go b/multihash/multihash.go new file mode 100644 index 0000000..4624b2c --- /dev/null +++ b/multihash/multihash.go @@ -0,0 +1,84 @@ +package multihash + +import ( + "bytes" + "errors" + "fmt" + "git.lumeweb.com/LumeWeb/libs5-go/internal/bases" + "git.lumeweb.com/LumeWeb/libs5-go/types" + "github.com/multiformats/go-multibase" + "unicode/utf8" +) + +var ( + errorNotBase64Url = errors.New("not a base64url string") +) + +type Multihash struct { + FullBytes []byte +} + +func NewMultihash(fullBytes []byte) *Multihash { + return &Multihash{FullBytes: fullBytes} +} + +func (m *Multihash) FunctionType() types.HashType { + return types.HashType(m.FullBytes[0]) +} + +func (m *Multihash) HashBytes() []byte { + return m.FullBytes[1:] +} + +func FromBase64Url(hash string) (*Multihash, error) { + encoder, _ := multibase.EncoderByName("base64url") + encoding, err := getEncoding(hash) + + if encoding != encoder.Encoding() { + return nil, errorNotBase64Url + } + + _, ret, err := multibase.Decode(hash) + if err != nil { + return nil, err + } + return NewMultihash(ret), nil +} + +func (m *Multihash) ToBase64Url() (string, error) { + return bases.ToBase64Url(m.FullBytes) +} + +func (m *Multihash) ToBase32() (string, error) { + return bases.ToBase32(m.FullBytes) +} + +func (m *Multihash) ToString() (string, error) { + if m.FunctionType() == types.HashType(types.CIDTypeBridge) { + return string(m.FullBytes), nil // Assumes the bytes are valid UTF-8 + } + return m.ToBase64Url() +} + +func (m *Multihash) Equals(other *Multihash) bool { + return bytes.Equal(m.FullBytes, other.FullBytes) +} + +func (m *Multihash) HashCode() int { + return int(m.FullBytes[0]) + + int(m.FullBytes[1])<<8 + + int(m.FullBytes[2])<<16 + + int(m.FullBytes[3])<<24 +} + +func getEncoding(hash string) (multibase.Encoding, error) { + r, _ := utf8.DecodeRuneInString(hash) + enc := multibase.Encoding(r) + + _, ok := multibase.EncodingToStr[enc] + if !ok { + return -1, fmt.Errorf("unsupported multibase encoding: %d", enc) + + } + return enc, nil +} diff --git a/multihash/multihash_test.go b/multihash/multihash_test.go new file mode 100644 index 0000000..05b9d43 --- /dev/null +++ b/multihash/multihash_test.go @@ -0,0 +1,211 @@ +package multihash + +import ( + "git.lumeweb.com/LumeWeb/libs5-go/internal/testdata" + "git.lumeweb.com/LumeWeb/libs5-go/types" + "reflect" + "strings" + "testing" +) + +func TestFromBase64Url(t *testing.T) { + type args struct { + hash string + } + tests := []struct { + name string + args args + want *Multihash + wantErr bool + }{ + { + name: "Valid Base64 URL Encoded String", + args: args{hash: testdata.MediaBase64CID}, + want: &Multihash{FullBytes: testdata.MediaCIDBytes}, + wantErr: false, + }, + { + name: "Invalid Base64 URL String", + args: args{hash: "@@invalid@@"}, + want: nil, + wantErr: true, + }, + { + name: "Empty String", + args: args{hash: ""}, + want: nil, + wantErr: true, // or false + }, + { + name: "Non-URL Base64 Encoded String", + args: args{hash: "aGVsbG8gd29ybGQ="}, + want: nil, + wantErr: true, + }, + { + name: "String Not Representing a Multihash", + args: args{hash: "cGxhaW50ZXh0"}, + want: nil, + wantErr: true, + }, + { + name: "Long String", + args: args{hash: "uYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh"}, + want: &Multihash{FullBytes: []byte(strings.Repeat("a", 750))}, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := FromBase64Url(tt.args.hash) + if (err != nil) != tt.wantErr { + t.Errorf("FromBase64Url() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("FromBase64Url() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestMultihash_FunctionType(t *testing.T) { + type fields struct { + FullBytes []byte + } + tests := []struct { + name string + fields fields + want types.HashType + }{ + { + name: "Is Raw CID", + fields: fields{ + FullBytes: testdata.RawCIDBytes[1:34], + }, + want: types.HashTypeBlake3, + }, { + name: "Is Resolver CID", + fields: fields{ + FullBytes: testdata.ResolverCIDBytes[1:34], + }, + want: types.HashTypeEd25519, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := &Multihash{ + FullBytes: tt.fields.FullBytes, + } + if got := m.FunctionType(); got != tt.want { + t.Errorf("FunctionType() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestMultihash_ToBase32(t *testing.T) { + type fields struct { + FullBytes []byte + } + tests := []struct { + name string + fields fields + want string + wantErr bool + }{ + { + name: "Is Raw CID", + fields: fields{ + FullBytes: testdata.RawCIDBytes, + }, + want: "beyprdl3g2it54ua2ian3a5wybrnva6kr7kzkpv6wbfvgjsb2aepw6yc6t4eq", + }, { + name: "Is Media CID", + fields: fields{ + FullBytes: testdata.MediaCIDBytes, + }, + want: "byupv6i7z5g6unmx2btc2ihsrrivnox6gqnwfgiwkpuw36d6q4dl35hi", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := &Multihash{ + FullBytes: tt.fields.FullBytes, + } + got, err := m.ToBase32() + if (err != nil) != tt.wantErr { + t.Errorf("ToBase32() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("ToBase32() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestMultihash_ToBase64Url(t *testing.T) { + type fields struct { + FullBytes []byte + } + tests := []struct { + name string + fields fields + want string + wantErr bool + }{ + { + name: "Is Raw CID", + fields: fields{ + FullBytes: testdata.RawCIDBytes, + }, + want: "uJh8Rr2bSJ95QGkAbsHbYDFtQeVH6sqfX1glqZMg6AR9vYF6fCQ", + }, { + name: "Is Media CID", + fields: fields{ + FullBytes: testdata.MediaCIDBytes, + }, + want: "uxR9fI_npvUay-gzFpB5RiirXX8aDbFMiyn0tvw_Q4Ne-nQ", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := &Multihash{ + FullBytes: tt.fields.FullBytes, + } + got, err := m.ToBase64Url() + if (err != nil) != tt.wantErr { + t.Errorf("ToBase64Url() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("ToBase64Url() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNewMultihash(t *testing.T) { + type args struct { + fullBytes []byte + } + tests := []struct { + name string + args args + want *Multihash + }{ + { + name: "Valid Base64 URL Encoded String", + args: args{fullBytes: testdata.RawCIDBytes}, + want: &Multihash{FullBytes: testdata.RawCIDBytes}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := NewMultihash(tt.args.fullBytes); !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewMultihash() = %v, want %v", got, tt.want) + } + }) + } +}