Anonymous field handling for NBT

This commit is contained in:
Tnze
2023-04-26 21:51:41 +08:00
parent 9582bc2a7e
commit 078aaba156
8 changed files with 377 additions and 72 deletions

View File

@ -6,7 +6,7 @@ The API is very similar to the standard library `encoding/json`.
(But fix some its problem) (But fix some its problem)
If you (high probability) have used that, it is easy to use this. If you (high probability) have used that, it is easy to use this.
## Supported Struct Tags ## Supported Struct Tags and Options
- `nbt` - The primary tag name. See below. - `nbt` - The primary tag name. See below.
- `nbtkey` - The key name of the field (Used to support commas `,` in tag names) - `nbtkey` - The key name of the field (Used to support commas `,` in tag names)
@ -49,7 +49,7 @@ type MyStruct struct {
} }
``` ```
### The `nbtkey` ### The `nbtkey` tag
Common issue with JSON standard libraries: inability to specify keys containing commas for structures. Common issue with JSON standard libraries: inability to specify keys containing commas for structures.
(e.g `{"a,b" : "c"}`) (e.g `{"a,b" : "c"}`)

View File

@ -8,6 +8,7 @@ import (
"io" "io"
"math" "math"
"reflect" "reflect"
"strings"
) )
// Unmarshal decode binary NBT data and fill into v // Unmarshal decode binary NBT data and fill into v
@ -351,11 +352,19 @@ func (d *Decoder) unmarshal(val reflect.Value, tagType byte) error {
} }
case TagCompound: case TagCompound:
u, ut, val, assign := indirect(val, false)
if assign != nil {
defer assign()
}
if u != nil {
return u.UnmarshalNBT(tagType, d.r)
}
if ut != nil {
return errors.New("cannot decode TagCompound as string")
}
switch vk := val.Kind(); vk { switch vk := val.Kind(); vk {
default:
return errors.New("cannot parse TagCompound as " + vk.String())
case reflect.Struct: case reflect.Struct:
tinfo := typeFields(val.Type()) fields := cachedTypeFields(val.Type())
for { for {
tt, tn, err := d.readTag() tt, tn, err := d.readTag()
if err != nil { if err != nil {
@ -364,9 +373,37 @@ func (d *Decoder) unmarshal(val reflect.Value, tagType byte) error {
if tt == TagEnd { if tt == TagEnd {
break break
} }
field := tinfo.findIndexByName(tn) var f *field
if field != -1 { if i, ok := fields.nameIndex[tn]; ok {
err = d.unmarshal(val.Field(field), tt) f = &fields.list[i]
} else {
// Fall back to linear search.
for i := range fields.list {
ff := &fields.list[i]
if strings.EqualFold(ff.name, tn) {
f = ff
break
}
}
}
if f != nil {
val := val
for _, i := range f.index {
if val.Kind() == reflect.Pointer {
if val.IsNil() {
// If a struct embeds a pointer to an unexported type,
// it is not possible to set a newly allocated value
// since the field is unexported.
if !val.CanSet() {
return fmt.Errorf("cannot set embedded pointer to unexported struct: %v", val.Type().Elem())
}
val.Set(reflect.New(val.Type().Elem()))
}
val = val.Elem()
}
val = val.Field(i)
}
err = d.unmarshal(val, tt)
if err != nil { if err != nil {
return fmt.Errorf("fail to decode tag %q: %w", tn, err) return fmt.Errorf("fail to decode tag %q: %w", tn, err)
} }
@ -377,11 +414,12 @@ func (d *Decoder) unmarshal(val reflect.Value, tagType byte) error {
} }
} }
case reflect.Map: case reflect.Map:
if val.Type().Key().Kind() != reflect.String { vt := val.Type()
if vt.Key().Kind() != reflect.String {
return errors.New("cannot parse TagCompound as " + val.Type().String()) return errors.New("cannot parse TagCompound as " + val.Type().String())
} }
if val.IsNil() { if val.IsNil() {
val.Set(reflect.MakeMap(val.Type())) val.Set(reflect.MakeMap(vt))
} }
for { for {
tt, tn, err := d.readTag() tt, tn, err := d.readTag()
@ -414,6 +452,8 @@ func (d *Decoder) unmarshal(val reflect.Value, tagType byte) error {
buf[tn] = value buf[tn] = value
} }
val.Set(reflect.ValueOf(buf)) val.Set(reflect.ValueOf(buf))
default:
return errors.New("cannot parse TagCompound as " + vk.String())
} }
} }

View File

@ -466,7 +466,7 @@ func TestDecoder_Decode_ErrorUnknownField(t *testing.T) {
func TestDecoder_Decode_keysWithComma(t *testing.T) { func TestDecoder_Decode_keysWithComma(t *testing.T) {
data := []byte{ data := []byte{
TagCompound, 0, 1, 'S', TagCompound, 0, 1, 'S',
TagString, 0, 1, 'T', TagString, 0, 1, 't',
0, 4, 'T', 'n', 'z', 'e', 0, 4, 'T', 'n', 'z', 'e',
TagEnd, TagEnd,
} }

View File

@ -181,9 +181,22 @@ func (e *Encoder) writeValue(val reflect.Value, tagType byte) error {
switch val.Kind() { switch val.Kind() {
case reflect.Struct: case reflect.Struct:
fields := typeFields(val.Type()) fields := cachedTypeFields(val.Type())
for _, t := range fields.fields { FieldLoop:
v := val.Field(t.index) for i := range fields.list {
t := &fields.list[i]
v := val
for _, i := range t.index {
if v.Kind() == reflect.Pointer {
if v.IsNil() {
continue FieldLoop
}
v = v.Elem()
}
v = v.Field(i)
}
if t.omitEmpty && isEmptyValue(v) { if t.omitEmpty && isEmptyValue(v) {
continue continue
} }
@ -192,7 +205,7 @@ func (e *Encoder) writeValue(val reflect.Value, tagType byte) error {
return fmt.Errorf("encode %q error: unsupport type %v", t.name, v.Type()) return fmt.Errorf("encode %q error: unsupport type %v", t.name, v.Type())
} }
if t.list { if t.asList {
switch typ { switch typ {
case TagByteArray, TagIntArray, TagLongArray: case TagByteArray, TagIntArray, TagLongArray:
typ = TagList // override the parsed type typ = TagList // override the parsed type

View File

@ -10,3 +10,16 @@ type Marshaler interface {
TagType() byte TagType() byte
MarshalNBT(w io.Writer) error MarshalNBT(w io.Writer) error
} }
// FieldsUnmarshaler is a type can hold many Tags just like a TagCompound.
//
// If and only if a type which implements this interface is used as an anonymous field of a struct,
// and didn't set a struct tag, the content it holds will be considered as in the outer struct.
type FieldsUnmarshaler interface {
UnmarshalField(tagType byte, tagName string, r DecoderReader) (ok bool, err error)
}
// FieldsMarshaler is similar to FieldsUnmarshaler, but for marshaling.
type FieldsMarshaler interface {
MarshalFields(w io.Writer) (ok bool, err error)
}

View File

@ -20,7 +20,7 @@ const (
// These values are stored in the parseState stack. // These values are stored in the parseState stack.
// They give the current state of a composite value // They give the current state of a composite value
// being scanned. If the parser is inside a nested value // being scanned. If the parser is inside a nested value,
// the parseState describes the nested state, outermost at entry 0. // the parseState describes the nested state, outermost at entry 0.
const ( const (
parseCompoundName = iota // parsing tag name (before colon) parseCompoundName = iota // parsing tag name (before colon)

82
nbt/special_test.go Normal file
View File

@ -0,0 +1,82 @@
package nbt_test
import (
"fmt"
"testing"
"github.com/Tnze/go-mc/nbt"
)
func ExampleMarshal_anonymousStructField() {
type A struct{ F string }
type B struct{ E string }
type S struct {
A // anonymous fields are usually marshaled as if their inner exported fields were fields in the outer struct
B `nbt:"B"` // anonymous field, but with an explicit tag name specified
}
var val S
val.F = "Tnze"
val.E = "GoMC"
data, err := nbt.Marshal(val)
if err != nil {
panic(err)
}
var snbt nbt.StringifiedMessage
if err := nbt.Unmarshal(data, &snbt); err != nil {
panic(err)
}
fmt.Println(snbt)
// Output:
// {F:Tnze,B:{E:GoMC}}
}
func ExampleUnmarshal_anonymousStructField() {
type A struct{ F string }
type B struct{ E string }
type S struct {
A // anonymous fields are usually marshaled as if their inner exported fields were fields in the outer struct
B `nbt:"B"` // anonymous field, but with an explicit tag name specified
}
data, err := nbt.Marshal(nbt.StringifiedMessage(`{F:Tnze,B:{E:GoMC}}`))
if err != nil {
panic(err)
}
var val S
if err := nbt.Unmarshal(data, &val); err != nil {
panic(err)
}
fmt.Println(val.F)
fmt.Println(val.E)
// Output:
// Tnze
// GoMC
}
func TestMarshal_anonymousPointerNesting(t *testing.T) {
type A struct{ T string }
type B struct{ *A }
type C struct{ B }
val := C{B{&A{"Tnze"}}}
data, err := nbt.Marshal(val)
if err != nil {
panic(err)
}
var snbt nbt.StringifiedMessage
if err := nbt.Unmarshal(data, &snbt); err != nil {
panic(err)
}
want := `{T:Tnze}`
if string(snbt) != want {
t.Errorf("Marshal nesting anonymous struct error, got %q, want %q", snbt, want)
}
}

View File

@ -2,84 +2,241 @@ package nbt
import ( import (
"reflect" "reflect"
"sort"
"strings" "strings"
"sync" "sync"
) )
type typeInfo struct { type structFields struct {
fields []structField list []field
nameToIndex map[string]int // index of the field in struct, not previous slice nameIndex map[string]int // index of the previous slice.
} }
type structField struct { type field struct {
name string name string
index int
tag bool
index []int
typ reflect.Type
omitEmpty bool omitEmpty bool
list bool asList bool
} }
var tInfoMap sync.Map // byIndex sorts field by index sequence.
type byIndex []field
func typeFields(typ reflect.Type) *typeInfo { func (x byIndex) Len() int { return len(x) }
if ti, ok := tInfoMap.Load(typ); ok {
return ti.(*typeInfo) func (x byIndex) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
func (x byIndex) Less(i, j int) bool {
for k, xik := range x[i].index {
if k >= len(x[j].index) {
return false
}
if xik != x[j].index[k] {
return xik < x[j].index[k]
}
} }
return len(x[i].index) < len(x[j].index)
}
tInfo := new(typeInfo) func typeFields(t reflect.Type) (tInfo structFields) {
tInfo.nameToIndex = make(map[string]int) // Anonymous fields to explore at the current level and the next.
if typ.Kind() == reflect.Struct { current := []field{}
n := typ.NumField() next := []field{{typ: t}}
tInfo.fields = make([]structField, 0, n)
for i := 0; i < n; i++ { // Count of queued names for current level and the next.
f := typ.Field(i) var count, nextCount map[reflect.Type]int
tag := f.Tag.Get("nbt")
if (f.PkgPath != "" && !f.Anonymous) || tag == "-" { // Types already visited at an earlier level.
continue // Private field visited := make(map[reflect.Type]struct{})
// Fields found.
var fields []field
for len(next) > 0 {
current, next = next, current[:0]
count, nextCount = nextCount, make(map[reflect.Type]int)
for _, f := range current {
if _, ok := visited[f.typ]; ok {
continue
} }
visited[f.typ] = struct{}{}
// parse tags // Scan f.typ for fields to include.
var field structField for i := 0; i < f.typ.NumField(); i++ {
name, opts, _ := strings.Cut(tag, ",") sf := f.typ.Field(i)
if keytag := f.Tag.Get("nbtkey"); keytag != "" { if sf.Anonymous {
name = keytag t := sf.Type
} else if name == "" { if t.Kind() == reflect.Pointer {
name = f.Name t = t.Elem()
} }
field.name = name if !sf.IsExported() && t.Kind() != reflect.Struct {
field.index = i // Ignore embedded fields of unexported non-struct types.
continue
// parse options }
for opts != "" { // Do not ignore embedded fields of unexported struct types
var name string // since they may have exported fields.
name, opts, _ = strings.Cut(opts, ",") } else if !sf.IsExported() {
switch name { // Ignore unexported non-embedded fields.
case "omitempty": continue
field.omitEmpty = true
case "list":
field.list = true
} }
}
if f.Tag.Get("nbt_type") == "list" {
field.list = true
}
tInfo.fields = append(tInfo.fields, field)
tInfo.nameToIndex[field.name] = i tag := sf.Tag.Get("nbt")
if _, ok := tInfo.nameToIndex[f.Name]; !ok { if tag == "-" {
tInfo.nameToIndex[f.Name] = i continue
}
// parse tags
name, opts, _ := strings.Cut(tag, ",")
index := make([]int, len(f.index)+1)
copy(index, f.index)
index[len(f.index)] = i
if keytag := sf.Tag.Get("nbtkey"); keytag != "" {
name = keytag
}
ft := sf.Type
if ft.Name() == "" && ft.Kind() == reflect.Pointer {
// Follow pointer.
ft = ft.Elem()
}
// parse options
var omitEmpty, asList bool
for opts != "" {
var name string
name, opts, _ = strings.Cut(opts, ",")
switch name {
case "omitempty":
omitEmpty = true
case "list":
asList = true
}
}
// Deprecated: use `nbt:",list"` instead.
if sf.Tag.Get("nbt_type") == "list" {
asList = true
}
// Record found field and index sequence.
if name != "" || !sf.Anonymous || ft.Kind() != reflect.Struct {
tagged := name != ""
if name == "" {
name = sf.Name
}
field := field{
name: name,
tag: tagged,
index: index,
typ: ft,
omitEmpty: omitEmpty,
asList: asList,
}
fields = append(fields, field)
if count[f.typ] > 1 {
// If there were multiple instances, add a second,
// so that the annihilation code will see a duplicate.
// It only cares about the distinction between 1 or 2,
// so don't bother generating any more copies.
fields = append(fields, fields[len(fields)-1])
}
continue
}
// Record new anonymous struct to explore in next round.
nextCount[ft]++
if nextCount[ft] == 1 {
next = append(next, field{name: ft.Name(), index: index, typ: ft})
}
} }
} }
} }
ti, _ := tInfoMap.LoadOrStore(typ, tInfo) sort.Slice(fields, func(i, j int) bool {
return ti.(*typeInfo) x := fields
// sort field by name, breaking ties with depth, then
// breaking ties with "name came from json tag", then
// breaking ties with index sequence.
if x[i].name != x[j].name {
return x[i].name < x[j].name
}
if len(x[i].index) != len(x[j].index) {
return len(x[i].index) < len(x[j].index)
}
if x[i].tag != x[j].tag {
return x[i].tag
}
return byIndex(x).Less(i, j)
})
// Delete all fields that are hidden by the Go rules for embedded fields,
// except that fields with JSON tags are promoted.
// The fields are sorted in primary order of name, secondary order
// of field index length. Loop over names; for each name, delete
// hidden fields by choosing the one dominant field that survives.
out := fields[:0]
for advance, i := 0, 0; i < len(fields); i += advance {
// One iteration per name.
// Find the sequence of fields with the name of this first field.
fi := fields[i]
name := fi.name
for advance = 1; i+advance < len(fields); advance++ {
fj := fields[i+advance]
if fj.name != name {
break
}
}
if advance == 1 { // Only one field with this name
out = append(out, fi)
continue
}
dominant, ok := dominantField(fields[i : i+advance])
if ok {
out = append(out, dominant)
}
}
fields = out
sort.Sort(byIndex(fields))
nameIndex := make(map[string]int, len(fields))
for i, field := range fields {
nameIndex[field.name] = i
}
return structFields{
list: fields,
nameIndex: nameIndex,
}
} }
func (t *typeInfo) findIndexByName(name string) int { // dominantField looks through the fields, all of which are known to
i, ok := t.nameToIndex[name] // have the same name, to find the single field that dominates the
if !ok { // others using Go's embedding rules, modified by the presence of
return -1 // NBT tags. If there are multiple top-level fields, the boolean
// will be false: This condition is an error in Go and we skip all
// the fields.
func dominantField(fields []field) (field, bool) {
// The fields are sorted in increasing index-length order, then by presence of tag.
// That means that the first field is the dominant one. We need only check
// for error cases: two fields at top level, either both tagged or neither tagged.
if len(fields) > 1 && len(fields[0].index) == len(fields[1].index) && fields[0].tag == fields[1].tag {
return field{}, false
} }
return i return fields[0], true
}
var fieldCache sync.Map
func cachedTypeFields(t reflect.Type) structFields {
if ti, ok := fieldCache.Load(t); ok {
return ti.(structFields)
}
tInfo := typeFields(t)
ti, _ := fieldCache.LoadOrStore(t, tInfo)
return ti.(structFields)
} }