From 87c479a2dcd480400c1b6267dfd74623b296c452 Mon Sep 17 00:00:00 2001 From: diamondburned Date: Thu, 3 Nov 2022 02:30:25 -0700 Subject: [PATCH] gateway: Use PartialUnmarshal for ReadyEventExtras --- gateway/events.go | 6 +++--- utils/json/json.go | 47 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/gateway/events.go b/gateway/events.go index 6a31ee9..1a337ab 100644 --- a/gateway/events.go +++ b/gateway/events.go @@ -692,7 +692,7 @@ func (r *ReadyEvent) UnmarshalJSON(b []byte) error { // Optionally unmarshal ReadyEventExtras. if !r.User.Bot { - r.ExtrasDecodeError = json.Unmarshal(b, &r.ReadyEventExtras) + r.ExtrasDecodeErrors = json.PartialUnmarshal(b, &r.ReadyEventExtras) } if ReadyEventKeepRaw { @@ -723,9 +723,9 @@ type ( // RawEventBody is the raw JSON body for the Ready event. It is only // available if ReadyEventKeepRaw is true. RawEventBody json.Raw - // ExtrasDecodeError will be non-nil if there was an error decoding the + // ExtrasDecodeErrors will be non-nil if there were errors decoding the // ReadyEventExtras. - ExtrasDecodeError error + ExtrasDecodeErrors []error } // ReadState is a single ReadState entry. It is undocumented. diff --git a/utils/json/json.go b/utils/json/json.go index 478bb42..3b94a35 100644 --- a/utils/json/json.go +++ b/utils/json/json.go @@ -5,6 +5,8 @@ package json import ( "encoding/json" "io" + "reflect" + "unsafe" ) type Driver interface { @@ -55,3 +57,48 @@ func DecodeStream(r io.Reader, v interface{}) error { func EncodeStream(w io.Writer, v interface{}) error { return Default.EncodeStream(w, v) } + +// PartialUnmarshal partially unmarshals the JSON in b onto v. Fields that +// cannot be unmarshaled will be left as their zero values. Fields that can be +// marshaled will be unmarshaled and its name will be added to the returned +// slice. +// +// Only use this for the most cursed of JSONs, such as ones coming from Discord. +// Try not to use this as much as possible. +func PartialUnmarshal(b []byte, v interface{}) []error { + var errs []error + + dstv := reflect.Indirect(reflect.ValueOf(v)) + dstt := dstv.Type() + + // ptrVal will be used by reflect to store our temporary object. This allows + // us to free up n heap allocations just for one pointer. + var ptrVal struct { + _ *struct{} + } + + dstfields := dstt.NumField() + for i := 0; i < dstfields; i++ { + dstfield := dstt.Field(i) + + // Create us a custom struct with this one field, except its type is a + // pointer type. We prefer to do this over parsing JSON's tags. + fake := reflect.NewAt(reflect.StructOf([]reflect.StructField{ + { + Name: dstfield.Name, + Type: reflect.PtrTo(dstfield.Type), + Tag: dstfield.Tag, + }, + }), unsafe.Pointer(&ptrVal)) + + // We can use this pointer to set the value of the field. + fake.Elem().Field(0).Set(dstv.Field(i).Addr()) + + // Unmarshal into this struct. + if err := json.Unmarshal(b, fake.Interface()); err != nil { + errs = append(errs, err) + } + } + + return errs +}