From e388bead4e9c626e3bd42ba413c631c9fce4cc74 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Tue, 29 Sep 2026 17:47:26 +0100 Subject: [PATCH 1/4] feat: type changes for trailer hash --- commands/blob/abort_test.go | 12 +- commands/blob/accept.go | 10 + commands/blob/cbor_gen.go | 317 ++++++++++++++++++++++++++--- commands/blob/codec.go | 225 +++++++++++++++++++++ commands/blob/gen/main.go | 4 +- commands/blob/json_gen.go | 334 +++++++++++++++++++++++++++---- commands/blob/spec_test.go | 135 +++++++++++++ commands/blob/types.go | 68 ++++++- commands/http/cbor_gen.go | 227 ++++++++++++++++++++- commands/http/gen/main.go | 2 + commands/http/json_gen.go | 215 +++++++++++++++++++- commands/http/put.go | 3 - commands/http/put_test.go | 41 ++++ commands/http/types.go | 16 +- commands/internal/codec/codec.go | 57 ++++++ 15 files changed, 1576 insertions(+), 90 deletions(-) create mode 100644 commands/blob/codec.go create mode 100644 commands/blob/spec_test.go create mode 100644 commands/http/put_test.go diff --git a/commands/blob/abort_test.go b/commands/blob/abort_test.go index 19ac4c6..8aef985 100644 --- a/commands/blob/abort_test.go +++ b/commands/blob/abort_test.go @@ -43,14 +43,14 @@ func TestReleaseArgumentsRoundTrip(t *testing.T) { // Round-trips RejectArguments through cbor. func TestRejectArgumentsRoundTrip(t *testing.T) { - in := blob.RejectArguments{ - Space: testutil.RandomDID(t), - Digest: testutil.RandomMultihash(t), - } + space, digest := testutil.RandomDID(t), testutil.RandomMultihash(t) + in := blob.RejectByDigest(space, digest) var buf bytes.Buffer require.NoError(t, in.MarshalCBOR(&buf)) var out blob.RejectArguments require.NoError(t, out.UnmarshalCBOR(&buf)) - require.Equal(t, in.Space, out.Space) - require.Equal(t, in.Digest, out.Digest) + require.Equal(t, space, out.Space()) + got, ok := out.Digest() + require.True(t, ok) + require.Equal(t, digest, got) } diff --git a/commands/blob/accept.go b/commands/blob/accept.go index 9494302..62ccd54 100644 --- a/commands/blob/accept.go +++ b/commands/blob/accept.go @@ -4,7 +4,17 @@ package blob import ( "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/errors" "github.com/fil-forge/ucantone/ucan/command" ) var Accept = binding.Bind[*AcceptArguments, *AcceptOK](command.MustParse("/blob/accept")) + +// BlobDigestMismatchErrorName is the stable receipt-failure name when the +// digest the storage node computed for the received data differs from the +// digest in the accept arguments or, when they name only a digest code, the +// digest in the `/http/put` receipt. +const BlobDigestMismatchErrorName = "BlobDigestMismatch" + +// ErrBlobDigestMismatch is the failure for a digest mismatch. +var ErrBlobDigestMismatch = errors.New(BlobDigestMismatchErrorName, "received data does not hash to the reported digest") diff --git a/commands/blob/cbor_gen.go b/commands/blob/cbor_gen.go index b870181..655d99b 100644 --- a/commands/blob/cbor_gen.go +++ b/commands/blob/cbor_gen.go @@ -33,7 +33,7 @@ func (t *AllocateArguments) MarshalCBOR(w io.Writer) error { return err } - // t.Blob (blob.Blob) (struct) + // t.Blob (blob.BlobSpec) (struct) if len("blob") > 8192 { return xerrors.Errorf("Value in field \"blob\" was too long") } @@ -124,7 +124,7 @@ func (t *AllocateArguments) UnmarshalCBOR(r io.Reader) (err error) { } switch string(nameBuf[:nameLen]) { - // t.Blob (blob.Blob) (struct) + // t.Blob (blob.BlobSpec) (struct) case "blob": { @@ -312,6 +312,135 @@ func (t *Blob) UnmarshalCBOR(r io.Reader) (err error) { return nil } +func (t *BlobDigestCode) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{162}); err != nil { + return err + } + + // t.Size (uint64) (uint64) + if len("size") > 8192 { + return xerrors.Errorf("Value in field \"size\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("size"))); err != nil { + return err + } + if _, err := cw.WriteString(string("size")); err != nil { + return err + } + + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.Size)); err != nil { + return err + } + + // t.DigestCode (uint64) (uint64) + if len("digestCode") > 8192 { + return xerrors.Errorf("Value in field \"digestCode\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("digestCode"))); err != nil { + return err + } + if _, err := cw.WriteString(string("digestCode")); err != nil { + return err + } + + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.DigestCode)); err != nil { + return err + } + + return nil +} + +func (t *BlobDigestCode) UnmarshalCBOR(r io.Reader) (err error) { + *t = BlobDigestCode{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("BlobDigestCode: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 10) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Size (uint64) (uint64) + case "size": + + { + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajUnsignedInt { + return fmt.Errorf("wrong type for uint64 field") + } + t.Size = uint64(extra) + + } + // t.DigestCode (uint64) (uint64) + case "digestCode": + + { + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajUnsignedInt { + return fmt.Errorf("wrong type for uint64 field") + } + t.DigestCode = uint64(extra) + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} func (t *AllocateOK) MarshalCBOR(w io.Writer) error { if t == nil { _, err := w.Write(cbg.CborNull) @@ -718,7 +847,7 @@ func (t *AcceptArguments) MarshalCBOR(w io.Writer) error { return err } - // t.Blob (blob.Blob) (struct) + // t.Blob (blob.BlobSpec) (struct) if len("blob") > 8192 { return xerrors.Errorf("Value in field \"blob\" was too long") } @@ -803,7 +932,7 @@ func (t *AcceptArguments) UnmarshalCBOR(r io.Reader) (err error) { } } - // t.Blob (blob.Blob) (struct) + // t.Blob (blob.BlobSpec) (struct) case "blob": { @@ -968,7 +1097,7 @@ func (t *AddArguments) MarshalCBOR(w io.Writer) error { return err } - // t.Blob (blob.Blob) (struct) + // t.Blob (blob.BlobSpec) (struct) if len("blob") > 8192 { return xerrors.Errorf("Value in field \"blob\" was too long") } @@ -1027,7 +1156,7 @@ func (t *AddArguments) UnmarshalCBOR(r io.Reader) (err error) { } switch string(nameBuf[:nameLen]) { - // t.Blob (blob.Blob) (struct) + // t.Blob (blob.BlobSpec) (struct) case "blob": { @@ -1428,8 +1557,13 @@ func (t *AbortArguments) MarshalCBOR(w io.Writer) error { } cw := cbg.NewCborWriter(w) + fieldCount := 2 - if _, err := cw.Write([]byte{162}); err != nil { + if t.Digest == nil { + fieldCount-- + } + + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { return err } @@ -1450,29 +1584,32 @@ func (t *AbortArguments) MarshalCBOR(w io.Writer) error { } // t.Digest (multihash.Multihash) (slice) - if len("digest") > 8192 { - return xerrors.Errorf("Value in field \"digest\" was too long") - } + if t.Digest != nil { - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("digest"))); err != nil { - return err - } - if _, err := cw.WriteString(string("digest")); err != nil { - return err - } + if len("digest") > 8192 { + return xerrors.Errorf("Value in field \"digest\" was too long") + } - if len(t.Digest) > 2097152 { - return xerrors.Errorf("Byte array in field t.Digest was too long") - } + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("digest"))); err != nil { + return err + } + if _, err := cw.WriteString(string("digest")); err != nil { + return err + } - if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.Digest))); err != nil { - return err - } + if len(t.Digest) > 2097152 { + return xerrors.Errorf("Byte array in field t.Digest was too long") + } - if _, err := cw.Write(t.Digest); err != nil { - return err - } + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.Digest))); err != nil { + return err + } + if _, err := cw.Write(t.Digest); err != nil { + return err + } + + } return nil } @@ -1563,7 +1700,7 @@ func (t *AbortArguments) UnmarshalCBOR(r io.Reader) (err error) { return nil } -func (t *RejectArguments) MarshalCBOR(w io.Writer) error { +func (t *RejectDigestArguments) MarshalCBOR(w io.Writer) error { if t == nil { _, err := w.Write(cbg.CborNull) return err @@ -1618,8 +1755,8 @@ func (t *RejectArguments) MarshalCBOR(w io.Writer) error { return nil } -func (t *RejectArguments) UnmarshalCBOR(r io.Reader) (err error) { - *t = RejectArguments{} +func (t *RejectDigestArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = RejectDigestArguments{} cr := cbg.NewCborReader(r) @@ -1638,7 +1775,7 @@ func (t *RejectArguments) UnmarshalCBOR(r io.Reader) (err error) { } if extra > cbg.MaxLength { - return fmt.Errorf("RejectArguments: map struct too large (%d)", extra) + return fmt.Errorf("RejectDigestArguments: map struct too large (%d)", extra) } n := extra @@ -1702,6 +1839,128 @@ func (t *RejectArguments) UnmarshalCBOR(r io.Reader) (err error) { return nil } +func (t *RejectAllocationArguments) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{162}); err != nil { + return err + } + + // t.Space (did.DID) (struct) + if len("space") > 8192 { + return xerrors.Errorf("Value in field \"space\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("space"))); err != nil { + return err + } + if _, err := cw.WriteString(string("space")); err != nil { + return err + } + + if err := t.Space.MarshalCBOR(cw); err != nil { + return err + } + + // t.Allocation (cid.Cid) (struct) + if len("allocation") > 8192 { + return xerrors.Errorf("Value in field \"allocation\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("allocation"))); err != nil { + return err + } + if _, err := cw.WriteString(string("allocation")); err != nil { + return err + } + + if err := cbg.WriteCid(cw, t.Allocation); err != nil { + return xerrors.Errorf("failed to write cid field t.Allocation: %w", err) + } + + return nil +} + +func (t *RejectAllocationArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = RejectAllocationArguments{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("RejectAllocationArguments: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 10) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Space (did.DID) (struct) + case "space": + + { + + if err := t.Space.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Space: %w", err) + } + + } + // t.Allocation (cid.Cid) (struct) + case "allocation": + + { + + c, err := cbg.ReadCid(cr) + if err != nil { + return xerrors.Errorf("failed to read cid field t.Allocation: %w", err) + } + + t.Allocation = c + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} func (t *ReplicateArguments) MarshalCBOR(w io.Writer) error { if t == nil { _, err := w.Write(cbg.CborNull) diff --git a/commands/blob/codec.go b/commands/blob/codec.go new file mode 100644 index 0000000..6a950b6 --- /dev/null +++ b/commands/blob/codec.go @@ -0,0 +1,225 @@ +//go:build !codegen + +package blob + +import ( + "bytes" + "io" + + "github.com/fil-forge/libforge/commands/internal/codec" + "github.com/fil-forge/ucantone/did" + "github.com/fil-forge/ucantone/errors" + "github.com/ipfs/go-cid" + "github.com/multiformats/go-multihash" +) + +// UnsupportedDigestCodeErrorName is the stable receipt-failure name when a +// `/blob/add` or `/blob/allocate` names a digest code the executor does not +// support, or the executor does not support adding a blob by digest code at +// all. A client that receives it can fall back to computing the digest before +// adding the blob. +const UnsupportedDigestCodeErrorName = "UnsupportedDigestCode" + +// ErrUnsupportedDigestCode is the failure for an unsupported digest code. +var ErrUnsupportedDigestCode = errors.New(UnsupportedDigestCodeErrorName, "adding a blob by digest code is supported only for sha2-256") + +var errNoVariant = errors.New("InvalidUnion", "union holds no variant") + +// SpecFromBlob returns the spec of a blob whose digest is known. +func SpecFromBlob(b Blob) BlobSpec { + return BlobSpec{blob: &b} +} + +// SpecFromDigestCode returns the spec of a blob of size bytes whose digest is +// to be computed with the multihash function code. +func SpecFromDigestCode(code, size uint64) BlobSpec { + return BlobSpec{code: &BlobDigestCode{DigestCode: code, Size: size}} +} + +// Blob returns the blob with its digest, when the spec names one. +func (s BlobSpec) Blob() (Blob, bool) { + if s.blob == nil { + return Blob{}, false + } + return *s.blob, true +} + +// DigestCode returns the hash function and size, when the spec names a digest +// code instead of a digest. +func (s BlobSpec) DigestCode() (BlobDigestCode, bool) { + if s.code == nil { + return BlobDigestCode{}, false + } + return *s.code, true +} + +// Size returns the size of the blob, whichever variant the spec holds. +func (s BlobSpec) Size() uint64 { + switch { + case s.blob != nil: + return s.blob.Size + case s.code != nil: + return s.code.Size + } + return 0 +} + +func (s BlobSpec) MarshalCBOR(w io.Writer) error { + switch { + case s.blob != nil && s.code == nil: + return s.blob.MarshalCBOR(w) + case s.code != nil && s.blob == nil: + return s.code.MarshalCBOR(w) + } + return errNoVariant +} + +func (s *BlobSpec) UnmarshalCBOR(r io.Reader) error { + key, raw, err := codec.ReadCborVariant(r, "digest", "digestCode") + if err != nil { + return err + } + return s.decode(key, raw, false) +} + +func (s BlobSpec) MarshalDagJSON(w io.Writer) error { + switch { + case s.blob != nil && s.code == nil: + return s.blob.MarshalDagJSON(w) + case s.code != nil && s.blob == nil: + return s.code.MarshalDagJSON(w) + } + return errNoVariant +} + +func (s *BlobSpec) UnmarshalDagJSON(r io.Reader) error { + key, raw, err := codec.ReadDagJSONVariant(r, "digest", "digestCode") + if err != nil { + return err + } + return s.decode(key, raw, true) +} + +func (s *BlobSpec) decode(key string, raw []byte, json bool) error { + *s = BlobSpec{} + if key == "digest" { + var b Blob + if err := unmarshalVariant(&b, raw, json); err != nil { + return err + } + s.blob = &b + return nil + } + var c BlobDigestCode + if err := unmarshalVariant(&c, raw, json); err != nil { + return err + } + s.code = &c + return nil +} + +// RejectByDigest returns arguments rejecting space's allocation for the blob +// with digest. +func RejectByDigest(space did.DID, digest multihash.Multihash) RejectArguments { + return RejectArguments{byDigest: &RejectDigestArguments{Space: space, Digest: digest}} +} + +// RejectByAllocation returns arguments rejecting space's allocation made +// without a digest by the `/blob/allocate` task allocation. +func RejectByAllocation(space did.DID, allocation cid.Cid) RejectArguments { + return RejectArguments{byAllocation: &RejectAllocationArguments{Space: space, Allocation: allocation}} +} + +// Space returns the space whose allocation is rejected. +func (a RejectArguments) Space() did.DID { + switch { + case a.byDigest != nil: + return a.byDigest.Space + case a.byAllocation != nil: + return a.byAllocation.Space + } + return did.DID{} +} + +// Digest returns the digest of the rejected blob, when the arguments name one. +func (a RejectArguments) Digest() (multihash.Multihash, bool) { + if a.byDigest == nil { + return nil, false + } + return a.byDigest.Digest, true +} + +// Allocation returns the `/blob/allocate` task of the rejected allocation, +// when the arguments name one instead of a digest. +func (a RejectArguments) Allocation() (cid.Cid, bool) { + if a.byAllocation == nil { + return cid.Undef, false + } + return a.byAllocation.Allocation, true +} + +func (a RejectArguments) MarshalCBOR(w io.Writer) error { + switch { + case a.byDigest != nil && a.byAllocation == nil: + return a.byDigest.MarshalCBOR(w) + case a.byAllocation != nil && a.byDigest == nil: + return a.byAllocation.MarshalCBOR(w) + } + return errNoVariant +} + +func (a *RejectArguments) UnmarshalCBOR(r io.Reader) error { + key, raw, err := codec.ReadCborVariant(r, "digest", "allocation") + if err != nil { + return err + } + return a.decode(key, raw, false) +} + +func (a RejectArguments) MarshalDagJSON(w io.Writer) error { + switch { + case a.byDigest != nil && a.byAllocation == nil: + return a.byDigest.MarshalDagJSON(w) + case a.byAllocation != nil && a.byDigest == nil: + return a.byAllocation.MarshalDagJSON(w) + } + return errNoVariant +} + +func (a *RejectArguments) UnmarshalDagJSON(r io.Reader) error { + key, raw, err := codec.ReadDagJSONVariant(r, "digest", "allocation") + if err != nil { + return err + } + return a.decode(key, raw, true) +} + +func (a *RejectArguments) decode(key string, raw []byte, json bool) error { + *a = RejectArguments{} + if key == "digest" { + var d RejectDigestArguments + if err := unmarshalVariant(&d, raw, json); err != nil { + return err + } + a.byDigest = &d + return nil + } + var al RejectAllocationArguments + if err := unmarshalVariant(&al, raw, json); err != nil { + return err + } + a.byAllocation = &al + return nil +} + +type variant interface { + UnmarshalCBOR(io.Reader) error + UnmarshalDagJSON(io.Reader) error +} + +func unmarshalVariant(v variant, raw []byte, json bool) error { + if json { + return v.UnmarshalDagJSON(bytes.NewReader(raw)) + } + return v.UnmarshalCBOR(bytes.NewReader(raw)) +} diff --git a/commands/blob/gen/main.go b/commands/blob/gen/main.go index 76b50a3..fb13d50 100644 --- a/commands/blob/gen/main.go +++ b/commands/blob/gen/main.go @@ -26,6 +26,7 @@ func main() { models := []any{ blob.AllocateArguments{}, blob.Blob{}, + blob.BlobDigestCode{}, blob.AllocateOK{}, blob.BlobAddress{}, blob.AcceptArguments{}, @@ -35,7 +36,8 @@ func main() { blob.RemoveArguments{}, blob.ReleaseArguments{}, blob.AbortArguments{}, - blob.RejectArguments{}, + blob.RejectDigestArguments{}, + blob.RejectAllocationArguments{}, blob.ReplicateArguments{}, blob.ReplicateOK{}, blob.ListArguments{}, diff --git a/commands/blob/json_gen.go b/commands/blob/json_gen.go index 11d0010..9d368ba 100644 --- a/commands/blob/json_gen.go +++ b/commands/blob/json_gen.go @@ -32,7 +32,7 @@ func (t *AllocateArguments) MarshalDagJSON(w io.Writer) error { } written := false - // t.Blob (blob.Blob) (struct) + // t.Blob (blob.BlobSpec) (struct) if len("blob") > 8192 { return fmt.Errorf("string in field \"blob\" was too long") } @@ -126,7 +126,7 @@ func (t *AllocateArguments) UnmarshalDagJSON(r io.Reader) (err error) { } switch name { - // t.Blob (blob.Blob) (struct) + // t.Blob (blob.BlobSpec) (struct) case "blob": if err := t.Blob.UnmarshalDagJSON(jr); err != nil { @@ -313,6 +313,138 @@ func (t *Blob) UnmarshalDagJSON(r io.Reader) (err error) { return nil } +func (t *BlobDigestCode) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + written := false + + // t.DigestCode (uint64) (uint64) + if len("digestCode") > 8192 { + return fmt.Errorf("string in field \"digestCode\" was too long") + } + if err := jw.WriteString(string("digestCode")); err != nil { + return fmt.Errorf("writing string for field \"digestCode\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + + if err := jw.WriteUint64(uint64(t.DigestCode)); err != nil { + return fmt.Errorf("writing uint64 for field t.DigestCode: %w", err) + } + + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Size (uint64) (uint64) + if len("size") > 8192 { + return fmt.Errorf("string in field \"size\" was too long") + } + if err := jw.WriteString(string("size")); err != nil { + return fmt.Errorf("writing string for field \"size\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + + if err := jw.WriteUint64(uint64(t.Size)); err != nil { + return fmt.Errorf("writing uint64 for field t.Size: %w", err) + } + + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *BlobDigestCode) UnmarshalDagJSON(r io.Reader) (err error) { + *t = BlobDigestCode{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for BlobDigestCode: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for BlobDigestCode: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for BlobDigestCode: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field BlobDigestCode: string too large") + } + return fmt.Errorf("reading string for field BlobDigestCode: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field BlobDigestCode: %w", err) + } + switch name { + + // t.DigestCode (uint64) (uint64) + case "digestCode": + { + + nval, err := jr.ReadNumberAsUint64() + if err != nil { + return fmt.Errorf("reading uint64 for field t.DigestCode: %w", err) + } + t.DigestCode = uint64(nval) + + } + + // t.Size (uint64) (uint64) + case "size": + { + + nval, err := jr.ReadNumberAsUint64() + if err != nil { + return fmt.Errorf("reading uint64 for field t.Size: %w", err) + } + t.Size = uint64(nval) + + } + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for BlobDigestCode: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field BlobDigestCode: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for BlobDigestCode") + } + } + } + + return nil +} func (t *AllocateOK) MarshalDagJSON(w io.Writer) error { jw := jsg.NewDagJsonWriter(w) if t == nil { @@ -724,7 +856,7 @@ func (t *AcceptArguments) MarshalDagJSON(w io.Writer) error { } } - // t.Blob (blob.Blob) (struct) + // t.Blob (blob.BlobSpec) (struct) if len("blob") > 8192 { return fmt.Errorf("string in field \"blob\" was too long") } @@ -803,7 +935,7 @@ func (t *AcceptArguments) UnmarshalDagJSON(r io.Reader) (err error) { return fmt.Errorf("unmarshaling t.Put: %w", err) } - // t.Blob (blob.Blob) (struct) + // t.Blob (blob.BlobSpec) (struct) case "blob": if err := t.Blob.UnmarshalDagJSON(jr); err != nil { @@ -974,7 +1106,7 @@ func (t *AddArguments) MarshalDagJSON(w io.Writer) error { return err } - // t.Blob (blob.Blob) (struct) + // t.Blob (blob.BlobSpec) (struct) if len("blob") > 8192 { return fmt.Errorf("string in field \"blob\" was too long") } @@ -1026,7 +1158,7 @@ func (t *AddArguments) UnmarshalDagJSON(r io.Reader) (err error) { } switch name { - // t.Blob (blob.Blob) (struct) + // t.Blob (blob.BlobSpec) (struct) case "blob": if err := t.Blob.UnmarshalDagJSON(jr); err != nil { @@ -1445,30 +1577,34 @@ func (t *AbortArguments) MarshalDagJSON(w io.Writer) error { } written = true - if written { - if err := jw.WriteComma(); err != nil { - return err + if t.Digest != nil { + if written { + if err := jw.WriteComma(); err != nil { + return err + } } } // t.Digest (multihash.Multihash) (slice) - if len("digest") > 8192 { - return fmt.Errorf("string in field \"digest\" was too long") - } - if err := jw.WriteString(string("digest")); err != nil { - return fmt.Errorf("writing string for field \"digest\": %w", err) - } - if err := jw.WriteObjectColon(); err != nil { - return err - } - if len(t.Digest) > 2097152 { - return fmt.Errorf("byte array in field t.Digest was too long") - } + if t.Digest != nil { + if len("digest") > 8192 { + return fmt.Errorf("string in field \"digest\" was too long") + } + if err := jw.WriteString(string("digest")); err != nil { + return fmt.Errorf("writing string for field \"digest\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if len(t.Digest) > 2097152 { + return fmt.Errorf("byte array in field t.Digest was too long") + } - if err := jw.WriteBytes(t.Digest); err != nil { - return fmt.Errorf("writing bytes for field t.Digest: %w", err) - } + if err := jw.WriteBytes(t.Digest); err != nil { + return fmt.Errorf("writing bytes for field t.Digest: %w", err) + } + } if err := jw.WriteObjectClose(); err != nil { return err } @@ -1558,7 +1694,7 @@ func (t *AbortArguments) UnmarshalDagJSON(r io.Reader) (err error) { return nil } -func (t *RejectArguments) MarshalDagJSON(w io.Writer) error { +func (t *RejectDigestArguments) MarshalDagJSON(w io.Writer) error { jw := jsg.NewDagJsonWriter(w) if t == nil { err := jw.WriteNull() @@ -1612,8 +1748,8 @@ func (t *RejectArguments) MarshalDagJSON(w io.Writer) error { } return nil } -func (t *RejectArguments) UnmarshalDagJSON(r io.Reader) (err error) { - *t = RejectArguments{} +func (t *RejectDigestArguments) UnmarshalDagJSON(r io.Reader) (err error) { + *t = RejectDigestArguments{} jr := jsg.NewDagJsonReader(r) defer func() { @@ -1622,27 +1758,27 @@ func (t *RejectArguments) UnmarshalDagJSON(r io.Reader) (err error) { } }() if err := jr.ReadObjectOpen(); err != nil { - return fmt.Errorf("reading object open for RejectArguments: %w", err) + return fmt.Errorf("reading object open for RejectDigestArguments: %w", err) } close, err := jr.PeekObjectClose() if err != nil { - return fmt.Errorf("peeking object close for RejectArguments: %w", err) + return fmt.Errorf("peeking object close for RejectDigestArguments: %w", err) } if close { if err := jr.ReadObjectClose(); err != nil { - return fmt.Errorf("reading object close for RejectArguments: %w", err) + return fmt.Errorf("reading object close for RejectDigestArguments: %w", err) } } else { for i := uint64(0); i < 8192; i++ { name, err := jr.ReadString(8192) if err != nil { if errors.Is(err, jsg.ErrLimitExceeded) { - return fmt.Errorf("reading string for field RejectArguments: string too large") + return fmt.Errorf("reading string for field RejectDigestArguments: string too large") } - return fmt.Errorf("reading string for field RejectArguments: %w", err) + return fmt.Errorf("reading string for field RejectDigestArguments: %w", err) } if err := jr.ReadObjectColon(); err != nil { - return fmt.Errorf("reading object colon for field RejectArguments: %w", err) + return fmt.Errorf("reading object colon for field RejectDigestArguments: %w", err) } switch name { @@ -1672,19 +1808,145 @@ func (t *RejectArguments) UnmarshalDagJSON(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it if err := jr.DiscardType(); err != nil { - return fmt.Errorf("ignoring field %s for RejectArguments: %w", name, err) + return fmt.Errorf("ignoring field %s for RejectDigestArguments: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field RejectDigestArguments: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for RejectDigestArguments") + } + } + } + + return nil +} +func (t *RejectAllocationArguments) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + written := false + + // t.Allocation (cid.Cid) (struct) + if len("allocation") > 8192 { + return fmt.Errorf("string in field \"allocation\" was too long") + } + if err := jw.WriteString(string("allocation")); err != nil { + return fmt.Errorf("writing string for field \"allocation\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + + if err := jw.WriteCid(t.Allocation); err != nil { + return fmt.Errorf("writing CID for field t.Allocation: %w", err) + } + + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Space (did.DID) (struct) + if len("space") > 8192 { + return fmt.Errorf("string in field \"space\" was too long") + } + if err := jw.WriteString(string("space")); err != nil { + return fmt.Errorf("writing string for field \"space\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.Space.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.Space: %w", err) + } + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *RejectAllocationArguments) UnmarshalDagJSON(r io.Reader) (err error) { + *t = RejectAllocationArguments{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for RejectAllocationArguments: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for RejectAllocationArguments: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for RejectAllocationArguments: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field RejectAllocationArguments: string too large") + } + return fmt.Errorf("reading string for field RejectAllocationArguments: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field RejectAllocationArguments: %w", err) + } + switch name { + + // t.Allocation (cid.Cid) (struct) + case "allocation": + { + + c, err := jr.ReadCid() + if err != nil { + return fmt.Errorf("reading CID for field t.Allocation: %w", err) + } + t.Allocation = c + + } + + // t.Space (did.DID) (struct) + case "space": + + if err := t.Space.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.Space: %w", err) + } + + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for RejectAllocationArguments: %w", name, err) } } close, err := jr.ReadObjectCloseOrComma() if err != nil { - return fmt.Errorf("reading object close or comma for field RejectArguments: %w", err) + return fmt.Errorf("reading object close or comma for field RejectAllocationArguments: %w", err) } if close { break } if i == 8192-1 { - return fmt.Errorf("map too large for RejectArguments") + return fmt.Errorf("map too large for RejectAllocationArguments") } } } diff --git a/commands/blob/spec_test.go b/commands/blob/spec_test.go new file mode 100644 index 0000000..dd60830 --- /dev/null +++ b/commands/blob/spec_test.go @@ -0,0 +1,135 @@ +//go:build !codegen + +package blob_test + +import ( + "bytes" + "strings" + "testing" + + "github.com/fil-forge/libforge/commands/blob" + "github.com/fil-forge/libforge/testutil" + "github.com/multiformats/go-multicodec" + "github.com/stretchr/testify/require" +) + +// A spec with a digest encodes exactly as a Blob does, so invocations that +// name a digest keep their task links. +func TestBlobSpecWithDigestEncodesAsBlob(t *testing.T) { + b := blob.Blob{Digest: testutil.RandomMultihash(t), Size: 1024} + spec := blob.SpecFromBlob(b) + + var wantCBOR, gotCBOR bytes.Buffer + require.NoError(t, b.MarshalCBOR(&wantCBOR)) + require.NoError(t, spec.MarshalCBOR(&gotCBOR)) + require.Equal(t, wantCBOR.Bytes(), gotCBOR.Bytes()) + + var wantJSON, gotJSON bytes.Buffer + require.NoError(t, b.MarshalDagJSON(&wantJSON)) + require.NoError(t, spec.MarshalDagJSON(&gotJSON)) + require.Equal(t, wantJSON.String(), gotJSON.String()) +} + +func TestBlobSpecRoundTrip(t *testing.T) { + b := blob.Blob{Digest: testutil.RandomMultihash(t), Size: 7} + code := uint64(multicodec.Sha2_256) + for name, in := range map[string]blob.BlobSpec{ + "digest": blob.SpecFromBlob(b), + "digest code": blob.SpecFromDigestCode(code, 2097152), + } { + t.Run(name, func(t *testing.T) { + var buf bytes.Buffer + require.NoError(t, in.MarshalCBOR(&buf)) + var out blob.BlobSpec + require.NoError(t, out.UnmarshalCBOR(&buf)) + require.Equal(t, in, out) + + var js bytes.Buffer + require.NoError(t, in.MarshalDagJSON(&js)) + var outJS blob.BlobSpec + require.NoError(t, outJS.UnmarshalDagJSON(&js)) + require.Equal(t, in, outJS) + }) + } + + gotBlob, ok := blob.SpecFromBlob(b).Blob() + require.True(t, ok) + require.Equal(t, b, gotBlob) + gotCode, ok := blob.SpecFromDigestCode(code, 9).DigestCode() + require.True(t, ok) + require.Equal(t, blob.BlobDigestCode{DigestCode: code, Size: 9}, gotCode) + require.EqualValues(t, 9, blob.SpecFromDigestCode(code, 9).Size()) +} + +// A spec travels inside the arguments' generated codecs: the nested union +// round-trips in both encodings. +func TestAddArgumentsWithDigestCodeRoundTrip(t *testing.T) { + in := blob.AddArguments{Blob: blob.SpecFromDigestCode(uint64(multicodec.Sha2_256), 42)} + var buf bytes.Buffer + require.NoError(t, in.MarshalCBOR(&buf)) + var out blob.AddArguments + require.NoError(t, out.UnmarshalCBOR(&buf)) + require.Equal(t, in, out) + + var js bytes.Buffer + require.NoError(t, in.MarshalDagJSON(&js)) + require.Equal(t, `{"blob":{"digestCode":18,"size":42}}`, js.String()) + var outJS blob.AddArguments + require.NoError(t, outJS.UnmarshalDagJSON(&js)) + require.Equal(t, in, outJS) +} + +// A spec holding neither variant cannot be encoded, and an encoding holding +// neither or both cannot be decoded. +func TestBlobSpecRejectsInvalidUnion(t *testing.T) { + var buf bytes.Buffer + require.Error(t, blob.BlobSpec{}.MarshalCBOR(&buf)) + require.Error(t, blob.BlobSpec{}.MarshalDagJSON(&buf)) + + for name, js := range map[string]string{ + "neither": `{"size":1}`, + "both": `{"digest":{"/":{"bytes":"EiA"}},"digestCode":18,"size":1}`, + } { + t.Run(name, func(t *testing.T) { + var out blob.BlobSpec + require.Error(t, out.UnmarshalDagJSON(strings.NewReader(js))) + }) + } +} + +func TestRejectArgumentsByAllocation(t *testing.T) { + space, alloc := testutil.RandomDID(t), testutil.RandomCID(t) + in := blob.RejectByAllocation(space, alloc) + + var buf bytes.Buffer + require.NoError(t, in.MarshalCBOR(&buf)) + var out blob.RejectArguments + require.NoError(t, out.UnmarshalCBOR(&buf)) + require.Equal(t, space, out.Space()) + _, hasDigest := out.Digest() + require.False(t, hasDigest) + got, ok := out.Allocation() + require.True(t, ok) + require.Equal(t, alloc, got) + + var js bytes.Buffer + require.NoError(t, in.MarshalDagJSON(&js)) + var outJS blob.RejectArguments + require.NoError(t, outJS.UnmarshalDagJSON(&js)) + require.Equal(t, in, outJS) + + require.Error(t, blob.RejectArguments{}.MarshalCBOR(&buf)) + var bad blob.RejectArguments + require.Error(t, bad.UnmarshalDagJSON(strings.NewReader(`{"space":"did:key:z6Mk"}`))) +} + +// An abort names only the cause when the add named only a digest code. +func TestAbortArgumentsWithoutDigest(t *testing.T) { + in := blob.AbortArguments{Cause: testutil.RandomCID(t)} + var buf bytes.Buffer + require.NoError(t, in.MarshalCBOR(&buf)) + var out blob.AbortArguments + require.NoError(t, out.UnmarshalCBOR(&buf)) + require.Empty(t, out.Digest) + require.Equal(t, in.Cause, out.Cause) +} diff --git a/commands/blob/types.go b/commands/blob/types.go index 27cb5ca..aa91d2c 100644 --- a/commands/blob/types.go +++ b/commands/blob/types.go @@ -13,8 +13,32 @@ type Blob struct { Size uint64 `cborgen:"size" dagjsongen:"size"` } +// BlobSpec describes a blob in the arguments of the capabilities that can +// add a blob before its digest is known: `/blob/add`, `/blob/allocate`, +// `/blob/accept` and the body of `/http/put`. It is a union of exactly one +// of: +// +// - [Blob]: the blob's digest and size, when the digest is known in advance; +// - [BlobDigestCode]: the code of the hash function and the size, when the +// digest is computed by both parties as the data streams. +// +// Encoding or decoding a spec that holds neither or both fails. The codec is +// hand-written (codec.go); the variants' codecs are generated. +type BlobSpec struct { + blob *Blob + code *BlobDigestCode +} + +// BlobDigestCode describes a blob whose digest is not known in advance: the +// multicodec code of the multihash function it is to be hashed with, and its +// size. +type BlobDigestCode struct { + DigestCode uint64 `cborgen:"digestCode" dagjsongen:"digestCode"` + Size uint64 `cborgen:"size" dagjsongen:"size"` +} + type AddArguments struct { - Blob Blob `cborgen:"blob" dagjsongen:"blob"` + Blob BlobSpec `cborgen:"blob" dagjsongen:"blob"` } type AddOK struct { @@ -25,7 +49,7 @@ type AddOK struct { type AcceptArguments struct { Space did.DID `cborgen:"space" dagjsongen:"space"` - Blob Blob `cborgen:"blob" dagjsongen:"blob"` + Blob BlobSpec `cborgen:"blob" dagjsongen:"blob"` Put promise.AwaitOK `cborgen:"_put" dagjsongen:"_put"` } @@ -37,9 +61,9 @@ type AcceptOK struct { } type AllocateArguments struct { - Space did.DID `cborgen:"space" dagjsongen:"space"` - Blob Blob `cborgen:"blob" dagjsongen:"blob"` - Cause cid.Cid `cborgen:"cause" dagjsongen:"cause"` + Space did.DID `cborgen:"space" dagjsongen:"space"` + Blob BlobSpec `cborgen:"blob" dagjsongen:"blob"` + Cause cid.Cid `cborgen:"cause" dagjsongen:"cause"` } type AllocateOK struct { @@ -100,20 +124,44 @@ type ReleaseArguments struct { // invocation subject. Cause is the `/blob/add` task link: the upload // service uses it to recover which storage node holds the parked blob — a // parked blob has no registration or acceptance to look the node up by. +// Digest is omitted when the `/blob/add` named only a digest code: Cause then +// identifies the upload on its own. type AbortArguments struct { - Digest multihash.Multihash `cborgen:"digest" dagjsongen:"digest"` + Digest multihash.Multihash `cborgen:"digest,omitempty" dagjsongen:"digest,omitempty"` Cause cid.Cid `cborgen:"cause" dagjsongen:"cause"` } -// RejectArguments drops Space's allocation for the parked (never-accepted) -// blob identified by Digest on the storage node; the node deletes any -// received bytes once no space holds an allocation or acceptance for the -// digest. +// RejectArguments drops Space's allocation for a parked (never-accepted) +// blob on the storage node; the node deletes any received bytes once no space +// holds an allocation or acceptance for the digest. It is a union of exactly +// one of: +// +// - [RejectDigestArguments]: the allocation of the blob with a digest; +// - [RejectAllocationArguments]: an allocation made without a digest, +// identified by the `/blob/allocate` task that created it. +// +// Encoding or decoding arguments that hold neither or both fails. The codec is +// hand-written (codec.go); the variants' codecs are generated. type RejectArguments struct { + byDigest *RejectDigestArguments + byAllocation *RejectAllocationArguments +} + +// RejectDigestArguments rejects Space's allocation for the blob identified by +// Digest. +type RejectDigestArguments struct { Space did.DID `cborgen:"space" dagjsongen:"space"` Digest multihash.Multihash `cborgen:"digest" dagjsongen:"digest"` } +// RejectAllocationArguments rejects Space's allocation made without a digest, +// identified by Allocation: the link to the `/blob/allocate` task that +// created it. +type RejectAllocationArguments struct { + Space did.DID `cborgen:"space" dagjsongen:"space"` + Allocation cid.Cid `cborgen:"allocation" dagjsongen:"allocation"` +} + type ReplicateArguments struct { // Blob is the blob that must be replicated. Blob Blob `cborgen:"blob" dagjsongen:"blob"` diff --git a/commands/http/cbor_gen.go b/commands/http/cbor_gen.go index da88658..0ea25cc 100644 --- a/commands/http/cbor_gen.go +++ b/commands/http/cbor_gen.go @@ -32,7 +32,7 @@ func (t *PutArguments) MarshalCBOR(w io.Writer) error { return err } - // t.Body (blob.Blob) (struct) + // t.Body (blob.BlobSpec) (struct) if len("body") > 8192 { return xerrors.Errorf("Value in field \"body\" was too long") } @@ -107,7 +107,7 @@ func (t *PutArguments) UnmarshalCBOR(r io.Reader) (err error) { } switch string(nameBuf[:nameLen]) { - // t.Body (blob.Blob) (struct) + // t.Body (blob.BlobSpec) (struct) case "body": { @@ -138,3 +138,226 @@ func (t *PutArguments) UnmarshalCBOR(r io.Reader) (err error) { return nil } +func (t *PutOK) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + fieldCount := 1 + + if t.Blob == nil { + fieldCount-- + } + + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { + return err + } + + // t.Blob (http.PutBlob) (struct) + if t.Blob != nil { + + if len("blob") > 8192 { + return xerrors.Errorf("Value in field \"blob\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("blob"))); err != nil { + return err + } + if _, err := cw.WriteString(string("blob")); err != nil { + return err + } + + if err := t.Blob.MarshalCBOR(cw); err != nil { + return err + } + } + return nil +} + +func (t *PutOK) UnmarshalCBOR(r io.Reader) (err error) { + *t = PutOK{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("PutOK: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 4) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Blob (http.PutBlob) (struct) + case "blob": + + { + + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + t.Blob = new(PutBlob) + if err := t.Blob.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Blob pointer: %w", err) + } + } + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *PutBlob) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{161}); err != nil { + return err + } + + // t.Digest (multihash.Multihash) (slice) + if len("digest") > 8192 { + return xerrors.Errorf("Value in field \"digest\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("digest"))); err != nil { + return err + } + if _, err := cw.WriteString(string("digest")); err != nil { + return err + } + + if len(t.Digest) > 2097152 { + return xerrors.Errorf("Byte array in field t.Digest was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.Digest))); err != nil { + return err + } + + if _, err := cw.Write(t.Digest); err != nil { + return err + } + + return nil +} + +func (t *PutBlob) UnmarshalCBOR(r io.Reader) (err error) { + *t = PutBlob{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("PutBlob: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 6) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Digest (multihash.Multihash) (slice) + case "digest": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.Digest: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + + if extra > 0 { + t.Digest = make([]uint8, extra) + } + + if _, err := io.ReadFull(cr, t.Digest); err != nil { + return err + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} diff --git a/commands/http/gen/main.go b/commands/http/gen/main.go index b12a0a4..b009b97 100644 --- a/commands/http/gen/main.go +++ b/commands/http/gen/main.go @@ -25,6 +25,8 @@ func tag(path string) { func main() { models := []any{ http.PutArguments{}, + http.PutOK{}, + http.PutBlob{}, } const ( cborFile = "../cbor_gen.go" diff --git a/commands/http/json_gen.go b/commands/http/json_gen.go index 61d152b..539f562 100644 --- a/commands/http/json_gen.go +++ b/commands/http/json_gen.go @@ -31,7 +31,7 @@ func (t *PutArguments) MarshalDagJSON(w io.Writer) error { } written := false - // t.Body (blob.Blob) (struct) + // t.Body (blob.BlobSpec) (struct) if len("body") > 8192 { return fmt.Errorf("string in field \"body\" was too long") } @@ -103,7 +103,7 @@ func (t *PutArguments) UnmarshalDagJSON(r io.Reader) (err error) { } switch name { - // t.Body (blob.Blob) (struct) + // t.Body (blob.BlobSpec) (struct) case "body": if err := t.Body.UnmarshalDagJSON(jr); err != nil { @@ -139,3 +139,214 @@ func (t *PutArguments) UnmarshalDagJSON(r io.Reader) (err error) { return nil } +func (t *PutOK) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + + // t.Blob (http.PutBlob) (struct) + if t.Blob != nil { + if len("blob") > 8192 { + return fmt.Errorf("string in field \"blob\" was too long") + } + if err := jw.WriteString(string("blob")); err != nil { + return fmt.Errorf("writing string for field \"blob\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.Blob.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.Blob: %w", err) + } + } + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *PutOK) UnmarshalDagJSON(r io.Reader) (err error) { + *t = PutOK{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for PutOK: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for PutOK: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for PutOK: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field PutOK: string too large") + } + return fmt.Errorf("reading string for field PutOK: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field PutOK: %w", err) + } + switch name { + + // t.Blob (http.PutBlob) (struct) + case "blob": + + { + null, err := jr.PeekNull() + if err != nil { + return fmt.Errorf("peeking null for field t.Blob: %w", err) + } + if null { + if err := jr.ReadNull(); err != nil { + return fmt.Errorf("reading null for field t.Blob: %w", err) + } + } else { + t.Blob = new(PutBlob) + if err := t.Blob.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.Blob pointer: %w", err) + } + } + } + + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for PutOK: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field PutOK: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for PutOK") + } + } + } + + return nil +} +func (t *PutBlob) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + + // t.Digest (multihash.Multihash) (slice) + if len("digest") > 8192 { + return fmt.Errorf("string in field \"digest\" was too long") + } + if err := jw.WriteString(string("digest")); err != nil { + return fmt.Errorf("writing string for field \"digest\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if len(t.Digest) > 2097152 { + return fmt.Errorf("byte array in field t.Digest was too long") + } + + if err := jw.WriteBytes(t.Digest); err != nil { + return fmt.Errorf("writing bytes for field t.Digest: %w", err) + } + + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *PutBlob) UnmarshalDagJSON(r io.Reader) (err error) { + *t = PutBlob{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for PutBlob: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for PutBlob: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for PutBlob: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field PutBlob: string too large") + } + return fmt.Errorf("reading string for field PutBlob: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field PutBlob: %w", err) + } + switch name { + + // t.Digest (multihash.Multihash) (slice) + case "digest": + + { + bval, err := jr.ReadBytes(2097152) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading bytes for field t.Digest: byte array too large") + } + return fmt.Errorf("reading bytes for field t.Digest: %w", err) + } + if len(bval) > 0 { + t.Digest = []uint8(bval) + } + } + + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for PutBlob: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field PutBlob: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for PutBlob") + } + } + } + + return nil +} diff --git a/commands/http/put.go b/commands/http/put.go index 91431c5..00f4d61 100644 --- a/commands/http/put.go +++ b/commands/http/put.go @@ -3,11 +3,8 @@ package http import ( - "github.com/fil-forge/libforge/commands" "github.com/fil-forge/ucantone/binding" "github.com/fil-forge/ucantone/ucan/command" ) -type PutOK = commands.Unit - var Put = binding.Bind[*PutArguments, *PutOK](command.MustParse("/http/put")) diff --git a/commands/http/put_test.go b/commands/http/put_test.go new file mode 100644 index 0000000..fe6c38d --- /dev/null +++ b/commands/http/put_test.go @@ -0,0 +1,41 @@ +//go:build !codegen + +package http_test + +import ( + "bytes" + "testing" + + "github.com/fil-forge/libforge/commands" + "github.com/fil-forge/libforge/commands/http" + "github.com/fil-forge/libforge/testutil" + "github.com/stretchr/testify/require" +) + +func TestPutOKRoundTrip(t *testing.T) { + in := http.PutOK{Blob: &http.PutBlob{Digest: testutil.RandomMultihash(t)}} + var buf bytes.Buffer + require.NoError(t, in.MarshalCBOR(&buf)) + var out http.PutOK + require.NoError(t, out.UnmarshalCBOR(&buf)) + require.Equal(t, in, out) + + var js bytes.Buffer + require.NoError(t, in.MarshalDagJSON(&js)) + var outJS http.PutOK + require.NoError(t, outJS.UnmarshalDagJSON(&js)) + require.Equal(t, in, outJS) +} + +// An empty result encodes as the empty map it was before it could carry a +// digest, so existing receipts decode and encode unchanged. +func TestPutOKEmptyEncodesAsUnit(t *testing.T) { + var want, got bytes.Buffer + require.NoError(t, (&commands.Unit{}).MarshalCBOR(&want)) + require.NoError(t, (&http.PutOK{}).MarshalCBOR(&got)) + require.Equal(t, want.Bytes(), got.Bytes()) + + var out http.PutOK + require.NoError(t, out.UnmarshalCBOR(bytes.NewReader(want.Bytes()))) + require.Nil(t, out.Blob) +} diff --git a/commands/http/types.go b/commands/http/types.go index 22e2084..29f680f 100644 --- a/commands/http/types.go +++ b/commands/http/types.go @@ -3,11 +3,25 @@ package http import ( "github.com/fil-forge/libforge/commands/blob" "github.com/fil-forge/ucantone/ucan/promise" + "github.com/multiformats/go-multihash" ) type PutArguments struct { - Body blob.Blob `cborgen:"body" dagjsongen:"body"` + Body blob.BlobSpec `cborgen:"body" dagjsongen:"body"` // Destination is the promise that resolves to the upload destination // where the blob should be PUT to. It is the result of a /blob/allocate task. Destination promise.AwaitOK `cborgen:"destination" dagjsongen:"destination"` } + +// PutOK is the result of a successful `/http/put`. Blob is set when the +// `/blob/allocate` task named only a digest code: it carries the digest the +// sender computed as it sent the data, which the storage node checks against +// its own at `/blob/accept`. +type PutOK struct { + Blob *PutBlob `cborgen:"blob,omitempty" dagjsongen:"blob,omitempty"` +} + +// PutBlob is the blob a `/http/put` delivered. +type PutBlob struct { + Digest multihash.Multihash `cborgen:"digest" dagjsongen:"digest"` +} diff --git a/commands/internal/codec/codec.go b/commands/internal/codec/codec.go index b8f73e4..7571b5b 100644 --- a/commands/internal/codec/codec.go +++ b/commands/internal/codec/codec.go @@ -5,12 +5,15 @@ package codec import ( + "bytes" "fmt" "io" "sort" + "strings" jsg "github.com/alanshaw/dag-json-gen" "github.com/fil-forge/ucantone/did" + "github.com/fil-forge/ucantone/ipld/datamodel" cid "github.com/ipfs/go-cid" cbg "github.com/whyrusleeping/cbor-gen" xerrors "golang.org/x/xerrors" @@ -194,3 +197,57 @@ func ReadJSONArray[T any](jr *jsg.DagJsonReader, read func() (T, error)) ([]T, e } } } + +// ReadCborVariant reads one CBOR map from r for a union whose variants are +// told apart by which of keys the map holds. It returns that key, and the map +// re-encoded for the variant's own decoder. It fails unless exactly one of +// keys is present. +func ReadCborVariant(r io.Reader, keys ...string) (string, []byte, error) { + var m datamodel.Map + if err := m.UnmarshalCBOR(r); err != nil { + return "", nil, err + } + key, err := variantKey(m, keys) + if err != nil { + return "", nil, err + } + var buf bytes.Buffer + if err := m.MarshalCBOR(&buf); err != nil { + return "", nil, err + } + return key, buf.Bytes(), nil +} + +// ReadDagJSONVariant is [ReadCborVariant] for DAG-JSON. +func ReadDagJSONVariant(r io.Reader, keys ...string) (string, []byte, error) { + var m datamodel.Map + if err := m.UnmarshalDagJSON(r); err != nil { + return "", nil, err + } + key, err := variantKey(m, keys) + if err != nil { + return "", nil, err + } + var buf bytes.Buffer + if err := m.MarshalDagJSON(&buf); err != nil { + return "", nil, err + } + return key, buf.Bytes(), nil +} + +func variantKey(m datamodel.Map, keys []string) (string, error) { + found := "" + for _, k := range keys { + if _, ok := m[k]; !ok { + continue + } + if found != "" { + return "", fmt.Errorf("exactly one of %s must be set, got %q and %q", strings.Join(keys, " or "), found, k) + } + found = k + } + if found == "" { + return "", fmt.Errorf("exactly one of %s must be set, got neither", strings.Join(keys, " or ")) + } + return found, nil +} From 412dcbab2064968412ca0467b7c82a7b49aae1c8 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Wed, 30 Sep 2026 16:55:21 +0100 Subject: [PATCH 2/4] refactor: no unbounded decode --- commands/blob/cbor_gen.go | 409 +++++++++++++++++++++++++++++++ commands/blob/codec.go | 89 +++---- commands/blob/gen/main.go | 2 + commands/blob/json_gen.go | 367 +++++++++++++++++++++++++++ commands/blob/spec_test.go | 52 ++++ commands/blob/types.go | 18 ++ commands/internal/codec/codec.go | 57 ----- 7 files changed, 884 insertions(+), 110 deletions(-) diff --git a/commands/blob/cbor_gen.go b/commands/blob/cbor_gen.go index 655d99b..bd5a8c9 100644 --- a/commands/blob/cbor_gen.go +++ b/commands/blob/cbor_gen.go @@ -441,6 +441,214 @@ func (t *BlobDigestCode) UnmarshalCBOR(r io.Reader) (err error) { return nil } +func (t *BlobSpecModel) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + fieldCount := 3 + + if t.Digest == nil { + fieldCount-- + } + + if t.DigestCode == nil { + fieldCount-- + } + + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { + return err + } + + // t.Size (uint64) (uint64) + if len("size") > 8192 { + return xerrors.Errorf("Value in field \"size\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("size"))); err != nil { + return err + } + if _, err := cw.WriteString(string("size")); err != nil { + return err + } + + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.Size)); err != nil { + return err + } + + // t.Digest (multihash.Multihash) (slice) + if t.Digest != nil { + + if len("digest") > 8192 { + return xerrors.Errorf("Value in field \"digest\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("digest"))); err != nil { + return err + } + if _, err := cw.WriteString(string("digest")); err != nil { + return err + } + + if len(t.Digest) > 2097152 { + return xerrors.Errorf("Byte array in field t.Digest was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.Digest))); err != nil { + return err + } + + if _, err := cw.Write(t.Digest); err != nil { + return err + } + + } + + // t.DigestCode (uint64) (uint64) + if t.DigestCode != nil { + + if len("digestCode") > 8192 { + return xerrors.Errorf("Value in field \"digestCode\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("digestCode"))); err != nil { + return err + } + if _, err := cw.WriteString(string("digestCode")); err != nil { + return err + } + + if t.DigestCode == nil { + if _, err := cw.Write(cbg.CborNull); err != nil { + return err + } + } else { + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(*t.DigestCode)); err != nil { + return err + } + } + + } + return nil +} + +func (t *BlobSpecModel) UnmarshalCBOR(r io.Reader) (err error) { + *t = BlobSpecModel{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("BlobSpecModel: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 10) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Size (uint64) (uint64) + case "size": + + { + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajUnsignedInt { + return fmt.Errorf("wrong type for uint64 field") + } + t.Size = uint64(extra) + + } + // t.Digest (multihash.Multihash) (slice) + case "digest": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.Digest: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + + if extra > 0 { + t.Digest = make([]uint8, extra) + } + + if _, err := io.ReadFull(cr, t.Digest); err != nil { + return err + } + + // t.DigestCode (uint64) (uint64) + case "digestCode": + + { + + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajUnsignedInt { + return fmt.Errorf("wrong type for uint64 field") + } + typed := uint64(extra) + t.DigestCode = &typed + } + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} func (t *AllocateOK) MarshalCBOR(w io.Writer) error { if t == nil { _, err := w.Write(cbg.CborNull) @@ -1961,6 +2169,207 @@ func (t *RejectAllocationArguments) UnmarshalCBOR(r io.Reader) (err error) { return nil } +func (t *RejectArgumentsModel) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + fieldCount := 3 + + if t.Digest == nil { + fieldCount-- + } + + if t.Allocation == nil { + fieldCount-- + } + + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { + return err + } + + // t.Space (did.DID) (struct) + if len("space") > 8192 { + return xerrors.Errorf("Value in field \"space\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("space"))); err != nil { + return err + } + if _, err := cw.WriteString(string("space")); err != nil { + return err + } + + if err := t.Space.MarshalCBOR(cw); err != nil { + return err + } + + // t.Digest (multihash.Multihash) (slice) + if t.Digest != nil { + + if len("digest") > 8192 { + return xerrors.Errorf("Value in field \"digest\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("digest"))); err != nil { + return err + } + if _, err := cw.WriteString(string("digest")); err != nil { + return err + } + + if len(t.Digest) > 2097152 { + return xerrors.Errorf("Byte array in field t.Digest was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.Digest))); err != nil { + return err + } + + if _, err := cw.Write(t.Digest); err != nil { + return err + } + + } + + // t.Allocation (cid.Cid) (struct) + if t.Allocation != nil { + + if len("allocation") > 8192 { + return xerrors.Errorf("Value in field \"allocation\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("allocation"))); err != nil { + return err + } + if _, err := cw.WriteString(string("allocation")); err != nil { + return err + } + + if t.Allocation == nil { + if _, err := cw.Write(cbg.CborNull); err != nil { + return err + } + } else { + if err := cbg.WriteCid(cw, *t.Allocation); err != nil { + return xerrors.Errorf("failed to write cid field t.Allocation: %w", err) + } + } + + } + return nil +} + +func (t *RejectArgumentsModel) UnmarshalCBOR(r io.Reader) (err error) { + *t = RejectArgumentsModel{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("RejectArgumentsModel: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 10) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Space (did.DID) (struct) + case "space": + + { + + if err := t.Space.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Space: %w", err) + } + + } + // t.Digest (multihash.Multihash) (slice) + case "digest": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.Digest: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + + if extra > 0 { + t.Digest = make([]uint8, extra) + } + + if _, err := io.ReadFull(cr, t.Digest); err != nil { + return err + } + + // t.Allocation (cid.Cid) (struct) + case "allocation": + + { + + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + + c, err := cbg.ReadCid(cr) + if err != nil { + return xerrors.Errorf("failed to read cid field t.Allocation: %w", err) + } + + t.Allocation = &c + } + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} func (t *ReplicateArguments) MarshalCBOR(w io.Writer) error { if t == nil { _, err := w.Write(cbg.CborNull) diff --git a/commands/blob/codec.go b/commands/blob/codec.go index 6a950b6..0095a11 100644 --- a/commands/blob/codec.go +++ b/commands/blob/codec.go @@ -3,10 +3,8 @@ package blob import ( - "bytes" "io" - "github.com/fil-forge/libforge/commands/internal/codec" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/errors" "github.com/ipfs/go-cid" @@ -23,7 +21,10 @@ const UnsupportedDigestCodeErrorName = "UnsupportedDigestCode" // ErrUnsupportedDigestCode is the failure for an unsupported digest code. var ErrUnsupportedDigestCode = errors.New(UnsupportedDigestCodeErrorName, "adding a blob by digest code is supported only for sha2-256") -var errNoVariant = errors.New("InvalidUnion", "union holds no variant") +var ( + errNoVariant = errors.New("InvalidUnion", "union holds no variant") + errBothVariants = errors.New("InvalidUnion", "union holds both variants") +) // SpecFromBlob returns the spec of a blob whose digest is known. func SpecFromBlob(b Blob) BlobSpec { @@ -75,11 +76,11 @@ func (s BlobSpec) MarshalCBOR(w io.Writer) error { } func (s *BlobSpec) UnmarshalCBOR(r io.Reader) error { - key, raw, err := codec.ReadCborVariant(r, "digest", "digestCode") - if err != nil { + var m BlobSpecModel + if err := m.UnmarshalCBOR(r); err != nil { return err } - return s.decode(key, raw, false) + return s.fromModel(m) } func (s BlobSpec) MarshalDagJSON(w io.Writer) error { @@ -93,28 +94,25 @@ func (s BlobSpec) MarshalDagJSON(w io.Writer) error { } func (s *BlobSpec) UnmarshalDagJSON(r io.Reader) error { - key, raw, err := codec.ReadDagJSONVariant(r, "digest", "digestCode") - if err != nil { + var m BlobSpecModel + if err := m.UnmarshalDagJSON(r); err != nil { return err } - return s.decode(key, raw, true) + return s.fromModel(m) } -func (s *BlobSpec) decode(key string, raw []byte, json bool) error { +func (s *BlobSpec) fromModel(m BlobSpecModel) error { *s = BlobSpec{} - if key == "digest" { - var b Blob - if err := unmarshalVariant(&b, raw, json); err != nil { - return err - } - s.blob = &b - return nil - } - var c BlobDigestCode - if err := unmarshalVariant(&c, raw, json); err != nil { - return err + switch hasDigest := len(m.Digest) > 0; { + case hasDigest && m.DigestCode == nil: + s.blob = &Blob{Digest: m.Digest, Size: m.Size} + case !hasDigest && m.DigestCode != nil: + s.code = &BlobDigestCode{DigestCode: *m.DigestCode, Size: m.Size} + case hasDigest: + return errBothVariants + default: + return errNoVariant } - s.code = &c return nil } @@ -169,11 +167,11 @@ func (a RejectArguments) MarshalCBOR(w io.Writer) error { } func (a *RejectArguments) UnmarshalCBOR(r io.Reader) error { - key, raw, err := codec.ReadCborVariant(r, "digest", "allocation") - if err != nil { + var m RejectArgumentsModel + if err := m.UnmarshalCBOR(r); err != nil { return err } - return a.decode(key, raw, false) + return a.fromModel(m) } func (a RejectArguments) MarshalDagJSON(w io.Writer) error { @@ -187,39 +185,24 @@ func (a RejectArguments) MarshalDagJSON(w io.Writer) error { } func (a *RejectArguments) UnmarshalDagJSON(r io.Reader) error { - key, raw, err := codec.ReadDagJSONVariant(r, "digest", "allocation") - if err != nil { + var m RejectArgumentsModel + if err := m.UnmarshalDagJSON(r); err != nil { return err } - return a.decode(key, raw, true) + return a.fromModel(m) } -func (a *RejectArguments) decode(key string, raw []byte, json bool) error { +func (a *RejectArguments) fromModel(m RejectArgumentsModel) error { *a = RejectArguments{} - if key == "digest" { - var d RejectDigestArguments - if err := unmarshalVariant(&d, raw, json); err != nil { - return err - } - a.byDigest = &d - return nil - } - var al RejectAllocationArguments - if err := unmarshalVariant(&al, raw, json); err != nil { - return err + switch hasDigest := len(m.Digest) > 0; { + case hasDigest && m.Allocation == nil: + a.byDigest = &RejectDigestArguments{Space: m.Space, Digest: m.Digest} + case !hasDigest && m.Allocation != nil: + a.byAllocation = &RejectAllocationArguments{Space: m.Space, Allocation: *m.Allocation} + case hasDigest: + return errBothVariants + default: + return errNoVariant } - a.byAllocation = &al return nil } - -type variant interface { - UnmarshalCBOR(io.Reader) error - UnmarshalDagJSON(io.Reader) error -} - -func unmarshalVariant(v variant, raw []byte, json bool) error { - if json { - return v.UnmarshalDagJSON(bytes.NewReader(raw)) - } - return v.UnmarshalCBOR(bytes.NewReader(raw)) -} diff --git a/commands/blob/gen/main.go b/commands/blob/gen/main.go index fb13d50..eecdb75 100644 --- a/commands/blob/gen/main.go +++ b/commands/blob/gen/main.go @@ -27,6 +27,7 @@ func main() { blob.AllocateArguments{}, blob.Blob{}, blob.BlobDigestCode{}, + blob.BlobSpecModel{}, blob.AllocateOK{}, blob.BlobAddress{}, blob.AcceptArguments{}, @@ -38,6 +39,7 @@ func main() { blob.AbortArguments{}, blob.RejectDigestArguments{}, blob.RejectAllocationArguments{}, + blob.RejectArgumentsModel{}, blob.ReplicateArguments{}, blob.ReplicateOK{}, blob.ListArguments{}, diff --git a/commands/blob/json_gen.go b/commands/blob/json_gen.go index 9d368ba..f75def9 100644 --- a/commands/blob/json_gen.go +++ b/commands/blob/json_gen.go @@ -445,6 +445,194 @@ func (t *BlobDigestCode) UnmarshalDagJSON(r io.Reader) (err error) { return nil } +func (t *BlobSpecModel) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + written := false + + // t.Digest (multihash.Multihash) (slice) + if t.Digest != nil { + if len("digest") > 8192 { + return fmt.Errorf("string in field \"digest\" was too long") + } + if err := jw.WriteString(string("digest")); err != nil { + return fmt.Errorf("writing string for field \"digest\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if len(t.Digest) > 2097152 { + return fmt.Errorf("byte array in field t.Digest was too long") + } + + if err := jw.WriteBytes(t.Digest); err != nil { + return fmt.Errorf("writing bytes for field t.Digest: %w", err) + } + + written = true + } + if t.DigestCode != nil { + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + } + + // t.DigestCode (uint64) (uint64) + if t.DigestCode != nil { + if len("digestCode") > 8192 { + return fmt.Errorf("string in field \"digestCode\" was too long") + } + if err := jw.WriteString(string("digestCode")); err != nil { + return fmt.Errorf("writing string for field \"digestCode\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + + if t.DigestCode == nil { + if err := jw.WriteNull(); err != nil { + return fmt.Errorf("writing null for field t.DigestCode: %w", err) + } + } else { + if err := jw.WriteUint64(uint64(*t.DigestCode)); err != nil { + return fmt.Errorf("writing uint64 for field t.DigestCode: %w", err) + } + } + + written = true + } + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Size (uint64) (uint64) + if len("size") > 8192 { + return fmt.Errorf("string in field \"size\" was too long") + } + if err := jw.WriteString(string("size")); err != nil { + return fmt.Errorf("writing string for field \"size\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + + if err := jw.WriteUint64(uint64(t.Size)); err != nil { + return fmt.Errorf("writing uint64 for field t.Size: %w", err) + } + + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *BlobSpecModel) UnmarshalDagJSON(r io.Reader) (err error) { + *t = BlobSpecModel{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for BlobSpecModel: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for BlobSpecModel: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for BlobSpecModel: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field BlobSpecModel: string too large") + } + return fmt.Errorf("reading string for field BlobSpecModel: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field BlobSpecModel: %w", err) + } + switch name { + + // t.Digest (multihash.Multihash) (slice) + case "digest": + + { + bval, err := jr.ReadBytes(2097152) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading bytes for field t.Digest: byte array too large") + } + return fmt.Errorf("reading bytes for field t.Digest: %w", err) + } + if len(bval) > 0 { + t.Digest = []uint8(bval) + } + } + + // t.DigestCode (uint64) (uint64) + case "digestCode": + { + + nval, err := jr.ReadNumberAsUint64OrNull() + if err != nil { + return fmt.Errorf("reading uint64 or null for field t.DigestCode: %w", err) + } + if nval != nil { + typed := uint64(*nval) + t.DigestCode = &typed + } + + } + + // t.Size (uint64) (uint64) + case "size": + { + + nval, err := jr.ReadNumberAsUint64() + if err != nil { + return fmt.Errorf("reading uint64 for field t.Size: %w", err) + } + t.Size = uint64(nval) + + } + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for BlobSpecModel: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field BlobSpecModel: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for BlobSpecModel") + } + } + } + + return nil +} func (t *AllocateOK) MarshalDagJSON(w io.Writer) error { jw := jsg.NewDagJsonWriter(w) if t == nil { @@ -1953,6 +2141,185 @@ func (t *RejectAllocationArguments) UnmarshalDagJSON(r io.Reader) (err error) { return nil } +func (t *RejectArgumentsModel) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + written := false + + // t.Allocation (cid.Cid) (struct) + if t.Allocation != nil { + if len("allocation") > 8192 { + return fmt.Errorf("string in field \"allocation\" was too long") + } + if err := jw.WriteString(string("allocation")); err != nil { + return fmt.Errorf("writing string for field \"allocation\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + + if t.Allocation == nil { + if err := jw.WriteNull(); err != nil { + return fmt.Errorf("writing null for field t.Allocation: %w", err) + } + } else { + if err := jw.WriteCid(*t.Allocation); err != nil { + return fmt.Errorf("writing CID for field t.Allocation: %w", err) + } + } + + written = true + } + if t.Digest != nil { + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + } + + // t.Digest (multihash.Multihash) (slice) + if t.Digest != nil { + if len("digest") > 8192 { + return fmt.Errorf("string in field \"digest\" was too long") + } + if err := jw.WriteString(string("digest")); err != nil { + return fmt.Errorf("writing string for field \"digest\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if len(t.Digest) > 2097152 { + return fmt.Errorf("byte array in field t.Digest was too long") + } + + if err := jw.WriteBytes(t.Digest); err != nil { + return fmt.Errorf("writing bytes for field t.Digest: %w", err) + } + + written = true + } + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Space (did.DID) (struct) + if len("space") > 8192 { + return fmt.Errorf("string in field \"space\" was too long") + } + if err := jw.WriteString(string("space")); err != nil { + return fmt.Errorf("writing string for field \"space\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.Space.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.Space: %w", err) + } + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *RejectArgumentsModel) UnmarshalDagJSON(r io.Reader) (err error) { + *t = RejectArgumentsModel{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for RejectArgumentsModel: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for RejectArgumentsModel: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for RejectArgumentsModel: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field RejectArgumentsModel: string too large") + } + return fmt.Errorf("reading string for field RejectArgumentsModel: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field RejectArgumentsModel: %w", err) + } + switch name { + + // t.Allocation (cid.Cid) (struct) + case "allocation": + { + + c, err := jr.ReadCidOrNull() + if err != nil { + return fmt.Errorf("reading CID or null for field t.Allocation: %w", err) + } + t.Allocation = c + + } + + // t.Digest (multihash.Multihash) (slice) + case "digest": + + { + bval, err := jr.ReadBytes(2097152) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading bytes for field t.Digest: byte array too large") + } + return fmt.Errorf("reading bytes for field t.Digest: %w", err) + } + if len(bval) > 0 { + t.Digest = []uint8(bval) + } + } + + // t.Space (did.DID) (struct) + case "space": + + if err := t.Space.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.Space: %w", err) + } + + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for RejectArgumentsModel: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field RejectArgumentsModel: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for RejectArgumentsModel") + } + } + } + + return nil +} func (t *ReplicateArguments) MarshalDagJSON(w io.Writer) error { jw := jsg.NewDagJsonWriter(w) if t == nil { diff --git a/commands/blob/spec_test.go b/commands/blob/spec_test.go index dd60830..89fd9be 100644 --- a/commands/blob/spec_test.go +++ b/commands/blob/spec_test.go @@ -133,3 +133,55 @@ func TestAbortArgumentsWithoutDigest(t *testing.T) { require.Empty(t, out.Digest) require.Equal(t, in.Cause, out.Cause) } + +// A union decodes through its model, which holds the fields of both variants: +// exactly one variant's field must be set, whichever codec carries it. +func TestUnionModelsRejectInvalidCBOR(t *testing.T) { + digest := testutil.RandomMultihash(t) + code := uint64(multicodec.Sha2_256) + alloc := testutil.RandomCID(t) + space := testutil.RandomDID(t) + + for name, m := range map[string]blob.BlobSpecModel{ + "neither": {Size: 1}, + "both": {Digest: digest, DigestCode: &code, Size: 1}, + } { + t.Run("spec "+name, func(t *testing.T) { + var buf bytes.Buffer + require.NoError(t, m.MarshalCBOR(&buf)) + var out blob.BlobSpec + require.Error(t, out.UnmarshalCBOR(&buf)) + }) + } + for name, m := range map[string]blob.RejectArgumentsModel{ + "neither": {Space: space}, + "both": {Space: space, Digest: digest, Allocation: &alloc}, + } { + t.Run("reject "+name, func(t *testing.T) { + var buf bytes.Buffer + require.NoError(t, m.MarshalCBOR(&buf)) + var out blob.RejectArguments + require.Error(t, out.UnmarshalCBOR(&buf)) + + var js bytes.Buffer + require.NoError(t, m.MarshalDagJSON(&js)) + require.Error(t, out.UnmarshalDagJSON(&js)) + }) + } +} + +// The model's generated decoder bounds each field as it reads it, so a spec +// decodes in one pass with no intermediate copy of its fields. +func TestBlobSpecRefusesOversizedDigest(t *testing.T) { + // {"digest": <3 MiB of bytes>, "size": 1}, written by hand: the generated + // encoder refuses a digest this large too. + n := 3 << 20 + var buf bytes.Buffer + buf.WriteByte(0xa2) + buf.WriteString("\x66digest") + buf.Write([]byte{0x5a, byte(n >> 24), byte(n >> 16), byte(n >> 8), byte(n)}) + buf.Write(bytes.Repeat([]byte{1}, n)) + buf.WriteString("\x64size\x01") + var out blob.BlobSpec + require.ErrorContains(t, out.UnmarshalCBOR(&buf), "t.Digest: byte array too large") +} diff --git a/commands/blob/types.go b/commands/blob/types.go index aa91d2c..6880c2e 100644 --- a/commands/blob/types.go +++ b/commands/blob/types.go @@ -37,6 +37,15 @@ type BlobDigestCode struct { Size uint64 `cborgen:"size" dagjsongen:"size"` } +// BlobSpecModel is the encoded form of a [BlobSpec]: the fields of both of its +// variants, so a spec decodes in one bounded pass. Exactly one of Digest and +// DigestCode is set in a valid spec. +type BlobSpecModel struct { + Digest multihash.Multihash `cborgen:"digest,omitempty" dagjsongen:"digest,omitempty"` + DigestCode *uint64 `cborgen:"digestCode,omitempty" dagjsongen:"digestCode,omitempty"` + Size uint64 `cborgen:"size" dagjsongen:"size"` +} + type AddArguments struct { Blob BlobSpec `cborgen:"blob" dagjsongen:"blob"` } @@ -147,6 +156,15 @@ type RejectArguments struct { byAllocation *RejectAllocationArguments } +// RejectArgumentsModel is the encoded form of [RejectArguments]: the fields of +// both of its variants, so the arguments decode in one bounded pass. Exactly +// one of Digest and Allocation is set in valid arguments. +type RejectArgumentsModel struct { + Space did.DID `cborgen:"space" dagjsongen:"space"` + Digest multihash.Multihash `cborgen:"digest,omitempty" dagjsongen:"digest,omitempty"` + Allocation *cid.Cid `cborgen:"allocation,omitempty" dagjsongen:"allocation,omitempty"` +} + // RejectDigestArguments rejects Space's allocation for the blob identified by // Digest. type RejectDigestArguments struct { diff --git a/commands/internal/codec/codec.go b/commands/internal/codec/codec.go index 7571b5b..b8f73e4 100644 --- a/commands/internal/codec/codec.go +++ b/commands/internal/codec/codec.go @@ -5,15 +5,12 @@ package codec import ( - "bytes" "fmt" "io" "sort" - "strings" jsg "github.com/alanshaw/dag-json-gen" "github.com/fil-forge/ucantone/did" - "github.com/fil-forge/ucantone/ipld/datamodel" cid "github.com/ipfs/go-cid" cbg "github.com/whyrusleeping/cbor-gen" xerrors "golang.org/x/xerrors" @@ -197,57 +194,3 @@ func ReadJSONArray[T any](jr *jsg.DagJsonReader, read func() (T, error)) ([]T, e } } } - -// ReadCborVariant reads one CBOR map from r for a union whose variants are -// told apart by which of keys the map holds. It returns that key, and the map -// re-encoded for the variant's own decoder. It fails unless exactly one of -// keys is present. -func ReadCborVariant(r io.Reader, keys ...string) (string, []byte, error) { - var m datamodel.Map - if err := m.UnmarshalCBOR(r); err != nil { - return "", nil, err - } - key, err := variantKey(m, keys) - if err != nil { - return "", nil, err - } - var buf bytes.Buffer - if err := m.MarshalCBOR(&buf); err != nil { - return "", nil, err - } - return key, buf.Bytes(), nil -} - -// ReadDagJSONVariant is [ReadCborVariant] for DAG-JSON. -func ReadDagJSONVariant(r io.Reader, keys ...string) (string, []byte, error) { - var m datamodel.Map - if err := m.UnmarshalDagJSON(r); err != nil { - return "", nil, err - } - key, err := variantKey(m, keys) - if err != nil { - return "", nil, err - } - var buf bytes.Buffer - if err := m.MarshalDagJSON(&buf); err != nil { - return "", nil, err - } - return key, buf.Bytes(), nil -} - -func variantKey(m datamodel.Map, keys []string) (string, error) { - found := "" - for _, k := range keys { - if _, ok := m[k]; !ok { - continue - } - if found != "" { - return "", fmt.Errorf("exactly one of %s must be set, got %q and %q", strings.Join(keys, " or "), found, k) - } - found = k - } - if found == "" { - return "", fmt.Errorf("exactly one of %s must be set, got neither", strings.Join(keys, " or ")) - } - return found, nil -} From ff62f29c6b77b6809e353aeae3318100ecd8a954 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Thu, 1 Oct 2026 10:20:07 +0100 Subject: [PATCH 3/4] refactor: api --- commands/blob/cbor_gen.go | 129 ------------------------------------ commands/blob/codec.go | 81 ++++++++++++----------- commands/blob/gen/main.go | 1 - commands/blob/json_gen.go | 132 ------------------------------------- commands/blob/spec_test.go | 31 +++++++-- commands/blob/types.go | 31 ++++----- 6 files changed, 82 insertions(+), 323 deletions(-) diff --git a/commands/blob/cbor_gen.go b/commands/blob/cbor_gen.go index bd5a8c9..1ffd9c7 100644 --- a/commands/blob/cbor_gen.go +++ b/commands/blob/cbor_gen.go @@ -312,135 +312,6 @@ func (t *Blob) UnmarshalCBOR(r io.Reader) (err error) { return nil } -func (t *BlobDigestCode) MarshalCBOR(w io.Writer) error { - if t == nil { - _, err := w.Write(cbg.CborNull) - return err - } - - cw := cbg.NewCborWriter(w) - - if _, err := cw.Write([]byte{162}); err != nil { - return err - } - - // t.Size (uint64) (uint64) - if len("size") > 8192 { - return xerrors.Errorf("Value in field \"size\" was too long") - } - - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("size"))); err != nil { - return err - } - if _, err := cw.WriteString(string("size")); err != nil { - return err - } - - if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.Size)); err != nil { - return err - } - - // t.DigestCode (uint64) (uint64) - if len("digestCode") > 8192 { - return xerrors.Errorf("Value in field \"digestCode\" was too long") - } - - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("digestCode"))); err != nil { - return err - } - if _, err := cw.WriteString(string("digestCode")); err != nil { - return err - } - - if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.DigestCode)); err != nil { - return err - } - - return nil -} - -func (t *BlobDigestCode) UnmarshalCBOR(r io.Reader) (err error) { - *t = BlobDigestCode{} - - cr := cbg.NewCborReader(r) - - maj, extra, err := cr.ReadHeader() - if err != nil { - return err - } - defer func() { - if err == io.EOF { - err = io.ErrUnexpectedEOF - } - }() - - if maj != cbg.MajMap { - return fmt.Errorf("cbor input should be of type map") - } - - if extra > cbg.MaxLength { - return fmt.Errorf("BlobDigestCode: map struct too large (%d)", extra) - } - - n := extra - - nameBuf := make([]byte, 10) - for i := uint64(0); i < n; i++ { - nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) - if err != nil { - return err - } - - if !ok { - // Field doesn't exist on this type, so ignore it - if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { - return err - } - continue - } - - switch string(nameBuf[:nameLen]) { - // t.Size (uint64) (uint64) - case "size": - - { - - maj, extra, err = cr.ReadHeader() - if err != nil { - return err - } - if maj != cbg.MajUnsignedInt { - return fmt.Errorf("wrong type for uint64 field") - } - t.Size = uint64(extra) - - } - // t.DigestCode (uint64) (uint64) - case "digestCode": - - { - - maj, extra, err = cr.ReadHeader() - if err != nil { - return err - } - if maj != cbg.MajUnsignedInt { - return fmt.Errorf("wrong type for uint64 field") - } - t.DigestCode = uint64(extra) - - } - - default: - // Field doesn't exist on this type, so ignore it - if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { - return err - } - } - } - - return nil -} func (t *BlobSpecModel) MarshalCBOR(w io.Writer) error { if t == nil { _, err := w.Write(cbg.CborNull) diff --git a/commands/blob/codec.go b/commands/blob/codec.go index 0095a11..ce50d2b 100644 --- a/commands/blob/codec.go +++ b/commands/blob/codec.go @@ -3,6 +3,7 @@ package blob import ( + "fmt" "io" "github.com/fil-forge/ucantone/did" @@ -26,53 +27,57 @@ var ( errBothVariants = errors.New("InvalidUnion", "union holds both variants") ) -// SpecFromBlob returns the spec of a blob whose digest is known. +// SpecFromBlob returns the spec of a blob whose digest is known. Its digest +// code is the digest's own; a digest that is not a multihash has code 0. func SpecFromBlob(b Blob) BlobSpec { - return BlobSpec{blob: &b} + s := BlobSpec{digest: b.Digest, size: b.Size, valid: true} + if d, err := multihash.Decode(b.Digest); err == nil { + s.code = d.Code + } + return s } // SpecFromDigestCode returns the spec of a blob of size bytes whose digest is // to be computed with the multihash function code. func SpecFromDigestCode(code, size uint64) BlobSpec { - return BlobSpec{code: &BlobDigestCode{DigestCode: code, Size: size}} + return BlobSpec{code: code, size: size, valid: true} } -// Blob returns the blob with its digest, when the spec names one. -func (s BlobSpec) Blob() (Blob, bool) { - if s.blob == nil { - return Blob{}, false - } - return *s.blob, true +// Digest returns the blob's digest, when the spec names one. +func (s BlobSpec) Digest() (multihash.Multihash, bool) { + return s.digest, len(s.digest) > 0 } -// DigestCode returns the hash function and size, when the spec names a digest -// code instead of a digest. -func (s BlobSpec) DigestCode() (BlobDigestCode, bool) { - if s.code == nil { - return BlobDigestCode{}, false - } - return *s.code, true +// DigestCode returns the code of the multihash function the blob's digest is +// computed with: the one the spec names, or its digest's own. +func (s BlobSpec) DigestCode() uint64 { + return s.code } -// Size returns the size of the blob, whichever variant the spec holds. +// Size returns the size of the blob. func (s BlobSpec) Size() uint64 { - switch { - case s.blob != nil: - return s.blob.Size - case s.code != nil: - return s.code.Size + return s.size +} + +// model returns the spec's encoded form, which names the digest or the +// digest code but never both. +func (s BlobSpec) model() (BlobSpecModel, error) { + if !s.valid { + return BlobSpecModel{}, errNoVariant + } + if len(s.digest) > 0 { + return BlobSpecModel{Digest: s.digest, Size: s.size}, nil } - return 0 + code := s.code + return BlobSpecModel{DigestCode: &code, Size: s.size}, nil } func (s BlobSpec) MarshalCBOR(w io.Writer) error { - switch { - case s.blob != nil && s.code == nil: - return s.blob.MarshalCBOR(w) - case s.code != nil && s.blob == nil: - return s.code.MarshalCBOR(w) + m, err := s.model() + if err != nil { + return err } - return errNoVariant + return m.MarshalCBOR(w) } func (s *BlobSpec) UnmarshalCBOR(r io.Reader) error { @@ -84,13 +89,11 @@ func (s *BlobSpec) UnmarshalCBOR(r io.Reader) error { } func (s BlobSpec) MarshalDagJSON(w io.Writer) error { - switch { - case s.blob != nil && s.code == nil: - return s.blob.MarshalDagJSON(w) - case s.code != nil && s.blob == nil: - return s.code.MarshalDagJSON(w) + m, err := s.model() + if err != nil { + return err } - return errNoVariant + return m.MarshalDagJSON(w) } func (s *BlobSpec) UnmarshalDagJSON(r io.Reader) error { @@ -105,9 +108,13 @@ func (s *BlobSpec) fromModel(m BlobSpecModel) error { *s = BlobSpec{} switch hasDigest := len(m.Digest) > 0; { case hasDigest && m.DigestCode == nil: - s.blob = &Blob{Digest: m.Digest, Size: m.Size} + d, err := multihash.Decode(m.Digest) + if err != nil { + return fmt.Errorf("decoding blob digest: %w", err) + } + *s = BlobSpec{digest: m.Digest, code: d.Code, size: m.Size, valid: true} case !hasDigest && m.DigestCode != nil: - s.code = &BlobDigestCode{DigestCode: *m.DigestCode, Size: m.Size} + *s = SpecFromDigestCode(*m.DigestCode, m.Size) case hasDigest: return errBothVariants default: diff --git a/commands/blob/gen/main.go b/commands/blob/gen/main.go index eecdb75..a412e6b 100644 --- a/commands/blob/gen/main.go +++ b/commands/blob/gen/main.go @@ -26,7 +26,6 @@ func main() { models := []any{ blob.AllocateArguments{}, blob.Blob{}, - blob.BlobDigestCode{}, blob.BlobSpecModel{}, blob.AllocateOK{}, blob.BlobAddress{}, diff --git a/commands/blob/json_gen.go b/commands/blob/json_gen.go index f75def9..1ef1e3d 100644 --- a/commands/blob/json_gen.go +++ b/commands/blob/json_gen.go @@ -313,138 +313,6 @@ func (t *Blob) UnmarshalDagJSON(r io.Reader) (err error) { return nil } -func (t *BlobDigestCode) MarshalDagJSON(w io.Writer) error { - jw := jsg.NewDagJsonWriter(w) - if t == nil { - err := jw.WriteNull() - return err - } - if err := jw.WriteObjectOpen(); err != nil { - return err - } - written := false - - // t.DigestCode (uint64) (uint64) - if len("digestCode") > 8192 { - return fmt.Errorf("string in field \"digestCode\" was too long") - } - if err := jw.WriteString(string("digestCode")); err != nil { - return fmt.Errorf("writing string for field \"digestCode\": %w", err) - } - if err := jw.WriteObjectColon(); err != nil { - return err - } - - if err := jw.WriteUint64(uint64(t.DigestCode)); err != nil { - return fmt.Errorf("writing uint64 for field t.DigestCode: %w", err) - } - - written = true - if written { - if err := jw.WriteComma(); err != nil { - return err - } - } - - // t.Size (uint64) (uint64) - if len("size") > 8192 { - return fmt.Errorf("string in field \"size\" was too long") - } - if err := jw.WriteString(string("size")); err != nil { - return fmt.Errorf("writing string for field \"size\": %w", err) - } - if err := jw.WriteObjectColon(); err != nil { - return err - } - - if err := jw.WriteUint64(uint64(t.Size)); err != nil { - return fmt.Errorf("writing uint64 for field t.Size: %w", err) - } - - if err := jw.WriteObjectClose(); err != nil { - return err - } - return nil -} -func (t *BlobDigestCode) UnmarshalDagJSON(r io.Reader) (err error) { - *t = BlobDigestCode{} - - jr := jsg.NewDagJsonReader(r) - defer func() { - if err == io.EOF { - err = io.ErrUnexpectedEOF - } - }() - if err := jr.ReadObjectOpen(); err != nil { - return fmt.Errorf("reading object open for BlobDigestCode: %w", err) - } - close, err := jr.PeekObjectClose() - if err != nil { - return fmt.Errorf("peeking object close for BlobDigestCode: %w", err) - } - if close { - if err := jr.ReadObjectClose(); err != nil { - return fmt.Errorf("reading object close for BlobDigestCode: %w", err) - } - } else { - for i := uint64(0); i < 8192; i++ { - name, err := jr.ReadString(8192) - if err != nil { - if errors.Is(err, jsg.ErrLimitExceeded) { - return fmt.Errorf("reading string for field BlobDigestCode: string too large") - } - return fmt.Errorf("reading string for field BlobDigestCode: %w", err) - } - if err := jr.ReadObjectColon(); err != nil { - return fmt.Errorf("reading object colon for field BlobDigestCode: %w", err) - } - switch name { - - // t.DigestCode (uint64) (uint64) - case "digestCode": - { - - nval, err := jr.ReadNumberAsUint64() - if err != nil { - return fmt.Errorf("reading uint64 for field t.DigestCode: %w", err) - } - t.DigestCode = uint64(nval) - - } - - // t.Size (uint64) (uint64) - case "size": - { - - nval, err := jr.ReadNumberAsUint64() - if err != nil { - return fmt.Errorf("reading uint64 for field t.Size: %w", err) - } - t.Size = uint64(nval) - - } - default: - // Field doesn't exist on this type, so ignore it - if err := jr.DiscardType(); err != nil { - return fmt.Errorf("ignoring field %s for BlobDigestCode: %w", name, err) - } - } - - close, err := jr.ReadObjectCloseOrComma() - if err != nil { - return fmt.Errorf("reading object close or comma for field BlobDigestCode: %w", err) - } - if close { - break - } - if i == 8192-1 { - return fmt.Errorf("map too large for BlobDigestCode") - } - } - } - - return nil -} func (t *BlobSpecModel) MarshalDagJSON(w io.Writer) error { jw := jsg.NewDagJsonWriter(w) if t == nil { diff --git a/commands/blob/spec_test.go b/commands/blob/spec_test.go index 89fd9be..253fcec 100644 --- a/commands/blob/spec_test.go +++ b/commands/blob/spec_test.go @@ -52,13 +52,32 @@ func TestBlobSpecRoundTrip(t *testing.T) { }) } - gotBlob, ok := blob.SpecFromBlob(b).Blob() - require.True(t, ok) - require.Equal(t, b, gotBlob) - gotCode, ok := blob.SpecFromDigestCode(code, 9).DigestCode() +} + +func TestBlobSpecAccessors(t *testing.T) { + digest := testutil.RandomMultihash(t) + byDigest := blob.SpecFromBlob(blob.Blob{Digest: digest, Size: 7}) + got, ok := byDigest.Digest() require.True(t, ok) - require.Equal(t, blob.BlobDigestCode{DigestCode: code, Size: 9}, gotCode) - require.EqualValues(t, 9, blob.SpecFromDigestCode(code, 9).Size()) + require.Equal(t, digest, got) + require.Equal(t, uint64(multicodec.Sha2_256), byDigest.DigestCode(), "a digest has its own code") + require.EqualValues(t, 7, byDigest.Size()) + + byCode := blob.SpecFromDigestCode(uint64(multicodec.Sha2_256), 9) + _, ok = byCode.Digest() + require.False(t, ok) + require.Equal(t, uint64(multicodec.Sha2_256), byCode.DigestCode()) + require.EqualValues(t, 9, byCode.Size()) +} + +// A spec naming a digest decodes only when the digest is a multihash, since +// its digest code comes from it. +func TestBlobSpecRefusesMalformedDigest(t *testing.T) { + m := blob.BlobSpecModel{Digest: []byte{0xff}, Size: 1} + var buf bytes.Buffer + require.NoError(t, m.MarshalCBOR(&buf)) + var out blob.BlobSpec + require.Error(t, out.UnmarshalCBOR(&buf)) } // A spec travels inside the arguments' generated codecs: the nested union diff --git a/commands/blob/types.go b/commands/blob/types.go index 6880c2e..1b37963 100644 --- a/commands/blob/types.go +++ b/commands/blob/types.go @@ -15,26 +15,21 @@ type Blob struct { // BlobSpec describes a blob in the arguments of the capabilities that can // add a blob before its digest is known: `/blob/add`, `/blob/allocate`, -// `/blob/accept` and the body of `/http/put`. It is a union of exactly one -// of: +// `/blob/accept` and the body of `/http/put`. It names the blob's size and +// either its digest, when the digest is known in advance, or only the code of +// the multihash function its digest is to be computed with, when both parties +// compute it as the data streams. A spec naming a digest has the digest's +// code too. // -// - [Blob]: the blob's digest and size, when the digest is known in advance; -// - [BlobDigestCode]: the code of the hash function and the size, when the -// digest is computed by both parties as the data streams. -// -// Encoding or decoding a spec that holds neither or both fails. The codec is -// hand-written (codec.go); the variants' codecs are generated. +// Construct one with [SpecFromBlob] or [SpecFromDigestCode]. Encoding the zero +// value fails, as does decoding a spec that names both a digest and a digest +// code, or neither. The codec is hand-written (codec.go), over the generated +// codec of [BlobSpecModel]. type BlobSpec struct { - blob *Blob - code *BlobDigestCode -} - -// BlobDigestCode describes a blob whose digest is not known in advance: the -// multicodec code of the multihash function it is to be hashed with, and its -// size. -type BlobDigestCode struct { - DigestCode uint64 `cborgen:"digestCode" dagjsongen:"digestCode"` - Size uint64 `cborgen:"size" dagjsongen:"size"` + digest multihash.Multihash + code uint64 + size uint64 + valid bool } // BlobSpecModel is the encoded form of a [BlobSpec]: the fields of both of its From 73ce08a5f0bb3e197d3779d039986bc5f74d5d48 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Thu, 1 Oct 2026 11:09:13 +0100 Subject: [PATCH 4/4] refactor: simplify abort and reject args --- commands/blob/abort.go | 13 +- commands/blob/abort_test.go | 24 +- commands/blob/cbor_gen.go | 458 ++---------------------------------- commands/blob/codec.go | 93 -------- commands/blob/gen/main.go | 4 +- commands/blob/json_gen.go | 434 ++-------------------------------- commands/blob/reject.go | 17 +- commands/blob/spec_test.go | 60 +---- commands/blob/types.go | 56 +---- 9 files changed, 83 insertions(+), 1076 deletions(-) diff --git a/commands/blob/abort.go b/commands/blob/abort.go index c6b4ca8..d6d7ef3 100644 --- a/commands/blob/abort.go +++ b/commands/blob/abort.go @@ -19,9 +19,9 @@ type AbortOK = commands.Unit // // Served by the upload service (subject = the space). A parked blob has no // registration or acceptance to look the storage node up by, so the service -// recovers it from the Cause receipt chain and forwards a `/blob/reject` -// (Cause itself is not forwarded — it is routing metadata, meaningless to -// the node). A missing or unknown Cause fails with MissingCause. Blobs the +// recovers it from the receipt chain of the Add task, and forwards a +// `/blob/reject` of the allocation the add made. A missing or unknown Add +// fails with MissingCause. Blobs the // space has accepted are released via `/blob/remove` instead; if the node // refuses the translated reject with BlobAccepted, the service surfaces that // named failure in the abort receipt. The abort mutates no upload-service @@ -32,9 +32,8 @@ type AbortOK = commands.Unit var Abort = binding.Bind[*AbortArguments, *AbortOK](command.MustParse("/blob/abort")) // MissingCauseErrorName is the stable receipt-failure name when an abort's -// Cause is missing or does not resolve to a known `/blob/add` task — -// without it the upload service cannot recover which storage node holds the -// parked blob. +// Add is missing or does not resolve to a known `/blob/add` task — without it +// the upload service cannot recover which storage node holds the parked blob. const MissingCauseErrorName = "MissingCause" -var ErrMissingCause = errors.New(MissingCauseErrorName, "abort requires the cause of the /blob/add task that parked the blob") +var ErrMissingCause = errors.New(MissingCauseErrorName, "abort requires the /blob/add task that parked the blob") diff --git a/commands/blob/abort_test.go b/commands/blob/abort_test.go index 8aef985..0667dd5 100644 --- a/commands/blob/abort_test.go +++ b/commands/blob/abort_test.go @@ -13,16 +13,12 @@ import ( // Round-trips AbortArguments through cbor. func TestAbortArgumentsRoundTrip(t *testing.T) { - in := blob.AbortArguments{ - Digest: testutil.RandomMultihash(t), - Cause: testutil.RandomCID(t), - } + in := blob.AbortArguments{Add: testutil.RandomCID(t)} var buf bytes.Buffer require.NoError(t, in.MarshalCBOR(&buf)) var out blob.AbortArguments require.NoError(t, out.UnmarshalCBOR(&buf)) - require.Equal(t, in.Digest, out.Digest) - require.Equal(t, in.Cause, out.Cause) + require.Equal(t, in, out) } // Round-trips ReleaseArguments through cbor. @@ -41,16 +37,18 @@ func TestReleaseArgumentsRoundTrip(t *testing.T) { require.Equal(t, in.Cause, out.Cause) } -// Round-trips RejectArguments through cbor. +// Round-trips RejectArguments through cbor and dag-json. func TestRejectArgumentsRoundTrip(t *testing.T) { - space, digest := testutil.RandomDID(t), testutil.RandomMultihash(t) - in := blob.RejectByDigest(space, digest) + in := blob.RejectArguments{Allocation: testutil.RandomCID(t)} var buf bytes.Buffer require.NoError(t, in.MarshalCBOR(&buf)) var out blob.RejectArguments require.NoError(t, out.UnmarshalCBOR(&buf)) - require.Equal(t, space, out.Space()) - got, ok := out.Digest() - require.True(t, ok) - require.Equal(t, digest, got) + require.Equal(t, in, out) + + var js bytes.Buffer + require.NoError(t, in.MarshalDagJSON(&js)) + var outJS blob.RejectArguments + require.NoError(t, outJS.UnmarshalDagJSON(&js)) + require.Equal(t, in, outJS) } diff --git a/commands/blob/cbor_gen.go b/commands/blob/cbor_gen.go index 1ffd9c7..ba008e3 100644 --- a/commands/blob/cbor_gen.go +++ b/commands/blob/cbor_gen.go @@ -1636,59 +1636,27 @@ func (t *AbortArguments) MarshalCBOR(w io.Writer) error { } cw := cbg.NewCborWriter(w) - fieldCount := 2 - if t.Digest == nil { - fieldCount-- - } - - if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { + if _, err := cw.Write([]byte{161}); err != nil { return err } - // t.Cause (cid.Cid) (struct) - if len("cause") > 8192 { - return xerrors.Errorf("Value in field \"cause\" was too long") + // t.Add (cid.Cid) (struct) + if len("add") > 8192 { + return xerrors.Errorf("Value in field \"add\" was too long") } - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("cause"))); err != nil { + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("add"))); err != nil { return err } - if _, err := cw.WriteString(string("cause")); err != nil { + if _, err := cw.WriteString(string("add")); err != nil { return err } - if err := cbg.WriteCid(cw, t.Cause); err != nil { - return xerrors.Errorf("failed to write cid field t.Cause: %w", err) + if err := cbg.WriteCid(cw, t.Add); err != nil { + return xerrors.Errorf("failed to write cid field t.Add: %w", err) } - // t.Digest (multihash.Multihash) (slice) - if t.Digest != nil { - - if len("digest") > 8192 { - return xerrors.Errorf("Value in field \"digest\" was too long") - } - - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("digest"))); err != nil { - return err - } - if _, err := cw.WriteString(string("digest")); err != nil { - return err - } - - if len(t.Digest) > 2097152 { - return xerrors.Errorf("Byte array in field t.Digest was too long") - } - - if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.Digest))); err != nil { - return err - } - - if _, err := cw.Write(t.Digest); err != nil { - return err - } - - } return nil } @@ -1717,7 +1685,7 @@ func (t *AbortArguments) UnmarshalCBOR(r io.Reader) (err error) { n := extra - nameBuf := make([]byte, 6) + nameBuf := make([]byte, 3) for i := uint64(0); i < n; i++ { nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) if err != nil { @@ -1733,179 +1701,18 @@ func (t *AbortArguments) UnmarshalCBOR(r io.Reader) (err error) { } switch string(nameBuf[:nameLen]) { - // t.Cause (cid.Cid) (struct) - case "cause": + // t.Add (cid.Cid) (struct) + case "add": { c, err := cbg.ReadCid(cr) if err != nil { - return xerrors.Errorf("failed to read cid field t.Cause: %w", err) - } - - t.Cause = c - - } - // t.Digest (multihash.Multihash) (slice) - case "digest": - - maj, extra, err = cr.ReadHeader() - if err != nil { - return err - } - - if extra > 2097152 { - return fmt.Errorf("t.Digest: byte array too large (%d)", extra) - } - if maj != cbg.MajByteString { - return fmt.Errorf("expected byte array") - } - - if extra > 0 { - t.Digest = make([]uint8, extra) - } - - if _, err := io.ReadFull(cr, t.Digest); err != nil { - return err - } - - default: - // Field doesn't exist on this type, so ignore it - if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { - return err - } - } - } - - return nil -} -func (t *RejectDigestArguments) MarshalCBOR(w io.Writer) error { - if t == nil { - _, err := w.Write(cbg.CborNull) - return err - } - - cw := cbg.NewCborWriter(w) - - if _, err := cw.Write([]byte{162}); err != nil { - return err - } - - // t.Space (did.DID) (struct) - if len("space") > 8192 { - return xerrors.Errorf("Value in field \"space\" was too long") - } - - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("space"))); err != nil { - return err - } - if _, err := cw.WriteString(string("space")); err != nil { - return err - } - - if err := t.Space.MarshalCBOR(cw); err != nil { - return err - } - - // t.Digest (multihash.Multihash) (slice) - if len("digest") > 8192 { - return xerrors.Errorf("Value in field \"digest\" was too long") - } - - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("digest"))); err != nil { - return err - } - if _, err := cw.WriteString(string("digest")); err != nil { - return err - } - - if len(t.Digest) > 2097152 { - return xerrors.Errorf("Byte array in field t.Digest was too long") - } - - if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.Digest))); err != nil { - return err - } - - if _, err := cw.Write(t.Digest); err != nil { - return err - } - - return nil -} - -func (t *RejectDigestArguments) UnmarshalCBOR(r io.Reader) (err error) { - *t = RejectDigestArguments{} - - cr := cbg.NewCborReader(r) - - maj, extra, err := cr.ReadHeader() - if err != nil { - return err - } - defer func() { - if err == io.EOF { - err = io.ErrUnexpectedEOF - } - }() - - if maj != cbg.MajMap { - return fmt.Errorf("cbor input should be of type map") - } - - if extra > cbg.MaxLength { - return fmt.Errorf("RejectDigestArguments: map struct too large (%d)", extra) - } - - n := extra - - nameBuf := make([]byte, 6) - for i := uint64(0); i < n; i++ { - nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) - if err != nil { - return err - } - - if !ok { - // Field doesn't exist on this type, so ignore it - if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { - return err - } - continue - } - - switch string(nameBuf[:nameLen]) { - // t.Space (did.DID) (struct) - case "space": - - { - - if err := t.Space.UnmarshalCBOR(cr); err != nil { - return xerrors.Errorf("unmarshaling t.Space: %w", err) + return xerrors.Errorf("failed to read cid field t.Add: %w", err) } - } - // t.Digest (multihash.Multihash) (slice) - case "digest": + t.Add = c - maj, extra, err = cr.ReadHeader() - if err != nil { - return err - } - - if extra > 2097152 { - return fmt.Errorf("t.Digest: byte array too large (%d)", extra) - } - if maj != cbg.MajByteString { - return fmt.Errorf("expected byte array") - } - - if extra > 0 { - t.Digest = make([]uint8, extra) - } - - if _, err := io.ReadFull(cr, t.Digest); err != nil { - return err } default: @@ -1918,7 +1725,7 @@ func (t *RejectDigestArguments) UnmarshalCBOR(r io.Reader) (err error) { return nil } -func (t *RejectAllocationArguments) MarshalCBOR(w io.Writer) error { +func (t *RejectArguments) MarshalCBOR(w io.Writer) error { if t == nil { _, err := w.Write(cbg.CborNull) return err @@ -1926,23 +1733,7 @@ func (t *RejectAllocationArguments) MarshalCBOR(w io.Writer) error { cw := cbg.NewCborWriter(w) - if _, err := cw.Write([]byte{162}); err != nil { - return err - } - - // t.Space (did.DID) (struct) - if len("space") > 8192 { - return xerrors.Errorf("Value in field \"space\" was too long") - } - - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("space"))); err != nil { - return err - } - if _, err := cw.WriteString(string("space")); err != nil { - return err - } - - if err := t.Space.MarshalCBOR(cw); err != nil { + if _, err := cw.Write([]byte{161}); err != nil { return err } @@ -1965,8 +1756,8 @@ func (t *RejectAllocationArguments) MarshalCBOR(w io.Writer) error { return nil } -func (t *RejectAllocationArguments) UnmarshalCBOR(r io.Reader) (err error) { - *t = RejectAllocationArguments{} +func (t *RejectArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = RejectArguments{} cr := cbg.NewCborReader(r) @@ -1985,7 +1776,7 @@ func (t *RejectAllocationArguments) UnmarshalCBOR(r io.Reader) (err error) { } if extra > cbg.MaxLength { - return fmt.Errorf("RejectAllocationArguments: map struct too large (%d)", extra) + return fmt.Errorf("RejectArguments: map struct too large (%d)", extra) } n := extra @@ -2006,17 +1797,7 @@ func (t *RejectAllocationArguments) UnmarshalCBOR(r io.Reader) (err error) { } switch string(nameBuf[:nameLen]) { - // t.Space (did.DID) (struct) - case "space": - - { - - if err := t.Space.UnmarshalCBOR(cr); err != nil { - return xerrors.Errorf("unmarshaling t.Space: %w", err) - } - - } - // t.Allocation (cid.Cid) (struct) + // t.Allocation (cid.Cid) (struct) case "allocation": { @@ -2040,207 +1821,6 @@ func (t *RejectAllocationArguments) UnmarshalCBOR(r io.Reader) (err error) { return nil } -func (t *RejectArgumentsModel) MarshalCBOR(w io.Writer) error { - if t == nil { - _, err := w.Write(cbg.CborNull) - return err - } - - cw := cbg.NewCborWriter(w) - fieldCount := 3 - - if t.Digest == nil { - fieldCount-- - } - - if t.Allocation == nil { - fieldCount-- - } - - if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { - return err - } - - // t.Space (did.DID) (struct) - if len("space") > 8192 { - return xerrors.Errorf("Value in field \"space\" was too long") - } - - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("space"))); err != nil { - return err - } - if _, err := cw.WriteString(string("space")); err != nil { - return err - } - - if err := t.Space.MarshalCBOR(cw); err != nil { - return err - } - - // t.Digest (multihash.Multihash) (slice) - if t.Digest != nil { - - if len("digest") > 8192 { - return xerrors.Errorf("Value in field \"digest\" was too long") - } - - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("digest"))); err != nil { - return err - } - if _, err := cw.WriteString(string("digest")); err != nil { - return err - } - - if len(t.Digest) > 2097152 { - return xerrors.Errorf("Byte array in field t.Digest was too long") - } - - if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.Digest))); err != nil { - return err - } - - if _, err := cw.Write(t.Digest); err != nil { - return err - } - - } - - // t.Allocation (cid.Cid) (struct) - if t.Allocation != nil { - - if len("allocation") > 8192 { - return xerrors.Errorf("Value in field \"allocation\" was too long") - } - - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("allocation"))); err != nil { - return err - } - if _, err := cw.WriteString(string("allocation")); err != nil { - return err - } - - if t.Allocation == nil { - if _, err := cw.Write(cbg.CborNull); err != nil { - return err - } - } else { - if err := cbg.WriteCid(cw, *t.Allocation); err != nil { - return xerrors.Errorf("failed to write cid field t.Allocation: %w", err) - } - } - - } - return nil -} - -func (t *RejectArgumentsModel) UnmarshalCBOR(r io.Reader) (err error) { - *t = RejectArgumentsModel{} - - cr := cbg.NewCborReader(r) - - maj, extra, err := cr.ReadHeader() - if err != nil { - return err - } - defer func() { - if err == io.EOF { - err = io.ErrUnexpectedEOF - } - }() - - if maj != cbg.MajMap { - return fmt.Errorf("cbor input should be of type map") - } - - if extra > cbg.MaxLength { - return fmt.Errorf("RejectArgumentsModel: map struct too large (%d)", extra) - } - - n := extra - - nameBuf := make([]byte, 10) - for i := uint64(0); i < n; i++ { - nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) - if err != nil { - return err - } - - if !ok { - // Field doesn't exist on this type, so ignore it - if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { - return err - } - continue - } - - switch string(nameBuf[:nameLen]) { - // t.Space (did.DID) (struct) - case "space": - - { - - if err := t.Space.UnmarshalCBOR(cr); err != nil { - return xerrors.Errorf("unmarshaling t.Space: %w", err) - } - - } - // t.Digest (multihash.Multihash) (slice) - case "digest": - - maj, extra, err = cr.ReadHeader() - if err != nil { - return err - } - - if extra > 2097152 { - return fmt.Errorf("t.Digest: byte array too large (%d)", extra) - } - if maj != cbg.MajByteString { - return fmt.Errorf("expected byte array") - } - - if extra > 0 { - t.Digest = make([]uint8, extra) - } - - if _, err := io.ReadFull(cr, t.Digest); err != nil { - return err - } - - // t.Allocation (cid.Cid) (struct) - case "allocation": - - { - - b, err := cr.ReadByte() - if err != nil { - return err - } - if b != cbg.CborNull[0] { - if err := cr.UnreadByte(); err != nil { - return err - } - - c, err := cbg.ReadCid(cr) - if err != nil { - return xerrors.Errorf("failed to read cid field t.Allocation: %w", err) - } - - t.Allocation = &c - } - - } - - default: - // Field doesn't exist on this type, so ignore it - if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { - return err - } - } - } - - return nil -} func (t *ReplicateArguments) MarshalCBOR(w io.Writer) error { if t == nil { _, err := w.Write(cbg.CborNull) diff --git a/commands/blob/codec.go b/commands/blob/codec.go index ce50d2b..febaebc 100644 --- a/commands/blob/codec.go +++ b/commands/blob/codec.go @@ -6,9 +6,7 @@ import ( "fmt" "io" - "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/errors" - "github.com/ipfs/go-cid" "github.com/multiformats/go-multihash" ) @@ -122,94 +120,3 @@ func (s *BlobSpec) fromModel(m BlobSpecModel) error { } return nil } - -// RejectByDigest returns arguments rejecting space's allocation for the blob -// with digest. -func RejectByDigest(space did.DID, digest multihash.Multihash) RejectArguments { - return RejectArguments{byDigest: &RejectDigestArguments{Space: space, Digest: digest}} -} - -// RejectByAllocation returns arguments rejecting space's allocation made -// without a digest by the `/blob/allocate` task allocation. -func RejectByAllocation(space did.DID, allocation cid.Cid) RejectArguments { - return RejectArguments{byAllocation: &RejectAllocationArguments{Space: space, Allocation: allocation}} -} - -// Space returns the space whose allocation is rejected. -func (a RejectArguments) Space() did.DID { - switch { - case a.byDigest != nil: - return a.byDigest.Space - case a.byAllocation != nil: - return a.byAllocation.Space - } - return did.DID{} -} - -// Digest returns the digest of the rejected blob, when the arguments name one. -func (a RejectArguments) Digest() (multihash.Multihash, bool) { - if a.byDigest == nil { - return nil, false - } - return a.byDigest.Digest, true -} - -// Allocation returns the `/blob/allocate` task of the rejected allocation, -// when the arguments name one instead of a digest. -func (a RejectArguments) Allocation() (cid.Cid, bool) { - if a.byAllocation == nil { - return cid.Undef, false - } - return a.byAllocation.Allocation, true -} - -func (a RejectArguments) MarshalCBOR(w io.Writer) error { - switch { - case a.byDigest != nil && a.byAllocation == nil: - return a.byDigest.MarshalCBOR(w) - case a.byAllocation != nil && a.byDigest == nil: - return a.byAllocation.MarshalCBOR(w) - } - return errNoVariant -} - -func (a *RejectArguments) UnmarshalCBOR(r io.Reader) error { - var m RejectArgumentsModel - if err := m.UnmarshalCBOR(r); err != nil { - return err - } - return a.fromModel(m) -} - -func (a RejectArguments) MarshalDagJSON(w io.Writer) error { - switch { - case a.byDigest != nil && a.byAllocation == nil: - return a.byDigest.MarshalDagJSON(w) - case a.byAllocation != nil && a.byDigest == nil: - return a.byAllocation.MarshalDagJSON(w) - } - return errNoVariant -} - -func (a *RejectArguments) UnmarshalDagJSON(r io.Reader) error { - var m RejectArgumentsModel - if err := m.UnmarshalDagJSON(r); err != nil { - return err - } - return a.fromModel(m) -} - -func (a *RejectArguments) fromModel(m RejectArgumentsModel) error { - *a = RejectArguments{} - switch hasDigest := len(m.Digest) > 0; { - case hasDigest && m.Allocation == nil: - a.byDigest = &RejectDigestArguments{Space: m.Space, Digest: m.Digest} - case !hasDigest && m.Allocation != nil: - a.byAllocation = &RejectAllocationArguments{Space: m.Space, Allocation: *m.Allocation} - case hasDigest: - return errBothVariants - default: - return errNoVariant - } - return nil -} diff --git a/commands/blob/gen/main.go b/commands/blob/gen/main.go index a412e6b..d8a22d1 100644 --- a/commands/blob/gen/main.go +++ b/commands/blob/gen/main.go @@ -36,9 +36,7 @@ func main() { blob.RemoveArguments{}, blob.ReleaseArguments{}, blob.AbortArguments{}, - blob.RejectDigestArguments{}, - blob.RejectAllocationArguments{}, - blob.RejectArgumentsModel{}, + blob.RejectArguments{}, blob.ReplicateArguments{}, blob.ReplicateOK{}, blob.ListArguments{}, diff --git a/commands/blob/json_gen.go b/commands/blob/json_gen.go index 1ef1e3d..dc8690e 100644 --- a/commands/blob/json_gen.go +++ b/commands/blob/json_gen.go @@ -1615,52 +1615,22 @@ func (t *AbortArguments) MarshalDagJSON(w io.Writer) error { if err := jw.WriteObjectOpen(); err != nil { return err } - written := false - // t.Cause (cid.Cid) (struct) - if len("cause") > 8192 { - return fmt.Errorf("string in field \"cause\" was too long") + // t.Add (cid.Cid) (struct) + if len("add") > 8192 { + return fmt.Errorf("string in field \"add\" was too long") } - if err := jw.WriteString(string("cause")); err != nil { - return fmt.Errorf("writing string for field \"cause\": %w", err) + if err := jw.WriteString(string("add")); err != nil { + return fmt.Errorf("writing string for field \"add\": %w", err) } if err := jw.WriteObjectColon(); err != nil { return err } - if err := jw.WriteCid(t.Cause); err != nil { - return fmt.Errorf("writing CID for field t.Cause: %w", err) + if err := jw.WriteCid(t.Add); err != nil { + return fmt.Errorf("writing CID for field t.Add: %w", err) } - written = true - if t.Digest != nil { - if written { - if err := jw.WriteComma(); err != nil { - return err - } - } - } - - // t.Digest (multihash.Multihash) (slice) - if t.Digest != nil { - if len("digest") > 8192 { - return fmt.Errorf("string in field \"digest\" was too long") - } - if err := jw.WriteString(string("digest")); err != nil { - return fmt.Errorf("writing string for field \"digest\": %w", err) - } - if err := jw.WriteObjectColon(); err != nil { - return err - } - if len(t.Digest) > 2097152 { - return fmt.Errorf("byte array in field t.Digest was too long") - } - - if err := jw.WriteBytes(t.Digest); err != nil { - return fmt.Errorf("writing bytes for field t.Digest: %w", err) - } - - } if err := jw.WriteObjectClose(); err != nil { return err } @@ -1700,34 +1670,17 @@ func (t *AbortArguments) UnmarshalDagJSON(r io.Reader) (err error) { } switch name { - // t.Cause (cid.Cid) (struct) - case "cause": + // t.Add (cid.Cid) (struct) + case "add": { c, err := jr.ReadCid() if err != nil { - return fmt.Errorf("reading CID for field t.Cause: %w", err) + return fmt.Errorf("reading CID for field t.Add: %w", err) } - t.Cause = c + t.Add = c } - - // t.Digest (multihash.Multihash) (slice) - case "digest": - - { - bval, err := jr.ReadBytes(2097152) - if err != nil { - if errors.Is(err, jsg.ErrLimitExceeded) { - return fmt.Errorf("reading bytes for field t.Digest: byte array too large") - } - return fmt.Errorf("reading bytes for field t.Digest: %w", err) - } - if len(bval) > 0 { - t.Digest = []uint8(bval) - } - } - default: // Field doesn't exist on this type, so ignore it if err := jr.DiscardType(); err != nil { @@ -1750,140 +1703,7 @@ func (t *AbortArguments) UnmarshalDagJSON(r io.Reader) (err error) { return nil } -func (t *RejectDigestArguments) MarshalDagJSON(w io.Writer) error { - jw := jsg.NewDagJsonWriter(w) - if t == nil { - err := jw.WriteNull() - return err - } - if err := jw.WriteObjectOpen(); err != nil { - return err - } - written := false - - // t.Digest (multihash.Multihash) (slice) - if len("digest") > 8192 { - return fmt.Errorf("string in field \"digest\" was too long") - } - if err := jw.WriteString(string("digest")); err != nil { - return fmt.Errorf("writing string for field \"digest\": %w", err) - } - if err := jw.WriteObjectColon(); err != nil { - return err - } - if len(t.Digest) > 2097152 { - return fmt.Errorf("byte array in field t.Digest was too long") - } - - if err := jw.WriteBytes(t.Digest); err != nil { - return fmt.Errorf("writing bytes for field t.Digest: %w", err) - } - - written = true - if written { - if err := jw.WriteComma(); err != nil { - return err - } - } - - // t.Space (did.DID) (struct) - if len("space") > 8192 { - return fmt.Errorf("string in field \"space\" was too long") - } - if err := jw.WriteString(string("space")); err != nil { - return fmt.Errorf("writing string for field \"space\": %w", err) - } - if err := jw.WriteObjectColon(); err != nil { - return err - } - if err := t.Space.MarshalDagJSON(jw); err != nil { - return fmt.Errorf("marshaling field t.Space: %w", err) - } - if err := jw.WriteObjectClose(); err != nil { - return err - } - return nil -} -func (t *RejectDigestArguments) UnmarshalDagJSON(r io.Reader) (err error) { - *t = RejectDigestArguments{} - - jr := jsg.NewDagJsonReader(r) - defer func() { - if err == io.EOF { - err = io.ErrUnexpectedEOF - } - }() - if err := jr.ReadObjectOpen(); err != nil { - return fmt.Errorf("reading object open for RejectDigestArguments: %w", err) - } - close, err := jr.PeekObjectClose() - if err != nil { - return fmt.Errorf("peeking object close for RejectDigestArguments: %w", err) - } - if close { - if err := jr.ReadObjectClose(); err != nil { - return fmt.Errorf("reading object close for RejectDigestArguments: %w", err) - } - } else { - for i := uint64(0); i < 8192; i++ { - name, err := jr.ReadString(8192) - if err != nil { - if errors.Is(err, jsg.ErrLimitExceeded) { - return fmt.Errorf("reading string for field RejectDigestArguments: string too large") - } - return fmt.Errorf("reading string for field RejectDigestArguments: %w", err) - } - if err := jr.ReadObjectColon(); err != nil { - return fmt.Errorf("reading object colon for field RejectDigestArguments: %w", err) - } - switch name { - - // t.Digest (multihash.Multihash) (slice) - case "digest": - - { - bval, err := jr.ReadBytes(2097152) - if err != nil { - if errors.Is(err, jsg.ErrLimitExceeded) { - return fmt.Errorf("reading bytes for field t.Digest: byte array too large") - } - return fmt.Errorf("reading bytes for field t.Digest: %w", err) - } - if len(bval) > 0 { - t.Digest = []uint8(bval) - } - } - - // t.Space (did.DID) (struct) - case "space": - - if err := t.Space.UnmarshalDagJSON(jr); err != nil { - return fmt.Errorf("unmarshaling t.Space: %w", err) - } - - default: - // Field doesn't exist on this type, so ignore it - if err := jr.DiscardType(); err != nil { - return fmt.Errorf("ignoring field %s for RejectDigestArguments: %w", name, err) - } - } - - close, err := jr.ReadObjectCloseOrComma() - if err != nil { - return fmt.Errorf("reading object close or comma for field RejectDigestArguments: %w", err) - } - if close { - break - } - if i == 8192-1 { - return fmt.Errorf("map too large for RejectDigestArguments") - } - } - } - - return nil -} -func (t *RejectAllocationArguments) MarshalDagJSON(w io.Writer) error { +func (t *RejectArguments) MarshalDagJSON(w io.Writer) error { jw := jsg.NewDagJsonWriter(w) if t == nil { err := jw.WriteNull() @@ -1892,7 +1712,6 @@ func (t *RejectAllocationArguments) MarshalDagJSON(w io.Writer) error { if err := jw.WriteObjectOpen(); err != nil { return err } - written := false // t.Allocation (cid.Cid) (struct) if len("allocation") > 8192 { @@ -1909,33 +1728,13 @@ func (t *RejectAllocationArguments) MarshalDagJSON(w io.Writer) error { return fmt.Errorf("writing CID for field t.Allocation: %w", err) } - written = true - if written { - if err := jw.WriteComma(); err != nil { - return err - } - } - - // t.Space (did.DID) (struct) - if len("space") > 8192 { - return fmt.Errorf("string in field \"space\" was too long") - } - if err := jw.WriteString(string("space")); err != nil { - return fmt.Errorf("writing string for field \"space\": %w", err) - } - if err := jw.WriteObjectColon(); err != nil { - return err - } - if err := t.Space.MarshalDagJSON(jw); err != nil { - return fmt.Errorf("marshaling field t.Space: %w", err) - } if err := jw.WriteObjectClose(); err != nil { return err } return nil } -func (t *RejectAllocationArguments) UnmarshalDagJSON(r io.Reader) (err error) { - *t = RejectAllocationArguments{} +func (t *RejectArguments) UnmarshalDagJSON(r io.Reader) (err error) { + *t = RejectArguments{} jr := jsg.NewDagJsonReader(r) defer func() { @@ -1944,27 +1743,27 @@ func (t *RejectAllocationArguments) UnmarshalDagJSON(r io.Reader) (err error) { } }() if err := jr.ReadObjectOpen(); err != nil { - return fmt.Errorf("reading object open for RejectAllocationArguments: %w", err) + return fmt.Errorf("reading object open for RejectArguments: %w", err) } close, err := jr.PeekObjectClose() if err != nil { - return fmt.Errorf("peeking object close for RejectAllocationArguments: %w", err) + return fmt.Errorf("peeking object close for RejectArguments: %w", err) } if close { if err := jr.ReadObjectClose(); err != nil { - return fmt.Errorf("reading object close for RejectAllocationArguments: %w", err) + return fmt.Errorf("reading object close for RejectArguments: %w", err) } } else { for i := uint64(0); i < 8192; i++ { name, err := jr.ReadString(8192) if err != nil { if errors.Is(err, jsg.ErrLimitExceeded) { - return fmt.Errorf("reading string for field RejectAllocationArguments: string too large") + return fmt.Errorf("reading string for field RejectArguments: string too large") } - return fmt.Errorf("reading string for field RejectAllocationArguments: %w", err) + return fmt.Errorf("reading string for field RejectArguments: %w", err) } if err := jr.ReadObjectColon(); err != nil { - return fmt.Errorf("reading object colon for field RejectAllocationArguments: %w", err) + return fmt.Errorf("reading object colon for field RejectArguments: %w", err) } switch name { @@ -1979,209 +1778,22 @@ func (t *RejectAllocationArguments) UnmarshalDagJSON(r io.Reader) (err error) { t.Allocation = c } - - // t.Space (did.DID) (struct) - case "space": - - if err := t.Space.UnmarshalDagJSON(jr); err != nil { - return fmt.Errorf("unmarshaling t.Space: %w", err) - } - - default: - // Field doesn't exist on this type, so ignore it - if err := jr.DiscardType(); err != nil { - return fmt.Errorf("ignoring field %s for RejectAllocationArguments: %w", name, err) - } - } - - close, err := jr.ReadObjectCloseOrComma() - if err != nil { - return fmt.Errorf("reading object close or comma for field RejectAllocationArguments: %w", err) - } - if close { - break - } - if i == 8192-1 { - return fmt.Errorf("map too large for RejectAllocationArguments") - } - } - } - - return nil -} -func (t *RejectArgumentsModel) MarshalDagJSON(w io.Writer) error { - jw := jsg.NewDagJsonWriter(w) - if t == nil { - err := jw.WriteNull() - return err - } - if err := jw.WriteObjectOpen(); err != nil { - return err - } - written := false - - // t.Allocation (cid.Cid) (struct) - if t.Allocation != nil { - if len("allocation") > 8192 { - return fmt.Errorf("string in field \"allocation\" was too long") - } - if err := jw.WriteString(string("allocation")); err != nil { - return fmt.Errorf("writing string for field \"allocation\": %w", err) - } - if err := jw.WriteObjectColon(); err != nil { - return err - } - - if t.Allocation == nil { - if err := jw.WriteNull(); err != nil { - return fmt.Errorf("writing null for field t.Allocation: %w", err) - } - } else { - if err := jw.WriteCid(*t.Allocation); err != nil { - return fmt.Errorf("writing CID for field t.Allocation: %w", err) - } - } - - written = true - } - if t.Digest != nil { - if written { - if err := jw.WriteComma(); err != nil { - return err - } - } - } - - // t.Digest (multihash.Multihash) (slice) - if t.Digest != nil { - if len("digest") > 8192 { - return fmt.Errorf("string in field \"digest\" was too long") - } - if err := jw.WriteString(string("digest")); err != nil { - return fmt.Errorf("writing string for field \"digest\": %w", err) - } - if err := jw.WriteObjectColon(); err != nil { - return err - } - if len(t.Digest) > 2097152 { - return fmt.Errorf("byte array in field t.Digest was too long") - } - - if err := jw.WriteBytes(t.Digest); err != nil { - return fmt.Errorf("writing bytes for field t.Digest: %w", err) - } - - written = true - } - if written { - if err := jw.WriteComma(); err != nil { - return err - } - } - - // t.Space (did.DID) (struct) - if len("space") > 8192 { - return fmt.Errorf("string in field \"space\" was too long") - } - if err := jw.WriteString(string("space")); err != nil { - return fmt.Errorf("writing string for field \"space\": %w", err) - } - if err := jw.WriteObjectColon(); err != nil { - return err - } - if err := t.Space.MarshalDagJSON(jw); err != nil { - return fmt.Errorf("marshaling field t.Space: %w", err) - } - if err := jw.WriteObjectClose(); err != nil { - return err - } - return nil -} -func (t *RejectArgumentsModel) UnmarshalDagJSON(r io.Reader) (err error) { - *t = RejectArgumentsModel{} - - jr := jsg.NewDagJsonReader(r) - defer func() { - if err == io.EOF { - err = io.ErrUnexpectedEOF - } - }() - if err := jr.ReadObjectOpen(); err != nil { - return fmt.Errorf("reading object open for RejectArgumentsModel: %w", err) - } - close, err := jr.PeekObjectClose() - if err != nil { - return fmt.Errorf("peeking object close for RejectArgumentsModel: %w", err) - } - if close { - if err := jr.ReadObjectClose(); err != nil { - return fmt.Errorf("reading object close for RejectArgumentsModel: %w", err) - } - } else { - for i := uint64(0); i < 8192; i++ { - name, err := jr.ReadString(8192) - if err != nil { - if errors.Is(err, jsg.ErrLimitExceeded) { - return fmt.Errorf("reading string for field RejectArgumentsModel: string too large") - } - return fmt.Errorf("reading string for field RejectArgumentsModel: %w", err) - } - if err := jr.ReadObjectColon(); err != nil { - return fmt.Errorf("reading object colon for field RejectArgumentsModel: %w", err) - } - switch name { - - // t.Allocation (cid.Cid) (struct) - case "allocation": - { - - c, err := jr.ReadCidOrNull() - if err != nil { - return fmt.Errorf("reading CID or null for field t.Allocation: %w", err) - } - t.Allocation = c - - } - - // t.Digest (multihash.Multihash) (slice) - case "digest": - - { - bval, err := jr.ReadBytes(2097152) - if err != nil { - if errors.Is(err, jsg.ErrLimitExceeded) { - return fmt.Errorf("reading bytes for field t.Digest: byte array too large") - } - return fmt.Errorf("reading bytes for field t.Digest: %w", err) - } - if len(bval) > 0 { - t.Digest = []uint8(bval) - } - } - - // t.Space (did.DID) (struct) - case "space": - - if err := t.Space.UnmarshalDagJSON(jr); err != nil { - return fmt.Errorf("unmarshaling t.Space: %w", err) - } - default: // Field doesn't exist on this type, so ignore it if err := jr.DiscardType(); err != nil { - return fmt.Errorf("ignoring field %s for RejectArgumentsModel: %w", name, err) + return fmt.Errorf("ignoring field %s for RejectArguments: %w", name, err) } } close, err := jr.ReadObjectCloseOrComma() if err != nil { - return fmt.Errorf("reading object close or comma for field RejectArgumentsModel: %w", err) + return fmt.Errorf("reading object close or comma for field RejectArguments: %w", err) } if close { break } if i == 8192-1 { - return fmt.Errorf("map too large for RejectArgumentsModel") + return fmt.Errorf("map too large for RejectArguments") } } } diff --git a/commands/blob/reject.go b/commands/blob/reject.go index 9abf29e..b2ac517 100644 --- a/commands/blob/reject.go +++ b/commands/blob/reject.go @@ -19,15 +19,16 @@ type RejectOK = commands.Unit // // Served by storage nodes (subject = the provider DID, invoked by the // upload service under its registration delegation, typically translating a -// client `/blob/abort`). The node drops the space's allocation and deletes -// any received bytes once no space holds an allocation or acceptance for -// the digest. +// client `/blob/abort`). The allocation is named by the `/blob/allocate` task +// that made it, and the node knows which space and blob it is for. The node +// drops the allocation and deletes any received bytes once no space holds an +// allocation or acceptance for them. // -// A blob that THE INVOKING SPACE has accepted is refused with BlobAccepted — -// a space's accepted blobs are released via `/blob/remove`, never rejected. -// The guard is scoped to the invoking space, not the digest: another space's -// acceptance of the same bytes must not block the reject — the node simply -// drops this space's allocation and retains the bytes for the space that +// A blob that the allocation's space has accepted is refused with +// BlobAccepted — a space's accepted blobs are released via `/blob/remove`, +// never rejected. The guard is scoped to that space, not the digest: another +// space's acceptance of the same bytes must not block the reject — the node +// simply drops this allocation and retains the bytes for the space that // still claims them. // // Idempotent: rejecting an unknown or already-rejected blob succeeds. diff --git a/commands/blob/spec_test.go b/commands/blob/spec_test.go index 253fcec..629aded 100644 --- a/commands/blob/spec_test.go +++ b/commands/blob/spec_test.go @@ -116,50 +116,11 @@ func TestBlobSpecRejectsInvalidUnion(t *testing.T) { } } -func TestRejectArgumentsByAllocation(t *testing.T) { - space, alloc := testutil.RandomDID(t), testutil.RandomCID(t) - in := blob.RejectByAllocation(space, alloc) - - var buf bytes.Buffer - require.NoError(t, in.MarshalCBOR(&buf)) - var out blob.RejectArguments - require.NoError(t, out.UnmarshalCBOR(&buf)) - require.Equal(t, space, out.Space()) - _, hasDigest := out.Digest() - require.False(t, hasDigest) - got, ok := out.Allocation() - require.True(t, ok) - require.Equal(t, alloc, got) - - var js bytes.Buffer - require.NoError(t, in.MarshalDagJSON(&js)) - var outJS blob.RejectArguments - require.NoError(t, outJS.UnmarshalDagJSON(&js)) - require.Equal(t, in, outJS) - - require.Error(t, blob.RejectArguments{}.MarshalCBOR(&buf)) - var bad blob.RejectArguments - require.Error(t, bad.UnmarshalDagJSON(strings.NewReader(`{"space":"did:key:z6Mk"}`))) -} - -// An abort names only the cause when the add named only a digest code. -func TestAbortArgumentsWithoutDigest(t *testing.T) { - in := blob.AbortArguments{Cause: testutil.RandomCID(t)} - var buf bytes.Buffer - require.NoError(t, in.MarshalCBOR(&buf)) - var out blob.AbortArguments - require.NoError(t, out.UnmarshalCBOR(&buf)) - require.Empty(t, out.Digest) - require.Equal(t, in.Cause, out.Cause) -} - -// A union decodes through its model, which holds the fields of both variants: -// exactly one variant's field must be set, whichever codec carries it. -func TestUnionModelsRejectInvalidCBOR(t *testing.T) { +// A spec decodes through its model, which holds the fields of both variants: +// exactly one variant's field must be set. +func TestBlobSpecModelRejectsInvalidCBOR(t *testing.T) { digest := testutil.RandomMultihash(t) code := uint64(multicodec.Sha2_256) - alloc := testutil.RandomCID(t) - space := testutil.RandomDID(t) for name, m := range map[string]blob.BlobSpecModel{ "neither": {Size: 1}, @@ -172,21 +133,6 @@ func TestUnionModelsRejectInvalidCBOR(t *testing.T) { require.Error(t, out.UnmarshalCBOR(&buf)) }) } - for name, m := range map[string]blob.RejectArgumentsModel{ - "neither": {Space: space}, - "both": {Space: space, Digest: digest, Allocation: &alloc}, - } { - t.Run("reject "+name, func(t *testing.T) { - var buf bytes.Buffer - require.NoError(t, m.MarshalCBOR(&buf)) - var out blob.RejectArguments - require.Error(t, out.UnmarshalCBOR(&buf)) - - var js bytes.Buffer - require.NoError(t, m.MarshalDagJSON(&js)) - require.Error(t, out.UnmarshalDagJSON(&js)) - }) - } } // The model's generated decoder bounds each field as it reads it, so a spec diff --git a/commands/blob/types.go b/commands/blob/types.go index 1b37963..5a439b9 100644 --- a/commands/blob/types.go +++ b/commands/blob/types.go @@ -123,55 +123,21 @@ type ReleaseArguments struct { Cause cid.Cid `cborgen:"cause" dagjsongen:"cause"` } -// AbortArguments abandons the invoking space's in-flight upload of the -// parked (never-accepted) blob identified by Digest. The space is the -// invocation subject. Cause is the `/blob/add` task link: the upload -// service uses it to recover which storage node holds the parked blob — a -// parked blob has no registration or acceptance to look the node up by. -// Digest is omitted when the `/blob/add` named only a digest code: Cause then -// identifies the upload on its own. +// AbortArguments abandons the space's in-flight upload of a parked +// (never-accepted) blob. The space is the invocation subject. Add is the +// `/blob/add` task link: the upload service follows its receipt chain to the +// storage node holding the upload and the allocation it made there. A parked +// blob has no registration or acceptance to look the node up by. type AbortArguments struct { - Digest multihash.Multihash `cborgen:"digest,omitempty" dagjsongen:"digest,omitempty"` - Cause cid.Cid `cborgen:"cause" dagjsongen:"cause"` + Add cid.Cid `cborgen:"add" dagjsongen:"add"` } -// RejectArguments drops Space's allocation for a parked (never-accepted) -// blob on the storage node; the node deletes any received bytes once no space -// holds an allocation or acceptance for the digest. It is a union of exactly -// one of: -// -// - [RejectDigestArguments]: the allocation of the blob with a digest; -// - [RejectAllocationArguments]: an allocation made without a digest, -// identified by the `/blob/allocate` task that created it. -// -// Encoding or decoding arguments that hold neither or both fails. The codec is -// hand-written (codec.go); the variants' codecs are generated. +// RejectArguments drops the allocation Allocation names, the link to the +// `/blob/allocate` task that made it, for a parked (never-accepted) blob on +// the storage node. The node knows the space and blob the allocation is for, +// and deletes any received bytes once no space holds an allocation or +// acceptance for them. type RejectArguments struct { - byDigest *RejectDigestArguments - byAllocation *RejectAllocationArguments -} - -// RejectArgumentsModel is the encoded form of [RejectArguments]: the fields of -// both of its variants, so the arguments decode in one bounded pass. Exactly -// one of Digest and Allocation is set in valid arguments. -type RejectArgumentsModel struct { - Space did.DID `cborgen:"space" dagjsongen:"space"` - Digest multihash.Multihash `cborgen:"digest,omitempty" dagjsongen:"digest,omitempty"` - Allocation *cid.Cid `cborgen:"allocation,omitempty" dagjsongen:"allocation,omitempty"` -} - -// RejectDigestArguments rejects Space's allocation for the blob identified by -// Digest. -type RejectDigestArguments struct { - Space did.DID `cborgen:"space" dagjsongen:"space"` - Digest multihash.Multihash `cborgen:"digest" dagjsongen:"digest"` -} - -// RejectAllocationArguments rejects Space's allocation made without a digest, -// identified by Allocation: the link to the `/blob/allocate` task that -// created it. -type RejectAllocationArguments struct { - Space did.DID `cborgen:"space" dagjsongen:"space"` Allocation cid.Cid `cborgen:"allocation" dagjsongen:"allocation"` }