From 60922ced96b54ccedd2e5ebc2248a516874b9353 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Wed, 2 Oct 2024 11:56:32 +0200 Subject: [PATCH] container: split into reader+writer --- pkg/container/container.go | 48 ------- pkg/container/reader.go | 173 ++++++++++++++++++++++ pkg/container/serial_test.go | 189 ++++++++++++++++++++++++ pkg/container/serialization.go | 216 ---------------------------- pkg/container/serialization_test.go | 175 ---------------------- pkg/container/writer.go | 107 ++++++++++++++ pkg/meta/meta.go | 16 +++ 7 files changed, 485 insertions(+), 439 deletions(-) delete mode 100644 pkg/container/container.go create mode 100644 pkg/container/reader.go create mode 100644 pkg/container/serial_test.go delete mode 100644 pkg/container/serialization.go delete mode 100644 pkg/container/serialization_test.go create mode 100644 pkg/container/writer.go diff --git a/pkg/container/container.go b/pkg/container/container.go deleted file mode 100644 index 472b8c5..0000000 --- a/pkg/container/container.go +++ /dev/null @@ -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") -} diff --git a/pkg/container/reader.go b/pkg/container/reader.go new file mode 100644 index 0000000..15f47b5 --- /dev/null +++ b/pkg/container/reader.go @@ -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 +} diff --git a/pkg/container/serial_test.go b/pkg/container/serial_test.go new file mode 100644 index 0000000..76e8d74 --- /dev/null +++ b/pkg/container/serial_test.go @@ -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 +} diff --git a/pkg/container/serialization.go b/pkg/container/serialization.go deleted file mode 100644 index c802891..0000000 --- a/pkg/container/serialization.go +++ /dev/null @@ -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) -} diff --git a/pkg/container/serialization_test.go b/pkg/container/serialization_test.go deleted file mode 100644 index 14ac11d..0000000 --- a/pkg/container/serialization_test.go +++ /dev/null @@ -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 -} diff --git a/pkg/container/writer.go b/pkg/container/writer.go new file mode 100644 index 0000000..28a9dc1 --- /dev/null +++ b/pkg/container/writer.go @@ -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) +} diff --git a/pkg/meta/meta.go b/pkg/meta/meta.go index 9dd8dbb..f3ea126 100644 --- a/pkg/meta/meta.go +++ b/pkg/meta/meta.go @@ -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