Fix nbt bugs that cannot handle tagNames contains commas

This commit is contained in:
Tnze
2023-04-16 20:42:28 +08:00
parent fd4a4bdeb1
commit 7bef059b44
7 changed files with 165 additions and 61 deletions

View File

@ -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
}

View File

@ -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: `<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"`
}
```

View File

@ -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 {

View File

@ -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)
}
}

View File

@ -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

View File

@ -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 (

View File

@ -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
}