Fix nbt bugs that cannot handle tagNames contains commas
This commit is contained in:
@ -8,6 +8,7 @@ import (
|
|||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"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
|
// authDigest computes a special SHA-1 digest required for Minecraft web
|
||||||
// authentication on Premium servers (online-mode=true).
|
// authentication on Premium servers (online-mode=true).
|
||||||
// Source: http://wiki.vg/Protocol_Encryption#Server
|
// 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 {
|
func authDigest(serverID string, sharedSecret, publicKey []byte) string {
|
||||||
h := sha1.New()
|
h := sha1.New()
|
||||||
h.Write([]byte(serverID))
|
h.Write([]byte(serverID))
|
||||||
@ -93,7 +91,7 @@ func authDigest(serverID string, sharedSecret, publicKey []byte) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Trim away zeroes
|
// Trim away zeroes
|
||||||
res := strings.TrimLeft(fmt.Sprintf("%x", hash), "0")
|
res := strings.TrimLeft(hex.EncodeToString(hash), "0")
|
||||||
if negative {
|
if negative {
|
||||||
res = "-" + res
|
res = "-" + res
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,63 @@
|
|||||||
# NBT [](https://pkg.go.dev/github.com/Tnze/go-mc/nbt)
|
# 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
|
The API is very similar to the standard library `encoding/json`.
|
||||||
use this.
|
(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:
|
default:
|
||||||
return errors.New("cannot parse TagCompound as " + vk.String())
|
return errors.New("cannot parse TagCompound as " + vk.String())
|
||||||
case reflect.Struct:
|
case reflect.Struct:
|
||||||
tinfo := getTypeInfo(val.Type())
|
tinfo := typeFields(val.Type())
|
||||||
for {
|
for {
|
||||||
tt, tn, err := d.readTag()
|
tt, tn, err := d.readTag()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -149,13 +149,13 @@ type BigTestStruct struct {
|
|||||||
Value float32 `nbt:"value"`
|
Value float32 `nbt:"value"`
|
||||||
} `nbt:"egg"`
|
} `nbt:"egg"`
|
||||||
} `nbt:"nested compound test"`
|
} `nbt:"nested compound test"`
|
||||||
ListTest []int64 `nbt:"listTest (long)" nbt_type:"list"`
|
ListTest []int64 `nbt:"listTest (long),list"`
|
||||||
ListTest2 [2]struct {
|
ListTest2 [2]struct {
|
||||||
Name string `nbt:"name"`
|
Name string `nbt:"name"`
|
||||||
CreatedOn int64 `nbt:"created-on"`
|
CreatedOn int64 `nbt:"created-on"`
|
||||||
} `nbt:"listTest (compound)"`
|
} `nbt:"listTest (compound)"`
|
||||||
ByteTest byte `nbt:"byteTest"`
|
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"`
|
DoubleTest float64 `nbt:"doubleTest"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -446,3 +446,45 @@ func TestDecoder_Decode_ErrorUnknownField(t *testing.T) {
|
|||||||
t.Errorf("should return an error unmarshalling unknown field")
|
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"
|
"math"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"unsafe"
|
"unsafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -182,24 +181,26 @@ func (e *Encoder) writeValue(val reflect.Value, tagType byte) error {
|
|||||||
|
|
||||||
switch val.Kind() {
|
switch val.Kind() {
|
||||||
case reflect.Struct:
|
case reflect.Struct:
|
||||||
n := val.NumField()
|
fields := typeFields(val.Type())
|
||||||
for i := 0; i < n; i++ {
|
for _, t := range fields.fields {
|
||||||
f := val.Type().Field(i)
|
v := val.Field(t.index)
|
||||||
v := val.Field(i)
|
if t.omitEmpty && isEmptyValue(v) {
|
||||||
tag := f.Tag.Get("nbt")
|
|
||||||
if (f.PkgPath != "" && !f.Anonymous) || tag == "-" {
|
|
||||||
continue // Private field
|
|
||||||
}
|
|
||||||
|
|
||||||
tagProps := parseTag(f, v, tag)
|
|
||||||
if tagProps.OmitEmpty && isEmptyValue(v) {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if tagProps.Type == TagNone {
|
typ, v := getTagType(v)
|
||||||
return fmt.Errorf("encode %q error: unsupport type %v", tagProps.Name, v.Type())
|
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
|
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 {
|
func (e *Encoder) writeTag(tagType byte, tagName string) error {
|
||||||
if _, err := e.w.Write([]byte{tagType}); err != nil {
|
if _, err := e.w.Write([]byte{tagType}); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
// Package nbt implement the Named Binary Tag format of Minecraft.
|
// 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
|
package nbt
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -2,17 +2,26 @@ package nbt
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
type typeInfo struct {
|
type typeInfo struct {
|
||||||
tagName string
|
fields []structField
|
||||||
nameToIndex map[string]int
|
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
|
var tInfoMap sync.Map
|
||||||
|
|
||||||
func getTypeInfo(typ reflect.Type) *typeInfo {
|
func typeFields(typ reflect.Type) *typeInfo {
|
||||||
if ti, ok := tInfoMap.Load(typ); ok {
|
if ti, ok := tInfoMap.Load(typ); ok {
|
||||||
return ti.(*typeInfo)
|
return ti.(*typeInfo)
|
||||||
}
|
}
|
||||||
@ -21,6 +30,7 @@ func getTypeInfo(typ reflect.Type) *typeInfo {
|
|||||||
tInfo.nameToIndex = make(map[string]int)
|
tInfo.nameToIndex = make(map[string]int)
|
||||||
if typ.Kind() == reflect.Struct {
|
if typ.Kind() == reflect.Struct {
|
||||||
n := typ.NumField()
|
n := typ.NumField()
|
||||||
|
tInfo.fields = make([]structField, 0, n)
|
||||||
for i := 0; i < n; i++ {
|
for i := 0; i < n; i++ {
|
||||||
f := typ.Field(i)
|
f := typ.Field(i)
|
||||||
tag := f.Tag.Get("nbt")
|
tag := f.Tag.Get("nbt")
|
||||||
@ -28,7 +38,34 @@ func getTypeInfo(typ reflect.Type) *typeInfo {
|
|||||||
continue // Private field
|
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 {
|
if _, ok := tInfo.nameToIndex[f.Name]; !ok {
|
||||||
tInfo.nameToIndex[f.Name] = i
|
tInfo.nameToIndex[f.Name] = i
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user