feat: initial Multihash
This commit is contained in:
parent
7f502187e6
commit
3d4fdfb9e3
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue