fix: use registered ext coders for named non-struct types (#55) - #110
Open
youdie006 wants to merge 1 commit into
Open
fix: use registered ext coders for named non-struct types (#55)#110youdie006 wants to merge 1 commit into
youdie006 wants to merge 1 commit into
Conversation
AddExtCoder/AddExtEncoder accept an ext.Encoder whose Type() may be any reflect.Type, but the ext registry was only consulted in the struct dispatch (calcStruct/writeStruct, and setStruct on decode). The general dispatch (calcSize/create on encode, decode on decode) switches purely on reflect.Kind, so an ext coder registered for a named non-struct type (e.g. type Role uint8, the common Go "enum" pattern) was silently ignored at the top level, inside slices, and as a struct field. time.Time works through the same API only because its kind is Struct. Marshal(Role(1)) returned 0x01 (plain fixint) instead of the fixext frame d4 02 01, and because the value was never encoded as an ext, Unmarshal could not recover it - the round-trip was lossy. Consult the ext registry by rv.Type() at the top of the general dispatch functions, mirroring what the struct path already does (calcSize -> CalcByteSize, create -> WriteToBytes, guarded by rv.IsValid() for omitted-field zero Values; decode mirrors setStruct's ext dispatch). When no ext coder matches, control falls through to the existing kind switch, so non-ext types, the struct path and time.Time are unaffected. Fixes shamaton#55.
There was a problem hiding this comment.
Pull request overview
Fixes a correctness gap in the ext-coder dispatch so that ext coders registered for named, non-struct Go types (e.g., enum-style type Role uint8) are honored during general encoding/decoding paths, restoring lossless round-trips.
Changes:
- Encode: consult the ext encoder registry by
rv.Type()before the kind switch incalcSizeandcreate. - Decode: consult the ext decoder registry before the kind switch in
decode, mirroring the struct-path ext handling. - Add tests covering named non-struct ext encoding and end-to-end Marshal/Unmarshal round-trip (top-level + slice).
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| internal/encoding/ext_test.go | Adds a focused internal test ensuring a named uint8 type uses fixext frames (top-level + slice). |
| internal/encoding/encoding.go | Adds ext-dispatch to the general encode size/write paths (not just the struct path). |
| internal/decoding/decoding.go | Adds ext-dispatch to the general decode path (not just the struct path). |
| ext_nonstruct_test.go | Adds an end-to-end public API test for Marshal/Unmarshal round-trips of named non-struct types. |
Suppressed comments (1)
internal/encoding/encoding.go:272
- Same as calcSize: this ext fast-path runs before the kind-based nil handling. If an ext encoder is registered for a pointer/slice/map type, nil values will be routed to WriteToBytes instead of encoding as nil, which is a behavior change vs the existing switch and may be surprising/unsafe.
if rv.IsValid() {
for i := range extCoders {
if extCoders[i].Type() == rv.Type() {
return extCoders[i].WriteToBytes(rv, offset, &e.d)
}
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
|
||
| // ext types: honor a registered ext decoder for any kind, not just structs | ||
| // (mirrors setStruct). Falls through to the kind switch when nothing matches. | ||
| if isExt, _, extErr := d.extEndOffset(offset); extErr == nil && isExt { |
Comment on lines
+66
to
+74
| // ext types: honor a registered ext encoder for any kind, not just structs | ||
| // (mirrors calcStruct). Falls through to the kind switch when nothing matches. | ||
| if rv.IsValid() { | ||
| for i := range extCoders { | ||
| if extCoders[i].Type() == rv.Type() { | ||
| return extCoders[i].CalcByteSize(rv) | ||
| } | ||
| } | ||
| } |
Comment on lines
+66
to
+70
| // ext types: honor a registered ext encoder for any kind, not just structs | ||
| // (mirrors calcStruct). Falls through to the kind switch when nothing matches. | ||
| if rv.IsValid() { | ||
| for i := range extCoders { | ||
| if extCoders[i].Type() == rv.Type() { |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Repro
Register an ext coder for a named non-struct type (as commonly used for Go "enums"):
msgpack.Marshal(Role(1))returns0x01(plain fixint) instead of the fixext framed4 02 01.msgpack.Marshal([]Role{1, 2})returns an array of plain ints (92 01 02) instead of ext frames (92 d4 02 01 d4 02 02).Because the value is never encoded as an ext,
Unmarshalcannot recover it either, so the round-trip is lossy.Root cause
AddExtCoder/AddExtEncoderaccept anext.EncoderwhoseType()may be anyreflect.Type, but the ext registry was only consulted in the struct dispatch (calcStruct/writeStruct, andsetStructon decode). The general dispatch (calcSize/createon encode,decodeon decode) switches purely onreflect.Kind, so an ext coder registered for a named non-struct type is silently ignored at the top level, inside slices, and as a struct field.This is a self-inconsistency:
time.Timeworks through the sameAddExtCoderAPI only because its kind isStruct; an equally-valid ext coder for a named int is dropped. The public API accepts anyreflect.Typewithout restriction, so ignoring it is a correctness bug, not a documented limitation.Fix
Consult the ext registry by
rv.Type()at the top of the general dispatch functions, mirroring exactly what the struct path already does:calcSize->CalcByteSize,create->WriteToBytes(guarded byrv.IsValid()for omitted-field zero Values).decodemirrorssetStruct's ext dispatch, so a fixext frame is restored into a named non-struct target.When no ext coder matches, control falls through to the existing kind switch, so non-ext types, the struct path, and
time.Timeare unaffected.Tests
internal/encoding/ext_test.go: asserts a nameduint8ext coder produces the fixext frame at the top level and inside a slice (not plain fixints).ext_nonstruct_test.go: fullMarshal->Unmarshalround-trip (top level and slice) proving the value is restored.go test ./...,gofmt -l ., andgo vet ./...are all clean.Reported by @Beefster09.
This change was made with AI assistance and reviewed by me before submission.