Fix nbt bugs that cannot handle tagNames contains commas
This commit is contained in:
@ -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
|
||||
}
|
||||
|
@ -1,6 +1,63 @@
|
||||
# NBT [](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: `<nbt 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"`
|
||||
}
|
||||
```
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 (
|
||||
|
@ -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
|
||||
}
|
||||
|
Reference in New Issue
Block a user