container: split into reader+writer

This commit is contained in:
Michael Muré
2024-10-02 11:56:32 +02:00
parent f7b4b48791
commit 60922ced96
7 changed files with 485 additions and 439 deletions

View File

@@ -1,48 +0,0 @@
package container
import (
"fmt"
"github.com/ipfs/go-cid"
"github.com/ucan-wg/go-ucan/token"
"github.com/ucan-wg/go-ucan/token/delegation"
)
// TODO: should the invocation being set as root in the car file?
var ErrNotFound = fmt.Errorf("not found")
type Container map[cid.Cid][]byte
func New() Container {
return make(Container)
}
func (ctn Container) AddBytes(cid cid.Cid, data []byte) {
ctn[cid] = data
}
func (ctn Container) GetBytes(cid cid.Cid) ([]byte, bool) {
b, ok := ctn[cid]
return b, ok
}
func (ctn Container) GetToken(cid cid.Cid) (token.Token, error) {
b, ok := ctn[cid]
if !ok {
return nil, ErrNotFound
}
return token.FromDagCbor(b)
}
func (ctn Container) GetDelegation(cid cid.Cid) (*delegation.Token, error) {
tkn, err := ctn.GetToken(cid)
if err != nil {
return nil, err
}
if tkn, ok := tkn.(*delegation.Token); ok {
return tkn, nil
}
return nil, fmt.Errorf("not a delegation token")
}

173
pkg/container/reader.go Normal file
View File

@@ -0,0 +1,173 @@
package container
import (
"compress/flate"
"compress/gzip"
"encoding/base64"
"fmt"
"io"
"github.com/ipfs/go-cid"
cbor "github.com/ipfs/go-ipld-cbor"
"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/codec/dagcbor"
"github.com/ipld/go-ipld-prime/datamodel"
"github.com/ucan-wg/go-ucan/token"
"github.com/ucan-wg/go-ucan/token/delegation"
"github.com/ucan-wg/go-ucan/token/invocation"
)
var ErrNotFound = fmt.Errorf("not found")
type Reader map[cid.Cid]token.Token
func (ctn Reader) GetToken(cid cid.Cid) (token.Token, error) {
tkn, ok := ctn[cid]
if !ok {
return nil, ErrNotFound
}
return tkn, nil
}
func (ctn Reader) GetDelegation(cid cid.Cid) (*delegation.Token, error) {
tkn, err := ctn.GetToken(cid)
if err != nil {
return nil, err
}
if tkn, ok := tkn.(*delegation.Token); ok {
return tkn, nil
}
return nil, fmt.Errorf("not a delegation token")
}
func (ctn Reader) GetInvocation() (*invocation.Token, error) {
for _, t := range ctn {
if inv, ok := t.(*invocation.Token); ok {
return inv, nil
}
}
return nil, ErrNotFound
}
func FromCar(r io.Reader) (Reader, error) {
_, it, err := readCar(r)
if err != nil {
return nil, err
}
ctn := make(Reader)
for block, err := range it {
if err != nil {
return nil, err
}
err = ctn.addToken(block.data)
if err != nil {
return nil, err
}
}
return ctn, nil
}
func FromCarBase64(r io.Reader) (Reader, error) {
return FromCar(base64.NewDecoder(base64.StdEncoding, r))
}
func FromCarGzip(r io.Reader) (Reader, error) {
r2, err := gzip.NewReader(r)
if err != nil {
return nil, err
}
defer r2.Close()
return FromCar(r2)
}
func FromCarGzipBase64(r io.Reader) (Reader, error) {
return FromCarGzip(base64.NewDecoder(base64.StdEncoding, r))
}
func FromCbor(r io.Reader) (Reader, error) {
var raw [][]byte
err := cbor.DecodeReader(r, &raw)
if err != nil {
return nil, err
}
ctn := make(Reader, len(raw))
for _, data := range raw {
err = ctn.addToken(data)
if err != nil {
return nil, err
}
}
return ctn, nil
}
func FromCbor2(r io.Reader) (Reader, error) {
n, err := ipld.DecodeStreaming(r, dagcbor.Decode)
if err != nil {
return nil, err
}
if n.Kind() != datamodel.Kind_List {
return nil, fmt.Errorf("not a list")
}
ctn := make(Reader, n.Length())
it := n.ListIterator()
for !it.Done() {
_, val, err := it.Next()
if err != nil {
return nil, err
}
data, err := val.AsBytes()
if err != nil {
return nil, err
}
err = ctn.addToken(data)
if err != nil {
return nil, err
}
}
return ctn, nil
}
func FromCborBase64(r io.Reader) (Reader, error) {
return FromCbor(base64.NewDecoder(base64.StdEncoding, r))
}
func FromCborGzip(r io.Reader) (Reader, error) {
r2, err := gzip.NewReader(r)
if err != nil {
return nil, err
}
defer r2.Close()
return FromCbor(r2)
}
func FromCborGzipBase64(r io.Reader) (Reader, error) {
return FromCborGzip(base64.NewDecoder(base64.StdEncoding, r))
}
func FromCborFlate(r io.Reader) (Reader, error) {
r2 := flate.NewReader(r)
defer r2.Close()
return FromCbor(r2)
}
func FromCborFlateBase64(r io.Reader) (Reader, error) {
return FromCborFlate(base64.NewDecoder(base64.StdEncoding, r))
}
func (ctn Reader) addToken(data []byte) error {
tkn, c, err := token.FromSealed(data)
if err != nil {
return err
}
ctn[c] = tkn
return nil
}

View File

@@ -0,0 +1,189 @@
package container
import (
"bytes"
"crypto/rand"
"fmt"
"io"
"testing"
"time"
"github.com/ipfs/go-cid"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/stretchr/testify/require"
"github.com/ucan-wg/go-ucan/did"
"github.com/ucan-wg/go-ucan/pkg/command"
"github.com/ucan-wg/go-ucan/pkg/policy"
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
"github.com/ucan-wg/go-ucan/pkg/policy/selector"
"github.com/ucan-wg/go-ucan/token/delegation"
)
func TestContainerRoundTrip(t *testing.T) {
for _, tc := range []struct {
name string
writer func(ctn Writer, w io.Writer) error
reader func(io.Reader) (Reader, error)
}{
{"car", Writer.ToCar, FromCar},
{"carBase64", Writer.ToCarBase64, FromCarBase64},
{"carGzip", Writer.ToCarGzip, FromCarGzip},
{"carGzipBase64", Writer.ToCarGzipBase64, FromCarGzipBase64},
{"cbor", Writer.ToCbor, FromCbor},
{"cborBase64", Writer.ToCborBase64, FromCborBase64},
{"cborGzip", Writer.ToCborGzip, FromCborGzip},
{"cborGzipBase64", Writer.ToCborGzipBase64, FromCborGzipBase64},
{"cborFlate", Writer.ToCborFlate, FromCborFlate},
{"cborFlateBase64", Writer.ToCborFlateBase64, FromCborFlateBase64},
{"cbor2", Writer.ToCbor2, FromCbor2},
} {
t.Run(tc.name, func(t *testing.T) {
tokens := make(map[cid.Cid]*delegation.Token)
var dataSize int
writer := NewWriter()
for i := 0; i < 10; i++ {
dlg, c, data := randToken()
writer.AddSealed(c, data)
tokens[c] = dlg
dataSize += len(data)
}
buf := bytes.NewBuffer(nil)
err := tc.writer(writer, buf)
require.NoError(t, err)
t.Logf("data size %d", dataSize)
t.Logf("container overhead: %d%%, %d bytes", int(float32(buf.Len()-dataSize)/float32(dataSize)*100.0), buf.Len()-dataSize)
reader, err := tc.reader(bytes.NewReader(buf.Bytes()))
require.NoError(t, err)
for c, dlg := range tokens {
tknRead, err := reader.GetToken(c)
require.NoError(t, err)
// require.Equal fails as time.Time holds a wall time that is going to be
// different, even if it represents the same event.
// We need to do the following instead.
dlgRead := tknRead.(*delegation.Token)
require.Equal(t, dlg.Issuer(), dlgRead.Issuer())
require.Equal(t, dlg.Audience(), dlgRead.Audience())
require.Equal(t, dlg.Subject(), dlgRead.Subject())
require.Equal(t, dlg.Command(), dlgRead.Command())
require.Equal(t, dlg.Policy(), dlgRead.Policy())
require.Equal(t, dlg.Nonce(), dlgRead.Nonce())
require.True(t, dlg.Meta().Equals(dlgRead.Meta()))
if dlg.NotBefore() != nil {
// within 1s as the original value gets truncated to seconds when serialized
require.WithinDuration(t, *dlg.NotBefore(), *dlgRead.NotBefore(), time.Second)
}
if dlg.Expiration() != nil {
// within 1s as the original value gets truncated to seconds when serialized
require.WithinDuration(t, *dlg.Expiration(), *dlgRead.Expiration(), time.Second)
}
}
})
}
}
func BenchmarkContainerSerialisation(b *testing.B) {
for _, tc := range []struct {
name string
writer func(ctn Writer, w io.Writer) error
reader func(io.Reader) (Reader, error)
}{
{"car", Writer.ToCar, FromCar},
{"carBase64", Writer.ToCarBase64, FromCarBase64},
{"carGzip", Writer.ToCarGzip, FromCarGzip},
{"carGzipBase64", Writer.ToCarGzipBase64, FromCarGzipBase64},
{"cbor", Writer.ToCbor, FromCbor},
{"cborBase64", Writer.ToCborBase64, FromCborBase64},
{"cborGzip", Writer.ToCborGzip, FromCborGzip},
{"cborGzipBase64", Writer.ToCborGzipBase64, FromCborGzipBase64},
{"cborFlate", Writer.ToCborFlate, FromCborFlate},
{"cborFlateBase64", Writer.ToCborFlateBase64, FromCborFlateBase64},
{"cbor2", Writer.ToCbor2, FromCbor2},
} {
writer := NewWriter()
for i := 0; i < 10; i++ {
_, c, data := randToken()
writer.AddSealed(c, data)
}
buf := bytes.NewBuffer(nil)
_ = tc.writer(writer, buf)
b.Run(tc.name+"_write", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
buf := bytes.NewBuffer(nil)
_ = tc.writer(writer, buf)
}
})
b.Run(tc.name+"_read", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, _ = tc.reader(bytes.NewReader(buf.Bytes()))
}
})
}
}
func randBytes(n int) []byte {
b := make([]byte, n)
_, _ = rand.Read(b)
return b
}
func randDID() (crypto.PrivKey, did.DID) {
privKey, _, err := crypto.GenerateEd25519Key(rand.Reader)
if err != nil {
panic(err)
}
d, err := did.FromPrivKey(privKey)
if err != nil {
panic(err)
}
return privKey, d
}
func randomString(length int) string {
b := make([]byte, length/2+1)
rand.Read(b)
return fmt.Sprintf("%x", b)[0:length]
}
func randToken() (*delegation.Token, cid.Cid, []byte) {
priv, iss := randDID()
_, aud := randDID()
cmd := command.New("foo", "bar")
pol := policy.Policy{policy.All(
selector.MustParse(".[]"),
policy.GreaterThan(selector.MustParse(".value"), literal.Int(2)),
)}
opts := []delegation.Option{
delegation.WithExpiration(time.Now().Add(time.Hour)),
delegation.WithSubject(iss),
}
for i := 0; i < 3; i++ {
opts = append(opts, delegation.WithMeta(randomString(8), randomString(10)))
}
t, err := delegation.New(priv, aud, cmd, pol, opts...)
if err != nil {
panic(err)
}
b, c, err := t.ToSealed(priv)
if err != nil {
panic(err)
}
return t, c, b
}

View File

@@ -1,216 +0,0 @@
package container
import (
"compress/flate"
"compress/gzip"
"encoding/base64"
"fmt"
"io"
"github.com/ipfs/go-cid"
cbor "github.com/ipfs/go-ipld-cbor"
"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/codec/dagcbor"
"github.com/ipld/go-ipld-prime/datamodel"
"github.com/ipld/go-ipld-prime/fluent/qp"
"github.com/ipld/go-ipld-prime/node/basicnode"
"github.com/multiformats/go-multihash"
)
func FromCar(r io.Reader) (Container, error) {
_, it, err := readCar(r)
if err != nil {
return nil, err
}
c := New()
for block, err := range it {
if err != nil {
return nil, err
}
c[block.c] = block.data
}
return c, nil
}
func (ctn Container) ToCar(w io.Writer) error {
return writeCar(w, nil, func(yield func(carBlock) bool) {
for c, bytes := range ctn {
if !yield(carBlock{c: c, data: bytes}) {
return
}
}
})
}
func FromCarBase64(r io.Reader) (Container, error) {
return FromCar(base64.NewDecoder(base64.StdEncoding, r))
}
func (ctn Container) ToCarBase64(w io.Writer) error {
w2 := base64.NewEncoder(base64.StdEncoding, w)
defer w2.Close()
return ctn.ToCar(w2)
}
func FromCarGzip(r io.Reader) (Container, error) {
r2, err := gzip.NewReader(r)
if err != nil {
return nil, err
}
defer r2.Close()
return FromCar(r2)
}
func (ctn Container) ToCarGzip(w io.Writer) error {
w2 := gzip.NewWriter(w)
defer w2.Close()
return ctn.ToCar(w2)
}
func FromCarGzipBase64(r io.Reader) (Container, error) {
return FromCarGzip(base64.NewDecoder(base64.StdEncoding, r))
}
func (ctn Container) ToCarGzipBase64(w io.Writer) error {
w2 := base64.NewEncoder(base64.StdEncoding, w)
defer w2.Close()
return ctn.ToCarGzip(w2)
}
func FromCbor(r io.Reader) (Container, error) {
var raw [][]byte
err := cbor.DecodeReader(r, &raw)
if err != nil {
return nil, err
}
// TODO: the CID computation will likely be handled in the envelope
// TODO: the envelope should likely be able to deserialize arbitrary types based on the tag value
// TODO: the container should likely expose the decoded token, and have search methods (simple, but also DAG reconstruction, graph path search)
cidBuilder := cid.V1Builder{Codec: cid.DagCBOR, MhType: multihash.SHA2_256}
ctn := make(Container, len(raw))
for _, bytes := range raw {
c, err := cidBuilder.Sum(bytes)
if err != nil {
return nil, err
}
ctn[c] = bytes
}
return ctn, nil
}
func FromCbor2(r io.Reader) (Container, error) {
n, err := ipld.DecodeStreaming(r, dagcbor.Decode)
if err != nil {
return nil, err
}
if n.Kind() != datamodel.Kind_List {
return nil, fmt.Errorf("not a list")
}
ctn := make(Container, n.Length())
cidBuilder := cid.V1Builder{Codec: cid.DagCBOR, MhType: multihash.SHA2_256}
it := n.ListIterator()
for !it.Done() {
_, val, err := it.Next()
if err != nil {
return nil, err
}
bytes, err := val.AsBytes()
if err != nil {
return nil, err
}
c, err := cidBuilder.Sum(bytes)
if err != nil {
return nil, err
}
ctn.AddBytes(c, bytes)
}
return ctn, nil
}
func (ctn Container) ToCbor2(w io.Writer) error {
node, err := qp.BuildList(basicnode.Prototype.Any, int64(len(ctn)), func(la datamodel.ListAssembler) {
for _, bytes := range ctn {
qp.ListEntry(la, qp.Bytes(bytes))
}
})
if err != nil {
return err
}
return ipld.EncodeStreaming(w, node, dagcbor.Encode)
}
func (ctn Container) ToCbor(w io.Writer) error {
raw := make([][]byte, 0, len(ctn))
for _, bytes := range ctn {
raw = append(raw, bytes)
}
return cbor.EncodeWriter(raw, w)
}
func FromCborBase64(r io.Reader) (Container, error) {
return FromCbor(base64.NewDecoder(base64.StdEncoding, r))
}
func (ctn Container) ToCborBase64(w io.Writer) error {
w2 := base64.NewEncoder(base64.StdEncoding, w)
defer w2.Close()
return ctn.ToCbor(w2)
}
func FromCborGzip(r io.Reader) (Container, error) {
r2, err := gzip.NewReader(r)
if err != nil {
return nil, err
}
defer r2.Close()
return FromCbor(r2)
}
func (ctn Container) ToCborGzip(w io.Writer) error {
w2 := gzip.NewWriter(w)
defer w2.Close()
return ctn.ToCbor(w2)
}
func FromCborGzipBase64(r io.Reader) (Container, error) {
return FromCborGzip(base64.NewDecoder(base64.StdEncoding, r))
}
func (ctn Container) ToCborGzipBase64(w io.Writer) error {
w2 := base64.NewEncoder(base64.StdEncoding, w)
defer w2.Close()
return ctn.ToCborGzip(w2)
}
func FromCborFlate(r io.Reader) (Container, error) {
r2 := flate.NewReader(r)
defer r2.Close()
return FromCbor(r2)
}
func (ctn Container) ToCborFlate(w io.Writer) error {
w2, err := flate.NewWriter(w, flate.DefaultCompression)
if err != nil {
return err
}
defer w2.Close()
return ctn.ToCbor(w2)
}
func FromCborFlateBase64(r io.Reader) (Container, error) {
return FromCborFlate(base64.NewDecoder(base64.StdEncoding, r))
}
func (ctn Container) ToCborFlateBase64(w io.Writer) error {
w2 := base64.NewEncoder(base64.StdEncoding, w)
defer w2.Close()
return ctn.ToCborFlate(w2)
}

View File

@@ -1,175 +0,0 @@
package container
import (
"bytes"
"crypto/rand"
"fmt"
"io"
"testing"
"time"
"github.com/ipfs/go-cid"
"github.com/libp2p/go-libp2p/core/crypto"
mh "github.com/multiformats/go-multihash"
"github.com/stretchr/testify/require"
"github.com/ucan-wg/go-ucan/did"
"github.com/ucan-wg/go-ucan/pkg/command"
"github.com/ucan-wg/go-ucan/pkg/policy"
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
"github.com/ucan-wg/go-ucan/pkg/policy/selector"
"github.com/ucan-wg/go-ucan/token/delegation"
)
func TestContainerRoundTrip(t *testing.T) {
for _, tc := range []struct {
name string
writer func(ctn Container, w io.Writer) error
reader func(io.Reader) (Container, error)
}{
{"car", Container.ToCar, FromCar},
{"carBase64", Container.ToCarBase64, FromCarBase64},
{"carGzip", Container.ToCarGzip, FromCarGzip},
{"carGzipBase64", Container.ToCarGzipBase64, FromCarGzipBase64},
{"cbor", Container.ToCbor, FromCbor},
{"cborBase64", Container.ToCborBase64, FromCborBase64},
{"cborGzip", Container.ToCborGzip, FromCborGzip},
{"cborGzipBase64", Container.ToCborGzipBase64, FromCborGzipBase64},
{"cborFlate", Container.ToCborFlate, FromCborFlate},
{"cborFlateBase64", Container.ToCborFlateBase64, FromCborFlateBase64},
{"cbor2", Container.ToCbor2, FromCbor2},
} {
t.Run(tc.name, func(t *testing.T) {
ctn := New()
builder := cid.V1Builder{Codec: cid.DagCBOR, MhType: mh.SHA2_256}
var dataSize int
for i := 0; i < 10; i++ {
data := randTokenBytes()
c, err := builder.Sum(data)
require.NoError(t, err)
ctn.AddBytes(c, data)
dataSize += len(data)
}
buf := bytes.NewBuffer(nil)
err := tc.writer(ctn, buf)
require.NoError(t, err)
t.Logf("data size %d", dataSize)
t.Logf("container overhead: %d%%, %d bytes", int(float32(buf.Len()-dataSize)/float32(dataSize)*100.0), buf.Len()-dataSize)
ctn2, err := tc.reader(bytes.NewReader(buf.Bytes()))
require.NoError(t, err)
require.Equal(t, ctn, ctn2)
})
}
}
func BenchmarkContainerSerialisation(b *testing.B) {
for _, tc := range []struct {
name string
writer func(ctn Container, w io.Writer) error
reader func(io.Reader) (Container, error)
}{
{"car", Container.ToCar, FromCar},
{"carBase64", Container.ToCarBase64, FromCarBase64},
{"carGzip", Container.ToCarGzip, FromCarGzip},
{"carGzipBase64", Container.ToCarGzipBase64, FromCarGzipBase64},
{"cbor", Container.ToCbor, FromCbor},
{"cborBase64", Container.ToCborBase64, FromCborBase64},
{"cborGzip", Container.ToCborGzip, FromCborGzip},
{"cborGzipBase64", Container.ToCborGzipBase64, FromCborGzipBase64},
{"cborFlate", Container.ToCborFlate, FromCborFlate},
{"cborFlateBase64", Container.ToCborFlateBase64, FromCborFlateBase64},
{"cbor2", Container.ToCbor2, FromCbor2},
} {
ctn := New()
builder := cid.V1Builder{Codec: cid.DagCBOR, MhType: mh.SHA2_256}
var dataSize int
for i := 0; i < 10; i++ {
data := randTokenBytes()
c, err := builder.Sum(data)
require.NoError(b, err)
ctn.AddBytes(c, data)
dataSize += len(data)
}
buf := bytes.NewBuffer(nil)
_ = tc.writer(ctn, buf)
b.Run(tc.name+"_write", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
buf := bytes.NewBuffer(nil)
_ = tc.writer(ctn, buf)
}
})
b.Run(tc.name+"_read", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, _ = tc.reader(bytes.NewReader(buf.Bytes()))
}
})
}
}
func randBytes(n int) []byte {
b := make([]byte, n)
_, _ = rand.Read(b)
return b
}
func randDID() (crypto.PrivKey, did.DID) {
privKey, _, err := crypto.GenerateEd25519Key(rand.Reader)
if err != nil {
panic(err)
}
d, err := did.FromPrivKey(privKey)
if err != nil {
panic(err)
}
return privKey, d
}
func randomString(length int) string {
b := make([]byte, length/2+1)
rand.Read(b)
return fmt.Sprintf("%x", b)[0:length]
}
func randTokenBytes() []byte {
priv, iss := randDID()
_, aud := randDID()
cmd := command.New("foo", "bar")
pol := policy.Policy{policy.All(
selector.MustParse(".[]"),
policy.GreaterThan(selector.MustParse(".value"), literal.Int(2)),
)}
opts := []delegation.Option{
delegation.WithExpiration(time.Now().Add(time.Hour)),
delegation.WithSubject(iss),
}
for i := 0; i < 3; i++ {
opts = append(opts, delegation.WithMeta(randomString(8), randomString(10)))
}
t, err := delegation.New(priv, aud, cmd, pol, opts...)
if err != nil {
panic(err)
}
b, _, err := t.ToSealed(priv)
if err != nil {
panic(err)
}
return b
}

107
pkg/container/writer.go Normal file
View File

@@ -0,0 +1,107 @@
package container
import (
"compress/flate"
"compress/gzip"
"encoding/base64"
"io"
"github.com/ipfs/go-cid"
cbor "github.com/ipfs/go-ipld-cbor"
"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/codec/dagcbor"
"github.com/ipld/go-ipld-prime/datamodel"
"github.com/ipld/go-ipld-prime/fluent/qp"
"github.com/ipld/go-ipld-prime/node/basicnode"
)
type Writer map[cid.Cid][]byte
func NewWriter() Writer {
return make(Writer)
}
func (ctn Writer) AddSealed(cid cid.Cid, data []byte) {
ctn[cid] = data
}
func (ctn Writer) ToCar(w io.Writer) error {
return writeCar(w, nil, func(yield func(carBlock) bool) {
for c, bytes := range ctn {
if !yield(carBlock{c: c, data: bytes}) {
return
}
}
})
}
func (ctn Writer) ToCarBase64(w io.Writer) error {
w2 := base64.NewEncoder(base64.StdEncoding, w)
defer w2.Close()
return ctn.ToCar(w2)
}
func (ctn Writer) ToCarGzip(w io.Writer) error {
w2 := gzip.NewWriter(w)
defer w2.Close()
return ctn.ToCar(w2)
}
func (ctn Writer) ToCarGzipBase64(w io.Writer) error {
w2 := base64.NewEncoder(base64.StdEncoding, w)
defer w2.Close()
return ctn.ToCarGzip(w2)
}
func (ctn Writer) ToCbor2(w io.Writer) error {
node, err := qp.BuildList(basicnode.Prototype.Any, int64(len(ctn)), func(la datamodel.ListAssembler) {
for _, bytes := range ctn {
qp.ListEntry(la, qp.Bytes(bytes))
}
})
if err != nil {
return err
}
return ipld.EncodeStreaming(w, node, dagcbor.Encode)
}
func (ctn Writer) ToCbor(w io.Writer) error {
raw := make([][]byte, 0, len(ctn))
for _, bytes := range ctn {
raw = append(raw, bytes)
}
return cbor.EncodeWriter(raw, w)
}
func (ctn Writer) ToCborBase64(w io.Writer) error {
w2 := base64.NewEncoder(base64.StdEncoding, w)
defer w2.Close()
return ctn.ToCbor(w2)
}
func (ctn Writer) ToCborGzip(w io.Writer) error {
w2 := gzip.NewWriter(w)
defer w2.Close()
return ctn.ToCbor(w2)
}
func (ctn Writer) ToCborGzipBase64(w io.Writer) error {
w2 := base64.NewEncoder(base64.StdEncoding, w)
defer w2.Close()
return ctn.ToCborGzip(w2)
}
func (ctn Writer) ToCborFlate(w io.Writer) error {
w2, err := flate.NewWriter(w, flate.DefaultCompression)
if err != nil {
return err
}
defer w2.Close()
return ctn.ToCbor(w2)
}
func (ctn Writer) ToCborFlateBase64(w io.Writer) error {
w2 := base64.NewEncoder(base64.StdEncoding, w)
defer w2.Close()
return ctn.ToCborFlate(w2)
}

View File

@@ -123,6 +123,22 @@ func (m *Meta) Add(key string, val any) error {
return nil
}
// Equals tells if two Meta hold the same key/values.
func (m *Meta) Equals(other *Meta) bool {
if len(m.Keys) != len(other.Keys) {
return false
}
if len(m.Values) != len(other.Values) {
return false
}
for _, key := range m.Keys {
if !ipld.DeepEqual(m.Values[key], other.Values[key]) {
return false
}
}
return true
}
func fqtn(val any) string {
var name string