Skip to content

Commit

Permalink
Return user error when CCF encodes attachment field
Browse files Browse the repository at this point in the history
The CCF encoder currently requires field values and types to match.

However, composite values that have attachment field don't satisfy
this requirement because:
- composite field values INCLUDE attachment field values
- composite field types EXCLUDE attachment field types

Currently, the encoder rejects this mismatch with a panic.

This commit makes CCF encoder return a user error rather than panic
for the caller to handle when this mismatch is encountered.
  • Loading branch information
fxamacker committed Jul 25, 2024
1 parent d39a165 commit c985da4
Show file tree
Hide file tree
Showing 2 changed files with 117 additions and 1 deletion.
66 changes: 66 additions & 0 deletions encoding/ccf/ccf_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,11 @@ import (
"github.com/onflow/cadence/encoding/ccf"
"github.com/onflow/cadence/runtime"
"github.com/onflow/cadence/runtime/common"
"github.com/onflow/cadence/runtime/errors"
"github.com/onflow/cadence/runtime/interpreter"
"github.com/onflow/cadence/runtime/sema"
"github.com/onflow/cadence/runtime/tests/checker"
"github.com/onflow/cadence/runtime/tests/runtime_utils"
"github.com/onflow/cadence/runtime/tests/utils"
)

Expand Down Expand Up @@ -17108,3 +17110,67 @@ func TestDecodeFunctionTypeBackwardCompatibility(t *testing.T) {

testDecode(t, data, val)
}

func TestEncodeEventWithAttachement(t *testing.T) {
script := `
access(all) struct S {
access(all) let x: Int
init(x: Int) {
self.x = x
}
}
access(all) attachment A for S {
access(all) let y: Int
init(y: Int) {
self.y = y
}
}
access(all) event Foo(s: S)
access(all)
fun main() {
let s = attach A(y: 3) to S(x: 4)
emit Foo(s: s)
}
`

v := exportEventFromScript(t, script)

_, err := ccf.Encode(v)

var attachmentFieldError ccf.AttachmentFieldNotSupportedEncodingError
require.ErrorAs(t, err, &attachmentFieldError)
require.Implements(t, (*errors.UserError)(nil), attachmentFieldError)
}

func exportEventFromScript(t *testing.T, script string) cadence.Event {
rt := runtime_utils.NewTestInterpreterRuntime()

var events []cadence.Event

inter := &runtime_utils.TestRuntimeInterface{
OnEmitEvent: func(event cadence.Event) error {
events = append(events, event)
return nil
},
}

_, err := rt.ExecuteScript(
runtime.Script{
Source: []byte(script),
},
runtime.Context{
Interface: inter,
Location: common.ScriptLocation{},
},
)

require.NoError(t, err)
require.Len(t, events, 1)

event := events[0]

return event
}
52 changes: 51 additions & 1 deletion encoding/ccf/encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,40 @@ func (em *encMode) NewEncoder(w io.Writer) *Encoder {

var defaultEncMode = &encMode{}

// AttachmentFieldNotSupportedEncodingError is a user error that is returned
// when encoding composite value (such as Event) that has cadence.Attachment field.
type AttachmentFieldNotSupportedEncodingError struct {
compositeType string
fieldCount int
fieldTypeCount int
}

func newAttachmentFieldNotSupportedError(
compositeType string,
fieldCount int,
fieldTypeCount int,
) error {
return AttachmentFieldNotSupportedEncodingError{
compositeType: compositeType,
fieldCount: fieldCount,
fieldTypeCount: fieldTypeCount,
}
}

func (e AttachmentFieldNotSupportedEncodingError) Error() string {
return fmt.Sprintf(
"encoding attachment field in composite value isn't supported: %s field count %d doesn't match declared field type count %d",
e.compositeType,
e.fieldCount,
e.fieldTypeCount,
)
}

func (e AttachmentFieldNotSupportedEncodingError) IsUserError() {
}

var _ cadenceErrors.UserError = AttachmentFieldNotSupportedEncodingError{}

// Encode returns the CCF-encoded representation of the given value
// by using default CCF encoding options. This function returns an
// error if the Cadence value cannot be represented in CCF.
Expand Down Expand Up @@ -219,7 +253,7 @@ func (e *Encoder) Encode(value cadence.Value) (err error) {
// Add context to error if there is any.
if err != nil {
err = fmt.Errorf(
"ccf: failed to encode value (type %T, %q): %s",
"ccf: failed to encode value (type %T, %q): %w",
value,
value.Type().ID(),
err,
Expand Down Expand Up @@ -1102,6 +1136,22 @@ func (e *Encoder) encodeComposite(
staticFieldTypes := getCompositeTypeFields(typ)

if len(staticFieldTypes) != len(fields) {

// The CCF encoder requires field values and types to match. However,
// composite values that have attachment field don't satisfy this requirement because:
// - composite field values INCLUDE attachment field values
// - composite field types EXCLUDE attachment field types
// Given this, CCF encoder will return a user error if composite value has attachment field.
for _, f := range fields {
if _, ok := f.(cadence.Attachment); ok {
panic(newAttachmentFieldNotSupportedError(
typ.ID(),
len(fields),
len(staticFieldTypes),
))
}
}

panic(cadenceErrors.NewUnexpectedError(
"%s field count %d doesn't match declared field type count %d",
typ.ID(),
Expand Down

0 comments on commit c985da4

Please sign in to comment.