From 7bef059b444a9a6cca48678877080bf443275133 Mon Sep 17 00:00:00 2001 From: Tnze Date: Sun, 16 Apr 2023 20:42:28 +0800 Subject: [PATCH] Fix nbt bugs that cannot handle tagNames contains commas --- bot/login.go | 6 ++--- nbt/README.md | 63 +++++++++++++++++++++++++++++++++++++++++++--- nbt/decode.go | 2 +- nbt/decode_test.go | 46 +++++++++++++++++++++++++++++++-- nbt/encode.go | 62 ++++++++++++--------------------------------- nbt/nbt.go | 2 +- nbt/typeinfo.go | 45 ++++++++++++++++++++++++++++++--- 7 files changed, 165 insertions(+), 61 deletions(-) diff --git a/bot/login.go b/bot/login.go index be3642b..51f536e 100644 --- a/bot/login.go +++ b/bot/login.go @@ -8,6 +8,7 @@ import ( "crypto/rsa" "crypto/sha1" "crypto/x509" + "encoding/hex" "encoding/json" "fmt" "io" @@ -76,9 +77,6 @@ func (e *encryptionRequest) ReadFrom(r io.Reader) (int64, error) { // authDigest computes a special SHA-1 digest required for Minecraft web // authentication on Premium servers (online-mode=true). // Source: http://wiki.vg/Protocol_Encryption#Server -// -// Also many, many thanks to SirCmpwn and his wonderful gist (C#): -// https://gist.github.com/SirCmpwn/404223052379e82f91e6 func authDigest(serverID string, sharedSecret, publicKey []byte) string { h := sha1.New() h.Write([]byte(serverID)) @@ -93,7 +91,7 @@ func authDigest(serverID string, sharedSecret, publicKey []byte) string { } // Trim away zeroes - res := strings.TrimLeft(fmt.Sprintf("%x", hash), "0") + res := strings.TrimLeft(hex.EncodeToString(hash), "0") if negative { res = "-" + res } diff --git a/nbt/README.md b/nbt/README.md index cb5414c..692e253 100644 --- a/nbt/README.md +++ b/nbt/README.md @@ -1,6 +1,63 @@ # NBT [![Go Reference](https://pkg.go.dev/badge/github.com/Tnze/go-mc/nbt.svg)](https://pkg.go.dev/github.com/Tnze/go-mc/nbt) -This package implement the [Named Binary Tag](https://wiki.vg/NBT) format of Minecraft. +This package implements the [Named Binary Tag](https://wiki.vg/NBT) format of Minecraft. -The API is very similar to the standard library `encoding/json`. If you (high probability) have used that, it is easy to -use this. +The API is very similar to the standard library `encoding/json`. +(But fix some its problem) +If you (high probability) have used that, it is easy to use this. + +## Supported Struct Tags + +- `nbt` - The primary tag name. See below. +- `nbtkey` - The key name of the field (Used to support commas `,` in tag names) + +### The `nbt` tag + +In most cases, you only need this one to specify the name of the tag. + +The format of `nbt` struct tag: `[,opt]`. + +It's a comma-separated list of options. +The first item is the name of the tag, and the rest are options. + +Like this: +```go +type MyStruct struct { + Name string `nbt:"name"` +} +``` + +To tell the encoder not to encode a field, use `-`: +```go +type MyStruct struct { + Internal string `nbt:"-"` +} +``` + +To tell the encoder to skip the field if it is zero value, use `omitempty`: +```go +type MyStruct struct { + Name string `nbt:"name,omitempty"` +} +``` + +Fields typed `[]byte`, `[]int32` and `[]int64` will be encoded as `TagByteArray`, `TagIntArray` and `TagLongArray` respectively by default. +You can override this behavior by specifying encode them as`TagList` by using `list`: +```go +type MyStruct struct { + Data []byte `nbt:"data,list"` +} +``` + +### The `nbtkey` + +Common issue with JSON standard libraries: inability to specify keys containing commas for structures. +(e.g `{"a,b" : "c"}`) + +So this is a workaround for that: + +```go +type MyStruct struct { + AB string `nbt:",omitempty" nbtkey:"a,b"` +} +``` \ No newline at end of file diff --git a/nbt/decode.go b/nbt/decode.go index 48f59d1..f1bd92d 100644 --- a/nbt/decode.go +++ b/nbt/decode.go @@ -355,7 +355,7 @@ func (d *Decoder) unmarshal(val reflect.Value, tagType byte) error { default: return errors.New("cannot parse TagCompound as " + vk.String()) case reflect.Struct: - tinfo := getTypeInfo(val.Type()) + tinfo := typeFields(val.Type()) for { tt, tn, err := d.readTag() if err != nil { diff --git a/nbt/decode_test.go b/nbt/decode_test.go index 3d38de7..e6a6301 100644 --- a/nbt/decode_test.go +++ b/nbt/decode_test.go @@ -149,13 +149,13 @@ type BigTestStruct struct { Value float32 `nbt:"value"` } `nbt:"egg"` } `nbt:"nested compound test"` - ListTest []int64 `nbt:"listTest (long)" nbt_type:"list"` + ListTest []int64 `nbt:"listTest (long),list"` ListTest2 [2]struct { Name string `nbt:"name"` CreatedOn int64 `nbt:"created-on"` } `nbt:"listTest (compound)"` ByteTest byte `nbt:"byteTest"` - ByteArrayTest []byte `nbt:"byteArrayTest (the first 1000 values of (n*n*255+n*7)%100, starting with n=0 (0, 62, 34, 16, 8, ...))"` + ByteArrayTest []byte `nbtkey:"byteArrayTest (the first 1000 values of (n*n*255+n*7)%100, starting with n=0 (0, 62, 34, 16, 8, ...))"` DoubleTest float64 `nbt:"doubleTest"` } @@ -446,3 +446,45 @@ func TestDecoder_Decode_ErrorUnknownField(t *testing.T) { t.Errorf("should return an error unmarshalling unknown field") } } + +func TestDecoder_Decode_keysWithComma(t *testing.T) { + data := []byte{ + TagCompound, 0, 1, 'S', + TagString, 0, 1, 'T', + 0, 4, 'T', 'n', 'z', 'e', + TagEnd, + } + var s struct { + T string `nbt:"t,omitempty"` + } + + if _, err := NewDecoder(bytes.NewReader(data)).Decode(&s); err != nil { + t.Errorf("decode error: %v", err) + } + + want := "Tnze" + if s.T != want { + t.Errorf("unmarshal error: got %q, want %q", s.T, want) + } +} + +func TestDecoder_Decode_keysWithComma2(t *testing.T) { + data := []byte{ + TagCompound, 0, 1, 'S', + TagString, 0, 11, 't', ',', 'o', 'm', 'i', 't', 'e', 'm', 'p', 't', 'y', + 0, 4, 'T', 'n', 'z', 'e', + TagEnd, + } + var s struct { + T string `nbtkey:"t,omitempty"` + } + + if _, err := NewDecoder(bytes.NewReader(data)).Decode(&s); err != nil { + t.Errorf("decode error: %v", err) + } + + want := "Tnze" + if s.T != want { + t.Errorf("unmarshal error: got %q, want %q", s.T, want) + } +} diff --git a/nbt/encode.go b/nbt/encode.go index e6b02cf..1e876b4 100644 --- a/nbt/encode.go +++ b/nbt/encode.go @@ -9,7 +9,6 @@ import ( "math" "reflect" "strconv" - "strings" "unsafe" ) @@ -182,24 +181,26 @@ func (e *Encoder) writeValue(val reflect.Value, tagType byte) error { switch val.Kind() { case reflect.Struct: - n := val.NumField() - for i := 0; i < n; i++ { - f := val.Type().Field(i) - v := val.Field(i) - tag := f.Tag.Get("nbt") - if (f.PkgPath != "" && !f.Anonymous) || tag == "-" { - continue // Private field - } - - tagProps := parseTag(f, v, tag) - if tagProps.OmitEmpty && isEmptyValue(v) { + fields := typeFields(val.Type()) + for _, t := range fields.fields { + v := val.Field(t.index) + if t.omitEmpty && isEmptyValue(v) { continue } - if tagProps.Type == TagNone { - return fmt.Errorf("encode %q error: unsupport type %v", tagProps.Name, v.Type()) + typ, v := getTagType(v) + if typ == TagNone { + return fmt.Errorf("encode %q error: unsupport type %v", t.name, v.Type()) } - if err := e.marshal(val.Field(i), tagProps.Type, tagProps.Name); err != nil { + if t.list { + if IsArrayTag(typ) { + typ = TagList // override the parsed type + } else { + return fmt.Errorf("invalid use of ,list struct tag, trying to encode %v as TagList", v.Type()) + } + } + + if err := e.marshal(v, typ, t.name); err != nil { return err } } @@ -319,37 +320,6 @@ func getTagTypeByType(vk reflect.Type) byte { } } -type tagProps struct { - Name string - Type byte - OmitEmpty bool -} - -func parseTag(f reflect.StructField, v reflect.Value, tagName string) (result tagProps) { - if strings.HasSuffix(tagName, ",omitempty") { - result.OmitEmpty = true - tagName = tagName[:len(tagName)-10] - } - - if tagName != "" { - result.Name = tagName - } else { - result.Name = f.Name - } - - nbtType := f.Tag.Get("nbt_type") - result.Type, _ = getTagType(v) - if strings.Contains(nbtType, "list") { - if IsArrayTag(result.Type) { - result.Type = TagList // for expanding the array to a standard list - } else { - panic("list is only supported for array types ([]byte, []int, []long)") - } - } - - return -} - func (e *Encoder) writeTag(tagType byte, tagName string) error { if _, err := e.w.Write([]byte{tagType}); err != nil { return err diff --git a/nbt/nbt.go b/nbt/nbt.go index 7fc6786..9ef0dd2 100644 --- a/nbt/nbt.go +++ b/nbt/nbt.go @@ -1,5 +1,5 @@ // Package nbt implement the Named Binary Tag format of Minecraft. -// It provides api like encoding/xml package. +// It provides api like encoding/json package. package nbt import ( diff --git a/nbt/typeinfo.go b/nbt/typeinfo.go index 9903804..bec5533 100644 --- a/nbt/typeinfo.go +++ b/nbt/typeinfo.go @@ -2,17 +2,26 @@ package nbt import ( "reflect" + "strings" "sync" ) type typeInfo struct { - tagName string - nameToIndex map[string]int + fields []structField + nameToIndex map[string]int // index of the field in struct, not previous slice +} + +type structField struct { + name string + index int + + omitEmpty bool + list bool } var tInfoMap sync.Map -func getTypeInfo(typ reflect.Type) *typeInfo { +func typeFields(typ reflect.Type) *typeInfo { if ti, ok := tInfoMap.Load(typ); ok { return ti.(*typeInfo) } @@ -21,6 +30,7 @@ func getTypeInfo(typ reflect.Type) *typeInfo { tInfo.nameToIndex = make(map[string]int) if typ.Kind() == reflect.Struct { n := typ.NumField() + tInfo.fields = make([]structField, 0, n) for i := 0; i < n; i++ { f := typ.Field(i) tag := f.Tag.Get("nbt") @@ -28,7 +38,34 @@ func getTypeInfo(typ reflect.Type) *typeInfo { continue // Private field } - tInfo.nameToIndex[tag] = i + // parse tags + var field structField + name, opts, _ := strings.Cut(tag, ",") + if keytag := f.Tag.Get("nbtkey"); keytag != "" { + name = keytag + } else if name == "" { + name = f.Name + } + field.name = name + field.index = i + + // parse options + for opts != "" { + var name string + name, opts, _ = strings.Cut(opts, ",") + switch name { + case "omitempty": + 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 if _, ok := tInfo.nameToIndex[f.Name]; !ok { tInfo.nameToIndex[f.Name] = i }