Merge branch 'snbt_convertor' into master
# Conflicts: # README.md
This commit is contained in:
31
README.md
31
README.md
@ -1,4 +1,5 @@
|
|||||||
# Go-MC
|
# Go-MC
|
||||||
|
|
||||||

|

|
||||||
[](https://pkg.go.dev/github.com/Tnze/go-mc)
|
[](https://pkg.go.dev/github.com/Tnze/go-mc)
|
||||||
[](https://goreportcard.com/report/github.com/Tnze/go-mc)
|
[](https://goreportcard.com/report/github.com/Tnze/go-mc)
|
||||||
@ -11,18 +12,20 @@ There's some library in Go support you to create your Minecraft client or server
|
|||||||
|
|
||||||
- [x] Chat Message (Support Json or old `§`)
|
- [x] Chat Message (Support Json or old `§`)
|
||||||
- [x] NBT (Based on reflection)
|
- [x] NBT (Based on reflection)
|
||||||
|
- [ ] SNBT -> NBT
|
||||||
- [x] Yggdrasil
|
- [x] Yggdrasil
|
||||||
- [x] Realms Server
|
- [x] Realms Server
|
||||||
- [x] RCON protocol (Server & Client)
|
- [x] RCON protocol (Server & Client)
|
||||||
- [x] Saves decoding & encoding
|
- [x] Saves decoding & encoding
|
||||||
- [x] Minecraft network protocol
|
- [x] Minecraft network protocol
|
||||||
- [x] Robot player framework
|
- [x] Robot framework
|
||||||
|
|
||||||
> 由于仍在开发中,部分API在未来版本中可能会变动
|
> 由于仍在开发中,部分API在未来版本中可能会变动
|
||||||
|
|
||||||
> `1.13.2` version is at [gomcbot](https://github.com/Tnze/gomcbot).
|
> `1.13.2` version is at [gomcbot](https://github.com/Tnze/gomcbot).
|
||||||
|
|
||||||
## Getting start
|
## Getting start
|
||||||
|
|
||||||
After you install golang:
|
After you install golang:
|
||||||
To get the latest version: `go get github.com/Tnze/go-mc@master`
|
To get the latest version: `go get github.com/Tnze/go-mc@master`
|
||||||
To get old versions (e.g. 1.14.3): `go get github.com/Tnze/go-mc@v1.14.3`
|
To get old versions (e.g. 1.14.3): `go get github.com/Tnze/go-mc@v1.14.3`
|
||||||
@ -36,7 +39,9 @@ First, you might have a try of the simple examples. It's a good start.
|
|||||||
|
|
||||||
### Basic Usage
|
### Basic Usage
|
||||||
|
|
||||||
One of the most useful functions of this lib is that it implements the network communication protocol of minecraft. It allows you to construct, send, receive, and parse network packets. All of them are encapsulated in `go-mc/net` and `go-mc/net/packet`.
|
One of the most useful functions of this lib is that it implements the network communication protocol of minecraft. It
|
||||||
|
allows you to construct, send, receive, and parse network packets. All of them are encapsulated in `go-mc/net`
|
||||||
|
and `go-mc/net/packet`.
|
||||||
|
|
||||||
这个库最核心的便是实现了Minecraft底层的网络通信协议,可以用与构造、发送、接收和解读MC数据包。这是靠 `go-mc/net` 和 `go-mc/net/packet`这两个包实现的。
|
这个库最核心的便是实现了Minecraft底层的网络通信协议,可以用与构造、发送、接收和解读MC数据包。这是靠 `go-mc/net` 和 `go-mc/net/packet`这两个包实现的。
|
||||||
|
|
||||||
@ -45,7 +50,8 @@ import "github.com/Tnze/go-mc/net"
|
|||||||
import pk "github.com/Tnze/go-mc/net/packet"
|
import pk "github.com/Tnze/go-mc/net/packet"
|
||||||
```
|
```
|
||||||
|
|
||||||
It's very easy to create a packet. For example, after any client connected the server, it sends a [Handshake Packet](https://wiki.vg/Protocol#Handshake). You can create this package with the following code:
|
It's very easy to create a packet. For example, after any client connected the server, it sends
|
||||||
|
a [Handshake Packet](https://wiki.vg/Protocol#Handshake). You can create this package with the following code:
|
||||||
|
|
||||||
构造一个数据包很简单,例如客户端连接时会发送一个[握手包](https://wiki.vg/Protocol#Handshake),你就可以用下面这段代码来生成这个包:
|
构造一个数据包很简单,例如客户端连接时会发送一个[握手包](https://wiki.vg/Protocol#Handshake),你就可以用下面这段代码来生成这个包:
|
||||||
|
|
||||||
@ -59,7 +65,8 @@ p := pk.Marshal(
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
Then you can send it to server using `conn.WritePacket(p)`. The `conn` is a `net.Conn` which is returned by `net.Dial()`. And don't forget to handle the error.^_^
|
Then you can send it to server using `conn.WritePacket(p)`. The `conn` is a `net.Conn` which is returned by `net.Dial()`
|
||||||
|
. And don't forget to handle the error.^_^
|
||||||
|
|
||||||
然后就可以调用`conn.WritePacket(p)`来发送这个p了,其中`conn`是连接对象。发数据包的时候记得不要忘记处理错误噢!
|
然后就可以调用`conn.WritePacket(p)`来发送这个p了,其中`conn`是连接对象。发数据包的时候记得不要忘记处理错误噢!
|
||||||
|
|
||||||
@ -90,7 +97,8 @@ Sometimes you are handling packet like this:
|
|||||||
| World Count | VarInt | Size of the following array. |
|
| World Count | VarInt | Size of the following array. |
|
||||||
| World Names | Array of Identifier | Identifiers for all worlds on the server. |
|
| World Names | Array of Identifier | Identifiers for all worlds on the server. |
|
||||||
|
|
||||||
That is, the first field is an integer type and the second field is an array (a `[]string` in this case). The integer represents the length of array.
|
That is, the first field is an integer type and the second field is an array (a `[]string` in this case). The integer
|
||||||
|
represents the length of array.
|
||||||
|
|
||||||
Traditionally, you can use the following method to read such a field:
|
Traditionally, you can use the following method to read such a field:
|
||||||
|
|
||||||
@ -112,7 +120,8 @@ for i := 0; i < int(WorldCount); i++ {
|
|||||||
|
|
||||||
But this is tediously long an not compatible with `p.Scan()` method.
|
But this is tediously long an not compatible with `p.Scan()` method.
|
||||||
|
|
||||||
In the latest version, two new types is added: `pk.Ary` and `pk.Opt`. Dedicated to handling "Array of ...." and "Optional ...." fields.
|
In the latest version, two new types is added: `pk.Ary` and `pk.Opt`. Dedicated to handling "Array of ...." and "
|
||||||
|
Optional ...." fields.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
var WorldCount pk.VarInt
|
var WorldCount pk.VarInt
|
||||||
@ -122,15 +131,17 @@ if err := p.Scan(&WorldCount, pk.Ary{&WorldCount, &WorldNames}); err != nil {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
As the `go-mc/net` package implements the minecraft network protocol, there is no update between the versions at this level. So net package actually supports any version. It's just that the ID and content of the package are different between different versions.
|
As the `go-mc/net` package implements the minecraft network protocol, there is no update between the versions at this
|
||||||
|
level. So net package actually supports any version. It's just that the ID and content of the package are different
|
||||||
|
between different versions.
|
||||||
|
|
||||||
由于`go-mc/net`实现的是MC底层的网络协议,而这个协议在MC更新时其实并不会有改动,MC更新时其实只是包的ID和内容的定义发生了变化,所以net包本身是跨版本的。
|
由于`go-mc/net`实现的是MC底层的网络协议,而这个协议在MC更新时其实并不会有改动,MC更新时其实只是包的ID和内容的定义发生了变化,所以net包本身是跨版本的。
|
||||||
|
|
||||||
Originally it's all right to write a bot with only `go-mc/net` package, but considering that the process of handshake, login and encryption is not difficult but complicated, I have implemented it in `go-mc/bot` package, which is **not cross-versions**. You may use it directly or as a reference for your own implementation.
|
Originally it's all right to write a bot with only `go-mc/net` package, but considering that the process of handshake,
|
||||||
|
login and encryption is not difficult but complicated, I have implemented it in `go-mc/bot` package, which is **not
|
||||||
|
cross-versions**. You may use it directly or as a reference for your own implementation.
|
||||||
|
|
||||||
理论上讲,只用`go-mc/net`包实现一个bot是完全可行的,但是为了节省大家从头去理解MC握手、登录、加密等协议的过程,在`go-mc/bot`中我已经把这些都实现了,只不过它不是跨版本的。你可以直接使用,或者作为自己实现的参考。
|
理论上讲,只用`go-mc/net`包实现一个bot是完全可行的,但是为了节省大家从头去理解MC握手、登录、加密等协议的过程,在`go-mc/bot`中我已经把这些都实现了,只不过它不是跨版本的。你可以直接使用,或者作为自己实现的参考。
|
||||||
|
|
||||||
|
31
nbt/bigTest_test.snbt
Normal file
31
nbt/bigTest_test.snbt
Normal file
File diff suppressed because one or more lines are too long
@ -41,7 +41,7 @@ func (e *Encoder) marshal(val reflect.Value, tagType byte, tagName string) error
|
|||||||
func (e *Encoder) writeHeader(val reflect.Value, tagType byte, tagName string) (err error) {
|
func (e *Encoder) writeHeader(val reflect.Value, tagType byte, tagName string) (err error) {
|
||||||
if tagType == TagList {
|
if tagType == TagList {
|
||||||
eleType := getTagType(val.Type().Elem())
|
eleType := getTagType(val.Type().Elem())
|
||||||
err = e.writeListHeader(eleType, tagName, val.Len())
|
err = e.writeListHeader(eleType, tagName, val.Len(), true)
|
||||||
} else {
|
} else {
|
||||||
err = e.writeTag(tagType, tagName)
|
err = e.writeTag(tagType, tagName)
|
||||||
}
|
}
|
||||||
@ -224,10 +224,12 @@ func (e *Encoder) writeTag(tagType byte, tagName string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Encoder) writeListHeader(elementType byte, tagName string, n int) (err error) {
|
func (e *Encoder) writeListHeader(elementType byte, tagName string, n int, writeTag bool) (err error) {
|
||||||
|
if writeTag {
|
||||||
if err = e.writeTag(TagList, tagName); err != nil {
|
if err = e.writeTag(TagList, tagName); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if _, err = e.w.Write([]byte{elementType}); err != nil {
|
if _, err = e.w.Write([]byte{elementType}); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
521
nbt/snbt_decode.go
Normal file
521
nbt/snbt_decode.go
Normal file
@ -0,0 +1,521 @@
|
|||||||
|
package nbt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type decodeState struct {
|
||||||
|
data []byte
|
||||||
|
off int // next read Offset in data
|
||||||
|
opcode int // last read result
|
||||||
|
scan scanner
|
||||||
|
}
|
||||||
|
|
||||||
|
const phasePanicMsg = "SNBT decoder out of sync - data changing underfoot?"
|
||||||
|
|
||||||
|
func (e *Encoder) WriteSNBT(snbt string) error {
|
||||||
|
d := decodeState{data: []byte(snbt)}
|
||||||
|
d.scan.reset()
|
||||||
|
return writeValue(e, &d, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeValue(e *Encoder, d *decodeState, tagName string) error {
|
||||||
|
d.scanWhile(scanSkipSpace)
|
||||||
|
switch d.opcode {
|
||||||
|
case scanError:
|
||||||
|
return d.error(d.scan.errContext)
|
||||||
|
default:
|
||||||
|
panic(phasePanicMsg)
|
||||||
|
|
||||||
|
case scanBeginLiteral:
|
||||||
|
start := d.readIndex()
|
||||||
|
if d.scanWhile(scanContinue); d.opcode == scanError {
|
||||||
|
return d.error(d.scan.errContext)
|
||||||
|
}
|
||||||
|
literal := d.data[start:d.readIndex()]
|
||||||
|
tagType, litVal := parseLiteral(literal)
|
||||||
|
if err := e.writeTag(tagType, tagName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return writeLiteralPayload(e, litVal)
|
||||||
|
|
||||||
|
case scanBeginCompound:
|
||||||
|
if err := e.writeTag(TagCompound, tagName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return writeCompoundPayload(e, d)
|
||||||
|
|
||||||
|
case scanBeginList:
|
||||||
|
_, err := writeListOrArray(e, d, true, tagName)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeLiteralPayload(e *Encoder, v interface{}) (err error) {
|
||||||
|
switch v.(type) {
|
||||||
|
case string:
|
||||||
|
str := v.(string)
|
||||||
|
err = e.writeInt16(int16(len(str)))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, err = e.w.Write([]byte(str))
|
||||||
|
case int8:
|
||||||
|
_, err = e.w.Write([]byte{byte(v.(int8))})
|
||||||
|
case int16:
|
||||||
|
err = e.writeInt16(v.(int16))
|
||||||
|
case int32:
|
||||||
|
err = e.writeInt32(v.(int32))
|
||||||
|
case int64:
|
||||||
|
err = e.writeInt64(v.(int64))
|
||||||
|
case float32:
|
||||||
|
err = e.writeInt32(int32(math.Float32bits(v.(float32))))
|
||||||
|
case float64:
|
||||||
|
err = e.writeInt64(int64(math.Float64bits(v.(float64))))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeCompoundPayload(e *Encoder, d *decodeState) error {
|
||||||
|
defer d.scanNext()
|
||||||
|
for {
|
||||||
|
d.scanWhile(scanSkipSpace)
|
||||||
|
if d.opcode == scanEndValue {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if d.opcode == scanError {
|
||||||
|
return d.error(d.scan.errContext)
|
||||||
|
}
|
||||||
|
if d.opcode != scanBeginLiteral {
|
||||||
|
panic(phasePanicMsg)
|
||||||
|
}
|
||||||
|
// read tag name
|
||||||
|
start := d.readIndex()
|
||||||
|
if d.scanWhile(scanContinue); d.opcode == scanError {
|
||||||
|
return d.error(d.scan.errContext)
|
||||||
|
}
|
||||||
|
var tagName string
|
||||||
|
if tt, v := parseLiteral(d.data[start:d.readIndex()]); tt == TagString {
|
||||||
|
tagName = v.(string)
|
||||||
|
} else {
|
||||||
|
tagName = string(d.data[start:d.readIndex()])
|
||||||
|
}
|
||||||
|
// read value
|
||||||
|
if d.opcode == scanSkipSpace {
|
||||||
|
d.scanWhile(scanSkipSpace)
|
||||||
|
}
|
||||||
|
if d.opcode == scanError {
|
||||||
|
return d.error(d.scan.errContext)
|
||||||
|
}
|
||||||
|
if d.opcode != scanCompoundTagName {
|
||||||
|
panic(phasePanicMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writeValue(e, d, tagName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next token must be , or }.
|
||||||
|
if d.opcode == scanSkipSpace {
|
||||||
|
d.scanWhile(scanSkipSpace)
|
||||||
|
}
|
||||||
|
if d.opcode == scanError {
|
||||||
|
return d.error(d.scan.errContext)
|
||||||
|
}
|
||||||
|
if d.opcode == scanEndValue {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if d.opcode != scanCompoundValue {
|
||||||
|
panic(phasePanicMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, err := e.w.Write([]byte{TagEnd})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeListOrArray(e *Encoder, d *decodeState, writeTag bool, tagName string) (tagType byte, err error) {
|
||||||
|
d.scanWhile(scanSkipSpace)
|
||||||
|
if d.opcode == scanEndValue { // ']', empty TAG_List
|
||||||
|
err = e.writeListHeader(TagEnd, tagName, 0, writeTag)
|
||||||
|
d.scanNext()
|
||||||
|
return TagList, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't know the length of the List,
|
||||||
|
// so we read them into a buffer and count.
|
||||||
|
var buf bytes.Buffer
|
||||||
|
var count int
|
||||||
|
e2 := NewEncoder(&buf)
|
||||||
|
start := d.readIndex()
|
||||||
|
|
||||||
|
switch d.opcode {
|
||||||
|
case scanBeginLiteral:
|
||||||
|
if d.scanWhile(scanContinue); d.opcode == scanError {
|
||||||
|
return TagList, d.error(d.scan.errContext)
|
||||||
|
}
|
||||||
|
literal := d.data[start:d.readIndex()]
|
||||||
|
if d.opcode == scanSkipSpace {
|
||||||
|
d.scanWhile(scanSkipSpace)
|
||||||
|
}
|
||||||
|
if d.opcode == scanError {
|
||||||
|
return tagType, d.error(d.scan.errContext)
|
||||||
|
}
|
||||||
|
if d.opcode == scanListType { // TAG_X_Array
|
||||||
|
var elemType byte
|
||||||
|
switch literal[0] {
|
||||||
|
case 'B':
|
||||||
|
tagType = TagByteArray
|
||||||
|
elemType = TagByte
|
||||||
|
case 'I':
|
||||||
|
tagType = TagIntArray
|
||||||
|
elemType = TagInt
|
||||||
|
case 'L':
|
||||||
|
tagType = TagLongArray
|
||||||
|
elemType = TagLong
|
||||||
|
default:
|
||||||
|
return TagList, d.error("unknown Array type")
|
||||||
|
}
|
||||||
|
if writeTag {
|
||||||
|
if err = e.writeTag(tagType, tagName); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if d.opcode == scanSkipSpace {
|
||||||
|
d.scanWhile(scanSkipSpace)
|
||||||
|
}
|
||||||
|
d.scanWhile(scanSkipSpace) // ;
|
||||||
|
if d.opcode == scanEndValue { // ]
|
||||||
|
// empty array
|
||||||
|
if err = e.writeInt32(0); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
if d.opcode == scanSkipSpace {
|
||||||
|
d.scanWhile(scanSkipSpace)
|
||||||
|
}
|
||||||
|
if d.opcode != scanBeginLiteral {
|
||||||
|
return tagType, d.error("not literal in Array")
|
||||||
|
}
|
||||||
|
start := d.readIndex()
|
||||||
|
|
||||||
|
if d.scanWhile(scanContinue); d.opcode == scanError {
|
||||||
|
return tagType, d.error(d.scan.errContext)
|
||||||
|
}
|
||||||
|
literal := d.data[start:d.readIndex()]
|
||||||
|
subType, litVal := parseLiteral(literal)
|
||||||
|
if subType != elemType {
|
||||||
|
err = d.error("unexpected element type in TAG_Array")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch elemType {
|
||||||
|
case TagByte:
|
||||||
|
_, err = e2.w.Write([]byte{byte(litVal.(int8))})
|
||||||
|
case TagInt:
|
||||||
|
err = e2.writeInt32(litVal.(int32))
|
||||||
|
case TagLong:
|
||||||
|
err = e2.writeInt64(litVal.(int64))
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
count++
|
||||||
|
|
||||||
|
if d.opcode == scanSkipSpace {
|
||||||
|
d.scanWhile(scanSkipSpace)
|
||||||
|
}
|
||||||
|
if d.opcode == scanError {
|
||||||
|
return tagType, d.error(d.scan.errContext)
|
||||||
|
}
|
||||||
|
if d.opcode == scanEndValue { // ]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if d.opcode != scanListValue {
|
||||||
|
panic(phasePanicMsg)
|
||||||
|
}
|
||||||
|
d.scanWhile(scanSkipSpace) // ,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = e.writeInt32(int32(count)); err != nil {
|
||||||
|
return tagType, err
|
||||||
|
}
|
||||||
|
_, err = e.w.Write(buf.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
return tagType, err
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if d.opcode != scanListValue { // TAG_List<TAG_String>
|
||||||
|
panic(phasePanicMsg)
|
||||||
|
}
|
||||||
|
var tagType byte
|
||||||
|
for {
|
||||||
|
t, v := parseLiteral(literal)
|
||||||
|
if tagType == 0 {
|
||||||
|
tagType = t
|
||||||
|
}
|
||||||
|
if t != tagType {
|
||||||
|
return TagList, d.error("different TagType in List")
|
||||||
|
}
|
||||||
|
err = writeLiteralPayload(e2, v)
|
||||||
|
if err != nil {
|
||||||
|
return tagType, err
|
||||||
|
}
|
||||||
|
count++
|
||||||
|
|
||||||
|
// read ',' or ']'
|
||||||
|
if d.opcode == scanSkipSpace {
|
||||||
|
d.scanWhile(scanSkipSpace)
|
||||||
|
}
|
||||||
|
if d.opcode == scanError {
|
||||||
|
return tagType, d.error(d.scan.errContext)
|
||||||
|
}
|
||||||
|
if d.opcode == scanEndValue {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if d.opcode != scanListValue {
|
||||||
|
panic(phasePanicMsg)
|
||||||
|
}
|
||||||
|
d.scanWhile(scanSkipSpace)
|
||||||
|
start = d.readIndex()
|
||||||
|
if d.scanWhile(scanContinue); d.opcode == scanError {
|
||||||
|
return tagType, d.error(d.scan.errContext)
|
||||||
|
}
|
||||||
|
literal = d.data[start:d.readIndex()]
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := e.writeListHeader(tagType, tagName, count, writeTag); err != nil {
|
||||||
|
return tagType, err
|
||||||
|
}
|
||||||
|
if _, err := e.w.Write(buf.Bytes()); err != nil {
|
||||||
|
return tagType, err
|
||||||
|
}
|
||||||
|
case scanBeginList: // TAG_List<TAG_List>
|
||||||
|
var elemType byte
|
||||||
|
for {
|
||||||
|
if d.opcode == scanSkipSpace {
|
||||||
|
d.scanWhile(scanSkipSpace)
|
||||||
|
}
|
||||||
|
if d.opcode != scanBeginList {
|
||||||
|
return TagList, d.error("different TagType in List")
|
||||||
|
}
|
||||||
|
elemType, err = writeListOrArray(e2, d, false, "")
|
||||||
|
if err != nil {
|
||||||
|
return tagType, err
|
||||||
|
}
|
||||||
|
count++
|
||||||
|
if d.opcode == scanSkipSpace {
|
||||||
|
d.scanWhile(scanSkipSpace)
|
||||||
|
}
|
||||||
|
if d.opcode == scanError {
|
||||||
|
return tagType, d.error(d.scan.errContext)
|
||||||
|
}
|
||||||
|
// ',' or ']'
|
||||||
|
if d.opcode == scanEndValue {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if d.opcode != scanListValue {
|
||||||
|
panic(phasePanicMsg)
|
||||||
|
}
|
||||||
|
// read '['
|
||||||
|
d.scanNext()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = e.writeListHeader(elemType, tagName, count, writeTag); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err = e.w.Write(buf.Bytes()); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case scanBeginCompound: // TAG_List<TAG_Compound>
|
||||||
|
for {
|
||||||
|
if d.opcode == scanSkipSpace {
|
||||||
|
d.scanWhile(scanSkipSpace)
|
||||||
|
}
|
||||||
|
if d.opcode != scanBeginCompound {
|
||||||
|
return TagList, d.error("different TagType in List")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = writeCompoundPayload(e2, d); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
count++
|
||||||
|
if d.opcode == scanSkipSpace {
|
||||||
|
d.scanWhile(scanSkipSpace)
|
||||||
|
}
|
||||||
|
// read ',' or ']'
|
||||||
|
if d.opcode == scanSkipSpace {
|
||||||
|
d.scanWhile(scanSkipSpace)
|
||||||
|
}
|
||||||
|
if d.opcode == scanError {
|
||||||
|
return tagType, d.error(d.scan.errContext)
|
||||||
|
}
|
||||||
|
if d.opcode == scanEndValue {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if d.opcode != scanListValue {
|
||||||
|
panic(phasePanicMsg)
|
||||||
|
}
|
||||||
|
// read '{'
|
||||||
|
d.scanNext()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = e.writeListHeader(TagCompound, tagName, count, writeTag); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = e.w.Write(buf.Bytes()); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d.scanNext()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// readIndex returns the position of the last byte read.
|
||||||
|
func (d *decodeState) readIndex() int {
|
||||||
|
return d.off - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// scanNext processes the byte at d.data[d.off].
|
||||||
|
func (d *decodeState) scanNext() {
|
||||||
|
if d.off < len(d.data) {
|
||||||
|
d.opcode = d.scan.step(&d.scan, d.data[d.off])
|
||||||
|
d.off++
|
||||||
|
} else {
|
||||||
|
//d.opcode = d.scan.eof()
|
||||||
|
d.off = len(d.data) + 1 // mark processed EOF with len+1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// scanWhile processes bytes in d.data[d.off:] until it
|
||||||
|
// receives a scan code not equal to op.
|
||||||
|
func (d *decodeState) scanWhile(op int) {
|
||||||
|
s, data, i := &d.scan, d.data, d.off
|
||||||
|
for i < len(data) {
|
||||||
|
newOp := s.step(s, data[i])
|
||||||
|
i++
|
||||||
|
if newOp != op {
|
||||||
|
d.opcode = newOp
|
||||||
|
d.off = i
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
d.off = len(data) + 1 // mark processed EOF with len+1
|
||||||
|
d.opcode = d.scan.eof()
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseLiteral parse an SNBT literal, might be
|
||||||
|
// TAG_String, TAG_Int, TAG_Float, ... etc.
|
||||||
|
// so returned value is one of string, int32, float32 ...
|
||||||
|
func parseLiteral(literal []byte) (byte, interface{}) {
|
||||||
|
switch literal[0] {
|
||||||
|
case '"', '\'': // Quoted String
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.Grow(len(literal) - 2)
|
||||||
|
for i := 1; ; i++ {
|
||||||
|
c := literal[i]
|
||||||
|
switch c {
|
||||||
|
case literal[0]:
|
||||||
|
return TagString, sb.String()
|
||||||
|
case '\\':
|
||||||
|
i++
|
||||||
|
c = literal[i]
|
||||||
|
}
|
||||||
|
sb.WriteByte(c)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
strlen := len(literal)
|
||||||
|
integer := true
|
||||||
|
number := true
|
||||||
|
unqstr := true
|
||||||
|
var numberType byte
|
||||||
|
|
||||||
|
for i, c := range literal {
|
||||||
|
if isNumber(c) {
|
||||||
|
continue
|
||||||
|
} else if integer {
|
||||||
|
if i == strlen-1 && i != 0 && isIntegerType(c) {
|
||||||
|
numberType = c
|
||||||
|
strlen--
|
||||||
|
} else if i > 0 || i == 0 && c != '-' {
|
||||||
|
integer = false
|
||||||
|
if i == 0 || c != '.' {
|
||||||
|
number = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if number {
|
||||||
|
if i == strlen-1 && isFloatType(c) {
|
||||||
|
numberType = c
|
||||||
|
} else {
|
||||||
|
number = false
|
||||||
|
}
|
||||||
|
} else if !isAllowedInUnquotedString(c) {
|
||||||
|
unqstr = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if integer {
|
||||||
|
num, err := strconv.ParseInt(string(literal[:strlen]), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
switch numberType {
|
||||||
|
case 'B', 'b':
|
||||||
|
return TagByte, int8(num)
|
||||||
|
case 'S', 's':
|
||||||
|
return TagShort, int16(num)
|
||||||
|
default:
|
||||||
|
return TagInt, int32(num)
|
||||||
|
case 'L', 'l':
|
||||||
|
return TagLong, num
|
||||||
|
case 'F', 'f':
|
||||||
|
return TagFloat, float32(num)
|
||||||
|
case 'D', 'd':
|
||||||
|
return TagDouble, float64(num)
|
||||||
|
}
|
||||||
|
} else if number {
|
||||||
|
num, err := strconv.ParseFloat(string(literal[:strlen-1]), 64)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
switch numberType {
|
||||||
|
case 'F', 'f':
|
||||||
|
return TagFloat, float32(num)
|
||||||
|
case 'D', 'd':
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
|
return TagDouble, num
|
||||||
|
}
|
||||||
|
} else if unqstr {
|
||||||
|
return TagString, string(literal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
panic(phasePanicMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *decodeState) error(msg string) *SyntaxError {
|
||||||
|
return &SyntaxError{Message: msg, Offset: d.off}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isIntegerType(c byte) bool {
|
||||||
|
return isFloatType(c) ||
|
||||||
|
c == 'B' || c == 'b' ||
|
||||||
|
c == 's' || c == 'S' ||
|
||||||
|
c == 'L' || c == 'l'
|
||||||
|
}
|
||||||
|
|
||||||
|
func isFloatType(c byte) bool {
|
||||||
|
return c == 'F' || c == 'f' || c == 'D' || c == 'd'
|
||||||
|
}
|
||||||
|
|
||||||
|
type SyntaxError struct {
|
||||||
|
Message string
|
||||||
|
Offset int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *SyntaxError) Error() string { return e.Message }
|
111
nbt/snbt_decode_test.go
Normal file
111
nbt/snbt_decode_test.go
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
package nbt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEncoder_WriteSNBT(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
e := NewEncoder(&buf)
|
||||||
|
testCases := []struct {
|
||||||
|
snbt string
|
||||||
|
nbt []byte
|
||||||
|
}{
|
||||||
|
{`10b`, []byte{1, 0, 0, 10}},
|
||||||
|
{`12S`, []byte{2, 0, 0, 0, 12}},
|
||||||
|
{`0`, []byte{3, 0, 0, 0, 0, 0, 0}},
|
||||||
|
{`12L`, []byte{4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12}},
|
||||||
|
|
||||||
|
{`""`, []byte{8, 0, 0, 0, 0}},
|
||||||
|
{`'""' `, []byte{8, 0, 0, 0, 2, '"', '"'}},
|
||||||
|
{`"ab\"c\""`, []byte{8, 0, 0, 0, 5, 'a', 'b', '"', 'c', '"'}},
|
||||||
|
{` "1\\23"`, []byte{8, 0, 0, 0, 4, '1', '\\', '2', '3'}},
|
||||||
|
|
||||||
|
{`{}`, []byte{10, 0, 0, 0}},
|
||||||
|
{`{a:1b}`, []byte{10, 0, 0, 1, 0, 1, 'a', 1, 0}},
|
||||||
|
{`{ a : 1b }`, []byte{10, 0, 0, 1, 0, 1, 'a', 1, 0}},
|
||||||
|
{`{b:1,2:c}`, []byte{10, 0, 0, 3, 0, 1, 'b', 0, 0, 0, 1, 8, 0, 1, '2', 0, 1, 'c', 0}},
|
||||||
|
{`{c:{d:{}}}`, []byte{10, 0, 0, 10, 0, 1, 'c', 10, 0, 1, 'd', 0, 0, 0}},
|
||||||
|
{`{h:{},"i":{}}`, []byte{10, 0, 0, 10, 0, 1, 'h', 0, 10, 0, 1, 'i', 0, 0}},
|
||||||
|
|
||||||
|
{`[]`, []byte{9, 0, 0, 0, 0, 0, 0, 0}},
|
||||||
|
{`[1b,2b,3b]`, []byte{9, 0, 0, 1, 0, 0, 0, 3, 1, 2, 3}},
|
||||||
|
{`[ 1b , 2b , 3b ]`, []byte{9, 0, 0, 1, 0, 0, 0, 3, 1, 2, 3}},
|
||||||
|
{`[a,"b",'c']`, []byte{9, 0, 0, 8, 0, 0, 0, 3, 0, 1, 'a', 0, 1, 'b', 0, 1, 'c'}},
|
||||||
|
{`[{},{a:1b},{}]`, []byte{9, 0, 0, 10, 0, 0, 0, 3, 0, 1, 0, 1, 'a', 1, 0, 0}},
|
||||||
|
{`[ { } , { a : 1b } , { } ] `, []byte{9, 0, 0, 10, 0, 0, 0, 3, 0, 1, 0, 1, 'a', 1, 0, 0}},
|
||||||
|
{`[[],[]]`, []byte{9, 0, 0, 9, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}},
|
||||||
|
|
||||||
|
{`[B; ]`, []byte{7, 0, 0, 0, 0, 0, 0}},
|
||||||
|
{`[B; 1b ,2B,3B]`, []byte{7, 0, 0, 0, 0, 0, 3, 1, 2, 3}},
|
||||||
|
{`[I;]`, []byte{11, 0, 0, 0, 0, 0, 0}},
|
||||||
|
{`[I; 1, 2 ,3]`, []byte{11, 0, 0, 0, 0, 0, 3, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 3}},
|
||||||
|
{`[L;]`, []byte{12, 0, 0, 0, 0, 0, 0}},
|
||||||
|
{`[ L; 1L,2L,3L]`, []byte{12, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3}},
|
||||||
|
|
||||||
|
{`{d:[]}`, []byte{10, 0, 0, 9, 0, 1, 'd', 0, 0, 0, 0, 0, 0}},
|
||||||
|
{`{e:[]}`, []byte{10, 0, 0, 9, 0, 1, 'e', 0, 0, 0, 0, 0, 0}},
|
||||||
|
{`{f:[], g:[]}`, []byte{10, 0, 0, 9, 0, 1, 'f', 0, 0, 0, 0, 0, 9, 0, 1, 'g', 0, 0, 0, 0, 0, 0}},
|
||||||
|
}
|
||||||
|
for i := range testCases {
|
||||||
|
buf.Reset()
|
||||||
|
if err := e.WriteSNBT(testCases[i].snbt); err != nil {
|
||||||
|
t.Errorf("Convert SNBT %q error: %v", testCases[i].snbt, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
want := testCases[i].nbt
|
||||||
|
got := buf.Bytes()
|
||||||
|
if !bytes.Equal(want, got) {
|
||||||
|
t.Errorf("Convert SNBT %q wrong:\nwant: % 02X\ngot: % 02X", testCases[i].snbt, want, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncoder_WriteSNBT_bigTest(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
e := NewEncoder(&buf)
|
||||||
|
|
||||||
|
err := e.WriteSNBT(bigTestSNBT)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkEncoder_WriteSNBT_bigTest(b *testing.B) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
e := NewEncoder(&buf)
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
err := e.WriteSNBT(bigTestSNBT)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
buf.Reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_WriteSNBT_nestingList(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
e := NewEncoder(&buf)
|
||||||
|
|
||||||
|
// Our maximum supported nesting depth is 10000.
|
||||||
|
// The nesting depth of 10001 is 10000
|
||||||
|
err := e.WriteSNBT(strings.Repeat("[", 10001) + strings.Repeat("]", 10001))
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Following code should return error instant of panic.
|
||||||
|
buf.Reset()
|
||||||
|
err = e.WriteSNBT(strings.Repeat("[", 10002) + strings.Repeat("]", 10002))
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Exceeded the maximum depth of support, but no error was reported")
|
||||||
|
}
|
||||||
|
// Panic test
|
||||||
|
buf.Reset()
|
||||||
|
err = e.WriteSNBT(strings.Repeat("[", 20000) + strings.Repeat("]", 20000))
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Exceeded the maximum depth of support, but no error was reported")
|
||||||
|
}
|
||||||
|
}
|
421
nbt/snbt_scanner.go
Normal file
421
nbt/snbt_scanner.go
Normal file
@ -0,0 +1,421 @@
|
|||||||
|
package nbt
|
||||||
|
|
||||||
|
import "strconv"
|
||||||
|
|
||||||
|
const (
|
||||||
|
scanContinue = iota // uninteresting byte
|
||||||
|
scanBeginLiteral // end implied by next result != scanContinue
|
||||||
|
scanBeginCompound // begin TAG_Compound (after left-brace )
|
||||||
|
scanBeginList // begin TAG_List (after left-brack)
|
||||||
|
scanListValue // just finished read list value (after comma)
|
||||||
|
scanListType // just finished read list type (after "B;" or "L;")
|
||||||
|
scanCompoundTagName // just finished read tag name (before colon)
|
||||||
|
scanCompoundValue // just finished read value (after comma)
|
||||||
|
scanSkipSpace // space byte; can skip; known to be last "continue" result
|
||||||
|
scanEndValue
|
||||||
|
|
||||||
|
scanEnd
|
||||||
|
scanError
|
||||||
|
)
|
||||||
|
|
||||||
|
// These values are stored in the parseState stack.
|
||||||
|
// They give the current state of a composite value
|
||||||
|
// being scanned. If the parser is inside a nested value
|
||||||
|
// the parseState describes the nested state, outermost at entry 0.
|
||||||
|
const (
|
||||||
|
parseCompoundName = iota // parsing tag name (before colon)
|
||||||
|
parseCompoundValue // parsing value (after colon)
|
||||||
|
parseListValue // parsing list
|
||||||
|
)
|
||||||
|
|
||||||
|
const maxNestingDepth = 10000
|
||||||
|
|
||||||
|
type scanner struct {
|
||||||
|
step func(s *scanner, c byte) int
|
||||||
|
parseState []int
|
||||||
|
errContext string
|
||||||
|
endTop bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset prepares the scanner for use.
|
||||||
|
// It must be called before calling s.step.
|
||||||
|
func (s *scanner) reset() {
|
||||||
|
s.step = stateBeginValue
|
||||||
|
s.parseState = s.parseState[0:0]
|
||||||
|
s.errContext = ""
|
||||||
|
s.endTop = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// pushParseState pushes a new parse state p onto the parse stack.
|
||||||
|
// an error state is returned if maxNestingDepth was exceeded, otherwise successState is returned.
|
||||||
|
func (s *scanner) pushParseState(c byte, newParseState int, successState int) int {
|
||||||
|
s.parseState = append(s.parseState, newParseState)
|
||||||
|
if len(s.parseState) <= maxNestingDepth {
|
||||||
|
return successState
|
||||||
|
}
|
||||||
|
return scanError
|
||||||
|
}
|
||||||
|
|
||||||
|
// popParseState pops a parse state (already obtained) off the stack
|
||||||
|
// and updates s.step accordingly.
|
||||||
|
func (s *scanner) popParseState() {
|
||||||
|
n := len(s.parseState) - 1
|
||||||
|
s.parseState = s.parseState[:n]
|
||||||
|
if n == 0 {
|
||||||
|
s.step = stateEndTop
|
||||||
|
s.endTop = true
|
||||||
|
} else {
|
||||||
|
s.step = stateEndValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// eof tells the scanner that the end of input has been reached.
|
||||||
|
// It returns a scan status just as s.step does.
|
||||||
|
func (s *scanner) eof() int {
|
||||||
|
if s.errContext != "" {
|
||||||
|
return scanError
|
||||||
|
}
|
||||||
|
if s.endTop {
|
||||||
|
return scanEnd
|
||||||
|
}
|
||||||
|
s.step(s, ' ')
|
||||||
|
if s.endTop {
|
||||||
|
return scanEnd
|
||||||
|
}
|
||||||
|
if s.errContext == "" {
|
||||||
|
s.errContext = "unexpected end of JSON input"
|
||||||
|
}
|
||||||
|
return scanError
|
||||||
|
}
|
||||||
|
|
||||||
|
// stateEndTop is the state after finishing the top-level value,
|
||||||
|
// such as after reading `{}` or `[1,2,3]`.
|
||||||
|
// Only space characters should be seen now.
|
||||||
|
func stateEndTop(s *scanner, c byte) int {
|
||||||
|
if !isSpace(c) {
|
||||||
|
// Complain about non-space byte on next call.
|
||||||
|
s.error(c, "after top-level value")
|
||||||
|
}
|
||||||
|
return scanEnd
|
||||||
|
}
|
||||||
|
|
||||||
|
func stateBeginValue(s *scanner, c byte) int {
|
||||||
|
if isSpace(c) {
|
||||||
|
s.step = stateBeginValue
|
||||||
|
return scanSkipSpace
|
||||||
|
}
|
||||||
|
switch c {
|
||||||
|
case '{': // beginning of TAG_Compound
|
||||||
|
s.step = stateCompoundOrEmpty
|
||||||
|
return s.pushParseState(c, parseCompoundName, scanBeginCompound)
|
||||||
|
case '[': // beginning of TAG_List
|
||||||
|
s.step = stateListOrArray
|
||||||
|
return s.pushParseState(c, parseListValue, scanBeginList)
|
||||||
|
case '"', '\'': // beginning of TAG_String
|
||||||
|
return stateBeginString(s, c)
|
||||||
|
case '-': // beginning of negative number
|
||||||
|
s.step = stateNeg
|
||||||
|
return scanBeginLiteral
|
||||||
|
default:
|
||||||
|
if isNumber(c) {
|
||||||
|
stateNum0(s, c)
|
||||||
|
return scanBeginLiteral
|
||||||
|
}
|
||||||
|
if isAllowedInUnquotedString(c) {
|
||||||
|
return stateBeginString(s, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s.error(c, "looking for beginning of value")
|
||||||
|
}
|
||||||
|
|
||||||
|
func stateCompoundOrEmpty(s *scanner, c byte) int {
|
||||||
|
if isSpace(c) {
|
||||||
|
return scanSkipSpace
|
||||||
|
}
|
||||||
|
if c == '}' {
|
||||||
|
n := len(s.parseState)
|
||||||
|
s.parseState[n-1] = parseCompoundValue
|
||||||
|
return stateEndValue(s, c)
|
||||||
|
}
|
||||||
|
return stateBeginString(s, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func stateBeginString(s *scanner, c byte) int {
|
||||||
|
if isSpace(c) {
|
||||||
|
return scanSkipSpace
|
||||||
|
}
|
||||||
|
switch c {
|
||||||
|
case '\'':
|
||||||
|
s.step = stateInSingleQuotedString
|
||||||
|
return scanBeginLiteral
|
||||||
|
case '"':
|
||||||
|
s.step = stateInDoubleQuotedString
|
||||||
|
return scanBeginLiteral
|
||||||
|
default:
|
||||||
|
if isAllowedInUnquotedString(c) {
|
||||||
|
s.step = stateInUnquotedString
|
||||||
|
return scanBeginLiteral
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s.error(c, "looking for beginning of string")
|
||||||
|
}
|
||||||
|
|
||||||
|
func stateInSingleQuotedString(s *scanner, c byte) int {
|
||||||
|
if c == '\\' {
|
||||||
|
s.step = stateInSingleQuotedStringEsc
|
||||||
|
return scanContinue
|
||||||
|
}
|
||||||
|
if c == '\'' {
|
||||||
|
s.step = stateEndValue
|
||||||
|
return scanContinue
|
||||||
|
}
|
||||||
|
return scanContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func stateInSingleQuotedStringEsc(s *scanner, c byte) int {
|
||||||
|
switch c {
|
||||||
|
case '\\', '\'':
|
||||||
|
s.step = stateInSingleQuotedString
|
||||||
|
return scanContinue
|
||||||
|
}
|
||||||
|
return s.error(c, "in string escape code")
|
||||||
|
}
|
||||||
|
|
||||||
|
func stateInDoubleQuotedString(s *scanner, c byte) int {
|
||||||
|
if c == '\\' {
|
||||||
|
s.step = stateInDqStringEsc
|
||||||
|
return scanContinue
|
||||||
|
}
|
||||||
|
if c == '"' {
|
||||||
|
s.step = stateEndValue
|
||||||
|
return scanContinue
|
||||||
|
}
|
||||||
|
return scanContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func stateInDqStringEsc(s *scanner, c byte) int {
|
||||||
|
switch c {
|
||||||
|
case 'b', 'f', 'n', 'r', 't', '\\', '/', '"':
|
||||||
|
s.step = stateInDoubleQuotedString
|
||||||
|
return scanContinue
|
||||||
|
}
|
||||||
|
return s.error(c, "in string escape code")
|
||||||
|
}
|
||||||
|
|
||||||
|
func stateInUnquotedString(s *scanner, c byte) int {
|
||||||
|
if isAllowedInUnquotedString(c) {
|
||||||
|
return scanContinue
|
||||||
|
}
|
||||||
|
return stateEndValue(s, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func stateListOrArray(s *scanner, c byte) int {
|
||||||
|
if isSpace(c) {
|
||||||
|
return scanSkipSpace
|
||||||
|
}
|
||||||
|
switch c {
|
||||||
|
case 'B', 'I', 'L':
|
||||||
|
s.step = stateListOrArrayT
|
||||||
|
return scanBeginLiteral
|
||||||
|
case ']':
|
||||||
|
return stateEndValue(s, c)
|
||||||
|
default:
|
||||||
|
return stateBeginValue(s, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stateListOrArrayT(s *scanner, c byte) int {
|
||||||
|
if c == ';' {
|
||||||
|
s.step = stateArrayT
|
||||||
|
return scanListType
|
||||||
|
}
|
||||||
|
return stateInUnquotedString(s, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func stateArrayT(s *scanner, c byte) int {
|
||||||
|
if isSpace(c) {
|
||||||
|
return scanSkipSpace
|
||||||
|
}
|
||||||
|
if c == ']' { // empty array
|
||||||
|
return scanEndValue
|
||||||
|
}
|
||||||
|
return stateBeginValue(s, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func stateNeg(s *scanner, c byte) int {
|
||||||
|
if isNumber(c) {
|
||||||
|
s.step = stateNum0
|
||||||
|
return scanBeginLiteral
|
||||||
|
}
|
||||||
|
if isAllowedInUnquotedString(c) {
|
||||||
|
s.step = stateInUnquotedString
|
||||||
|
return scanBeginLiteral
|
||||||
|
}
|
||||||
|
return s.error(c, "not a number after '-'")
|
||||||
|
}
|
||||||
|
|
||||||
|
func stateNum0(s *scanner, c byte) int {
|
||||||
|
if isNumber(c) {
|
||||||
|
s.step = stateNum1
|
||||||
|
return scanContinue
|
||||||
|
}
|
||||||
|
return stateEndNumValue(s, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func stateNum1(s *scanner, c byte) int {
|
||||||
|
if isNumber(c) {
|
||||||
|
s.step = stateNum1
|
||||||
|
return scanContinue
|
||||||
|
}
|
||||||
|
if c == '.' {
|
||||||
|
s.step = stateNumDot
|
||||||
|
return scanContinue
|
||||||
|
}
|
||||||
|
return stateEndNumValue(s, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// stateDot is the state after reading the integer and decimal point in a number,
|
||||||
|
// such as after reading `1.`.
|
||||||
|
func stateNumDot(s *scanner, c byte) int {
|
||||||
|
if isNumber(c) {
|
||||||
|
s.step = stateNumDot0
|
||||||
|
return scanContinue
|
||||||
|
}
|
||||||
|
if isAllowedInUnquotedString(c) {
|
||||||
|
s.step = stateInUnquotedString
|
||||||
|
return scanContinue
|
||||||
|
}
|
||||||
|
return s.error(c, "after decimal point in numeric literal")
|
||||||
|
}
|
||||||
|
|
||||||
|
// stateNumDot0 is the state after reading the integer, decimal point, and subsequent
|
||||||
|
// digits of a number, such as after reading `3.14`.
|
||||||
|
func stateNumDot0(s *scanner, c byte) int {
|
||||||
|
if isNumber(c) {
|
||||||
|
s.step = stateNumDot0
|
||||||
|
return scanContinue
|
||||||
|
}
|
||||||
|
return stateEndNumDotValue(s, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func stateEndNumValue(s *scanner, c byte) int {
|
||||||
|
switch c {
|
||||||
|
case 'b', 'B': // TAG_Byte
|
||||||
|
s.step = stateEndValue
|
||||||
|
return scanContinue
|
||||||
|
case 's', 'S': // TAG_Short
|
||||||
|
s.step = stateEndValue
|
||||||
|
return scanContinue
|
||||||
|
case 'l', 'L': // TAG_Long
|
||||||
|
s.step = stateEndValue
|
||||||
|
return scanContinue
|
||||||
|
case 'f', 'F', 'd', 'D':
|
||||||
|
return stateEndNumDotValue(s, c)
|
||||||
|
}
|
||||||
|
if isAllowedInUnquotedString(c) {
|
||||||
|
s.step = stateInUnquotedString
|
||||||
|
return scanContinue
|
||||||
|
}
|
||||||
|
return stateEndValue(s, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func stateEndNumDotValue(s *scanner, c byte) int {
|
||||||
|
switch c {
|
||||||
|
case 'f', 'F': // TAG_Float
|
||||||
|
s.step = stateEndValue
|
||||||
|
return scanContinue
|
||||||
|
case 'd', 'D': // TAG_Double
|
||||||
|
s.step = stateEndValue
|
||||||
|
return scanContinue
|
||||||
|
}
|
||||||
|
return stateEndValue(s, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func stateEndValue(s *scanner, c byte) int {
|
||||||
|
n := len(s.parseState)
|
||||||
|
if n == 0 {
|
||||||
|
// Completed top-level before the current byte.
|
||||||
|
s.step = stateEndTop
|
||||||
|
s.endTop = true
|
||||||
|
return stateEndTop(s, c)
|
||||||
|
}
|
||||||
|
if isSpace(c) {
|
||||||
|
return scanSkipSpace
|
||||||
|
}
|
||||||
|
|
||||||
|
ps := s.parseState[n-1]
|
||||||
|
switch ps {
|
||||||
|
case parseCompoundName:
|
||||||
|
if c == ':' {
|
||||||
|
s.parseState[n-1] = parseCompoundValue
|
||||||
|
s.step = stateBeginValue
|
||||||
|
return scanCompoundTagName
|
||||||
|
}
|
||||||
|
return s.error(c, "after compound tag name")
|
||||||
|
case parseCompoundValue:
|
||||||
|
switch c {
|
||||||
|
case ',':
|
||||||
|
s.parseState[n-1] = parseCompoundName
|
||||||
|
s.step = stateBeginString
|
||||||
|
return scanCompoundValue
|
||||||
|
case '}':
|
||||||
|
s.popParseState()
|
||||||
|
return scanEndValue
|
||||||
|
}
|
||||||
|
return s.error(c, "after compound value")
|
||||||
|
case parseListValue:
|
||||||
|
switch c {
|
||||||
|
case ',':
|
||||||
|
s.step = stateBeginValue
|
||||||
|
return scanListValue
|
||||||
|
case ']':
|
||||||
|
s.popParseState()
|
||||||
|
return scanEndValue
|
||||||
|
}
|
||||||
|
return s.error(c, "after list element")
|
||||||
|
}
|
||||||
|
return s.error(c, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *scanner) error(c byte, context string) int {
|
||||||
|
s.step = stateError
|
||||||
|
s.errContext = "invalid character " + quoteChar(c) + " " + context
|
||||||
|
return scanError
|
||||||
|
}
|
||||||
|
|
||||||
|
// stateError is the state after reaching a syntax error,
|
||||||
|
// such as after reading `[1}` or `5.1.2`.
|
||||||
|
func stateError(*scanner, byte) int {
|
||||||
|
return scanError
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSpace(c byte) bool {
|
||||||
|
return c <= ' ' && (c == ' ' || c == '\t' || c == '\r' || c == '\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
func isNumber(c byte) bool {
|
||||||
|
return c >= '0' && c <= '9'
|
||||||
|
}
|
||||||
|
|
||||||
|
func isAllowedInUnquotedString(c byte) bool {
|
||||||
|
return c == '_' || c == '-' ||
|
||||||
|
c == '.' || c == '+' ||
|
||||||
|
c >= '0' && c <= '9' ||
|
||||||
|
c >= 'A' && c <= 'Z' ||
|
||||||
|
c >= 'a' && c <= 'z'
|
||||||
|
}
|
||||||
|
|
||||||
|
// quoteChar formats c as a quoted character literal
|
||||||
|
func quoteChar(c byte) string {
|
||||||
|
// special cases - different from quoted strings
|
||||||
|
if c == '\'' {
|
||||||
|
return `'\''`
|
||||||
|
}
|
||||||
|
if c == '"' {
|
||||||
|
return `'"'`
|
||||||
|
}
|
||||||
|
|
||||||
|
// use quoted string with different quotation marks
|
||||||
|
s := strconv.Quote(string(c))
|
||||||
|
return "'" + s[1:len(s)-1] + "'"
|
||||||
|
}
|
103
nbt/snbt_scanner_test.go
Normal file
103
nbt/snbt_scanner_test.go
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
package nbt
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSNBT_checkScanCode(t *testing.T) {
|
||||||
|
//t.SkipNow()
|
||||||
|
var s scanner
|
||||||
|
s.reset()
|
||||||
|
for _, c := range []byte(`[I;123,345], `) {
|
||||||
|
t.Logf("[%c] - %d", c, s.step(&s, c))
|
||||||
|
}
|
||||||
|
t.Logf("[%c] - %d", ' ', s.eof())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSNBT_number(t *testing.T) {
|
||||||
|
goods := []string{
|
||||||
|
"0", "1234567890", "3.1415926",
|
||||||
|
"-0", "-1234567890", "-3.1415926",
|
||||||
|
"255B", "1234s", "6666L",
|
||||||
|
"314F", "3.14f", "3.14159265358979323846264D",
|
||||||
|
}
|
||||||
|
var s scanner
|
||||||
|
scan := func(str string) bool {
|
||||||
|
s.reset()
|
||||||
|
for _, c := range []byte(str) {
|
||||||
|
res := s.step(&s, c)
|
||||||
|
if res == scanError {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, str := range goods {
|
||||||
|
if scan(str) == false {
|
||||||
|
t.Errorf("scan valid data %q error: %v", str, s.errContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:embed bigTest_test.snbt
|
||||||
|
var bigTestSNBT string
|
||||||
|
|
||||||
|
func TestSNBT_compound(t *testing.T) {
|
||||||
|
goods := []string{
|
||||||
|
`{}`, `{name:3.14f}`, `{ "name" : 12345 }`,
|
||||||
|
`{ abc: { }}`, `{ "a b\"c": {}, def: 12345}`,
|
||||||
|
`{ ghi: [], klm: 1}`,
|
||||||
|
bigTestSNBT,
|
||||||
|
}
|
||||||
|
var s scanner
|
||||||
|
for _, str := range goods {
|
||||||
|
s.reset()
|
||||||
|
for i, c := range []byte(str) {
|
||||||
|
res := s.step(&s, c)
|
||||||
|
if res == scanError {
|
||||||
|
t.Errorf("scan valid data %q error: %v at [%d]", str[:i], s.errContext, i)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestSNBT_list(t *testing.T) {
|
||||||
|
goods := []string{
|
||||||
|
`[]`, `[a, 'b', "c", d]`, // List of string
|
||||||
|
`[{}, {}, {"a\"b":520}]`, // List of Compound
|
||||||
|
`[B,C,D]`, `[L, "abc"]`, // List of string (like array)
|
||||||
|
`[B; 01B, 02B, 3B, 10B, 127B]`, // Array
|
||||||
|
`[I;]`, `[B; ]`, // Empty array
|
||||||
|
}
|
||||||
|
var s scanner
|
||||||
|
scan := func(str string) bool {
|
||||||
|
s.reset()
|
||||||
|
for _, c := range []byte(str) {
|
||||||
|
res := s.step(&s, c)
|
||||||
|
if res == scanError {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, str := range goods {
|
||||||
|
if scan(str) == false {
|
||||||
|
t.Errorf("scan valid data %q error: %v", str, s.errContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkSNBT_bigTest(b *testing.B) {
|
||||||
|
var s scanner
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
s.reset()
|
||||||
|
for _, c := range []byte(bigTestSNBT) {
|
||||||
|
res := s.step(&s, c)
|
||||||
|
if res == scanError {
|
||||||
|
b.Errorf("scan valid data %q error: %v at [%d]", bigTestSNBT[:i], s.errContext, i)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user