feat: initial Multihash

This commit is contained in:
Derrick Hammer 2024-01-03 03:47:47 -05:00
parent 7f502187e6
commit 3d4fdfb9e3
Signed by: pcfreak30
GPG Key ID: C997C339BE476FF2
2 changed files with 295 additions and 0 deletions

84
multihash/multihash.go Normal file
View File

@ -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
}

211
multihash/multihash_test.go Normal file
View File

@ -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)
}
})
}
}