Initial commit: RegionIO Minecraft server core (26.1.2/protocol 775)
Vanilla-faithful overworld generator (final_density + multi-noise biomes), full connection lifecycle (status/login/configuration/play), chunk streaming, creative block editing, and the protocol/nbt/registry infrastructure.
This commit is contained in:
commit
a7bb9496ae
146 changed files with 217621 additions and 0 deletions
229
internal/nbt/decode.go
Normal file
229
internal/nbt/decode.go
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
package nbt
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"math"
|
||||
)
|
||||
|
||||
var (
|
||||
errTruncated = errors.New("nbt: truncated input")
|
||||
errBadTag = errors.New("nbt: unknown tag id")
|
||||
errNegativeLen = errors.New("nbt: negative length")
|
||||
)
|
||||
|
||||
// decoder walks a byte slice, tracking a cursor.
|
||||
type decoder struct {
|
||||
b []byte
|
||||
pos int
|
||||
}
|
||||
|
||||
// Unmarshal decodes a network-format payload (unnamed root) into a Tag.
|
||||
func Unmarshal(b []byte) (Tag, error) {
|
||||
d := &decoder{b: b}
|
||||
id, err := d.u8()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if id == TagEnd {
|
||||
return nil, nil
|
||||
}
|
||||
return d.payload(id)
|
||||
}
|
||||
|
||||
// UnmarshalNamed decodes a classic named-format payload, returning the root
|
||||
// name and tag.
|
||||
func UnmarshalNamed(b []byte) (string, Tag, error) {
|
||||
d := &decoder{b: b}
|
||||
id, err := d.u8()
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
if id == TagEnd {
|
||||
return "", nil, nil
|
||||
}
|
||||
name, err := d.str()
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
t, err := d.payload(id)
|
||||
return name, t, err
|
||||
}
|
||||
|
||||
func (d *decoder) need(n int) error {
|
||||
if n < 0 {
|
||||
return errNegativeLen
|
||||
}
|
||||
if d.pos+n > len(d.b) {
|
||||
return errTruncated
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *decoder) u8() (byte, error) {
|
||||
if err := d.need(1); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
v := d.b[d.pos]
|
||||
d.pos++
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (d *decoder) u16() (uint16, error) {
|
||||
if err := d.need(2); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
v := binary.BigEndian.Uint16(d.b[d.pos:])
|
||||
d.pos += 2
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (d *decoder) u32() (uint32, error) {
|
||||
if err := d.need(4); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
v := binary.BigEndian.Uint32(d.b[d.pos:])
|
||||
d.pos += 4
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (d *decoder) u64() (uint64, error) {
|
||||
if err := d.need(8); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
v := binary.BigEndian.Uint64(d.b[d.pos:])
|
||||
d.pos += 8
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (d *decoder) str() (string, error) {
|
||||
n, err := d.u16()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := d.need(int(n)); err != nil {
|
||||
return "", err
|
||||
}
|
||||
s, err := decodeModifiedUTF8(d.b[d.pos : d.pos+int(n)])
|
||||
d.pos += int(n)
|
||||
return s, err
|
||||
}
|
||||
|
||||
// payload decodes a tag payload of the given type id.
|
||||
func (d *decoder) payload(id byte) (Tag, error) {
|
||||
switch id {
|
||||
case TagByte:
|
||||
v, err := d.u8()
|
||||
return Byte(int8(v)), err
|
||||
case TagShort:
|
||||
v, err := d.u16()
|
||||
return Short(int16(v)), err
|
||||
case TagInt:
|
||||
v, err := d.u32()
|
||||
return Int(int32(v)), err
|
||||
case TagLong:
|
||||
v, err := d.u64()
|
||||
return Long(int64(v)), err
|
||||
case TagFloat:
|
||||
v, err := d.u32()
|
||||
return Float(math.Float32frombits(v)), err
|
||||
case TagDouble:
|
||||
v, err := d.u64()
|
||||
return Double(math.Float64frombits(v)), err
|
||||
case TagByteArray:
|
||||
n, err := d.u32()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := d.need(int(int32(n))); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make(ByteArray, n)
|
||||
copy(out, d.b[d.pos:d.pos+int(n)])
|
||||
d.pos += int(n)
|
||||
return out, nil
|
||||
case TagString:
|
||||
s, err := d.str()
|
||||
return String(s), err
|
||||
case TagIntArray:
|
||||
n, err := d.u32()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make(IntArray, int32(n))
|
||||
for i := range out {
|
||||
v, err := d.u32()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out[i] = int32(v)
|
||||
}
|
||||
return out, nil
|
||||
case TagLongArray:
|
||||
n, err := d.u32()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make(LongArray, int32(n))
|
||||
for i := range out {
|
||||
v, err := d.u64()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out[i] = int64(v)
|
||||
}
|
||||
return out, nil
|
||||
case TagList:
|
||||
return d.list()
|
||||
case TagCompound:
|
||||
return d.compound()
|
||||
default:
|
||||
return nil, errBadTag
|
||||
}
|
||||
}
|
||||
|
||||
func (d *decoder) list() (Tag, error) {
|
||||
elemID, err := d.u8()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
n, err := d.u32()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
count := int(int32(n))
|
||||
if count < 0 {
|
||||
return nil, errNegativeLen
|
||||
}
|
||||
l := List{ElemID: elemID, Elems: make([]Tag, 0, count)}
|
||||
for i := 0; i < count; i++ {
|
||||
t, err := d.payload(elemID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l.Elems = append(l.Elems, t)
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
func (d *decoder) compound() (Tag, error) {
|
||||
c := NewCompound()
|
||||
for {
|
||||
id, err := d.u8()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if id == TagEnd {
|
||||
return c, nil
|
||||
}
|
||||
name, err := d.str()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t, err := d.payload(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.Set(name, t)
|
||||
}
|
||||
}
|
||||
95
internal/nbt/encode.go
Normal file
95
internal/nbt/encode.go
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
package nbt
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
|
||||
// Marshal encodes root in the network format: the root tag's type byte followed
|
||||
// by its payload, with no root name (Minecraft 1.20.2+).
|
||||
func Marshal(root Tag) []byte {
|
||||
dst := []byte{root.ID()}
|
||||
return appendPayload(dst, root)
|
||||
}
|
||||
|
||||
// MarshalNamed encodes root in the classic named format: type byte, root name,
|
||||
// then payload. Used for files and legacy framing.
|
||||
func MarshalNamed(name string, root Tag) []byte {
|
||||
dst := []byte{root.ID()}
|
||||
dst = appendString(dst, name)
|
||||
return appendPayload(dst, root)
|
||||
}
|
||||
|
||||
// appendString writes a modified-UTF-8 string with an unsigned-short length.
|
||||
func appendString(dst []byte, s string) []byte {
|
||||
start := len(dst)
|
||||
dst = append(dst, 0, 0) // length placeholder
|
||||
dst = encodeModifiedUTF8(dst, s)
|
||||
binary.BigEndian.PutUint16(dst[start:], uint16(len(dst)-start-2))
|
||||
return dst
|
||||
}
|
||||
|
||||
// appendPayload writes a tag's payload (no type byte, no name).
|
||||
func appendPayload(dst []byte, t Tag) []byte {
|
||||
switch v := t.(type) {
|
||||
case Byte:
|
||||
return append(dst, byte(v))
|
||||
case Short:
|
||||
return binary.BigEndian.AppendUint16(dst, uint16(v))
|
||||
case Int:
|
||||
return binary.BigEndian.AppendUint32(dst, uint32(v))
|
||||
case Long:
|
||||
return binary.BigEndian.AppendUint64(dst, uint64(v))
|
||||
case Float:
|
||||
return binary.BigEndian.AppendUint32(dst, math.Float32bits(float32(v)))
|
||||
case Double:
|
||||
return binary.BigEndian.AppendUint64(dst, math.Float64bits(float64(v)))
|
||||
case ByteArray:
|
||||
dst = binary.BigEndian.AppendUint32(dst, uint32(len(v)))
|
||||
return append(dst, v...)
|
||||
case String:
|
||||
return appendString(dst, string(v))
|
||||
case IntArray:
|
||||
dst = binary.BigEndian.AppendUint32(dst, uint32(len(v)))
|
||||
for _, n := range v {
|
||||
dst = binary.BigEndian.AppendUint32(dst, uint32(n))
|
||||
}
|
||||
return dst
|
||||
case LongArray:
|
||||
dst = binary.BigEndian.AppendUint32(dst, uint32(len(v)))
|
||||
for _, n := range v {
|
||||
dst = binary.BigEndian.AppendUint64(dst, uint64(n))
|
||||
}
|
||||
return dst
|
||||
case List:
|
||||
return appendList(dst, v)
|
||||
case *Compound:
|
||||
return appendCompound(dst, v)
|
||||
default:
|
||||
panic(fmt.Sprintf("nbt: cannot encode %T", t))
|
||||
}
|
||||
}
|
||||
|
||||
func appendList(dst []byte, l List) []byte {
|
||||
elemID := l.ElemID
|
||||
if len(l.Elems) == 0 {
|
||||
elemID = TagEnd // Java writes TagEnd for empty lists.
|
||||
}
|
||||
dst = append(dst, elemID)
|
||||
dst = binary.BigEndian.AppendUint32(dst, uint32(len(l.Elems)))
|
||||
for _, e := range l.Elems {
|
||||
dst = appendPayload(dst, e)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
func appendCompound(dst []byte, c *Compound) []byte {
|
||||
for _, k := range c.keys {
|
||||
t := c.m[k]
|
||||
dst = append(dst, t.ID())
|
||||
dst = appendString(dst, k)
|
||||
dst = appendPayload(dst, t)
|
||||
}
|
||||
return append(dst, TagEnd)
|
||||
}
|
||||
59
internal/nbt/mutf8.go
Normal file
59
internal/nbt/mutf8.go
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
package nbt
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"unicode/utf16"
|
||||
)
|
||||
|
||||
// errBadMUTF8 is returned when a modified-UTF-8 byte sequence is malformed.
|
||||
var errBadMUTF8 = errors.New("nbt: invalid modified UTF-8")
|
||||
|
||||
// encodeModifiedUTF8 encodes s as Java modified UTF-8: ASCII stays one byte,
|
||||
// NUL and code points up to U+07FF take two bytes, U+0800..U+FFFF take three,
|
||||
// and supplementary code points are split into a surrogate pair (six bytes).
|
||||
func encodeModifiedUTF8(dst []byte, s string) []byte {
|
||||
for _, u := range utf16.Encode([]rune(s)) {
|
||||
switch {
|
||||
case u >= 0x0001 && u <= 0x007F:
|
||||
dst = append(dst, byte(u))
|
||||
case u == 0 || u <= 0x07FF:
|
||||
dst = append(dst,
|
||||
0xC0|byte(u>>6),
|
||||
0x80|byte(u&0x3F))
|
||||
default:
|
||||
dst = append(dst,
|
||||
0xE0|byte(u>>12),
|
||||
0x80|byte((u>>6)&0x3F),
|
||||
0x80|byte(u&0x3F))
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// decodeModifiedUTF8 decodes b (length-delimited modified UTF-8) into a string.
|
||||
func decodeModifiedUTF8(b []byte) (string, error) {
|
||||
units := make([]uint16, 0, len(b))
|
||||
for i := 0; i < len(b); {
|
||||
c := b[i]
|
||||
switch {
|
||||
case c&0x80 == 0: // 0xxxxxxx
|
||||
units = append(units, uint16(c))
|
||||
i++
|
||||
case c&0xE0 == 0xC0: // 110xxxxx 10xxxxxx
|
||||
if i+1 >= len(b) || b[i+1]&0xC0 != 0x80 {
|
||||
return "", errBadMUTF8
|
||||
}
|
||||
units = append(units, uint16(c&0x1F)<<6|uint16(b[i+1]&0x3F))
|
||||
i += 2
|
||||
case c&0xF0 == 0xE0: // 1110xxxx 10xxxxxx 10xxxxxx
|
||||
if i+2 >= len(b) || b[i+1]&0xC0 != 0x80 || b[i+2]&0xC0 != 0x80 {
|
||||
return "", errBadMUTF8
|
||||
}
|
||||
units = append(units, uint16(c&0x0F)<<12|uint16(b[i+1]&0x3F)<<6|uint16(b[i+2]&0x3F))
|
||||
i += 3
|
||||
default:
|
||||
return "", errBadMUTF8
|
||||
}
|
||||
}
|
||||
return string(utf16.Decode(units)), nil
|
||||
}
|
||||
98
internal/nbt/nbt.go
Normal file
98
internal/nbt/nbt.go
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
// Package nbt implements Minecraft's Named Binary Tag format, including the
|
||||
// network variant used since 1.20.2 where the root tag carries no name.
|
||||
//
|
||||
// All numeric payloads are big-endian. Strings use Java's "modified UTF-8":
|
||||
// an unsigned-short byte length followed by bytes where NUL and supplementary
|
||||
// code points are escaped (see encode/decodeModifiedUTF8).
|
||||
package nbt
|
||||
|
||||
// Tag type identifiers as defined by the NBT specification.
|
||||
const (
|
||||
TagEnd = 0x00
|
||||
TagByte = 0x01
|
||||
TagShort = 0x02
|
||||
TagInt = 0x03
|
||||
TagLong = 0x04
|
||||
TagFloat = 0x05
|
||||
TagDouble = 0x06
|
||||
TagByteArray = 0x07
|
||||
TagString = 0x08
|
||||
TagList = 0x09
|
||||
TagCompound = 0x0A
|
||||
TagIntArray = 0x0B
|
||||
TagLongArray = 0x0C
|
||||
)
|
||||
|
||||
// Tag is any NBT value. ID returns its tag type identifier.
|
||||
type Tag interface{ ID() byte }
|
||||
|
||||
// Primitive and array tag types map directly onto Go types.
|
||||
type (
|
||||
Byte int8
|
||||
Short int16
|
||||
Int int32
|
||||
Long int64
|
||||
Float float32
|
||||
Double float64
|
||||
ByteArray []byte
|
||||
String string
|
||||
IntArray []int32
|
||||
LongArray []int64
|
||||
)
|
||||
|
||||
func (Byte) ID() byte { return TagByte }
|
||||
func (Short) ID() byte { return TagShort }
|
||||
func (Int) ID() byte { return TagInt }
|
||||
func (Long) ID() byte { return TagLong }
|
||||
func (Float) ID() byte { return TagFloat }
|
||||
func (Double) ID() byte { return TagDouble }
|
||||
func (ByteArray) ID() byte { return TagByteArray }
|
||||
func (String) ID() byte { return TagString }
|
||||
func (IntArray) ID() byte { return TagIntArray }
|
||||
func (LongArray) ID() byte { return TagLongArray }
|
||||
|
||||
// List is a homogeneous sequence of unnamed tags. ElemID must match the type
|
||||
// of every element; for an empty list it is written as TagEnd, per Java.
|
||||
type List struct {
|
||||
ElemID byte
|
||||
Elems []Tag
|
||||
}
|
||||
|
||||
func (List) ID() byte { return TagList }
|
||||
|
||||
// Compound is an ordered set of named tags. Insertion order is preserved so
|
||||
// that encoding is deterministic (the protocol does not require any order).
|
||||
type Compound struct {
|
||||
keys []string
|
||||
m map[string]Tag
|
||||
}
|
||||
|
||||
func (*Compound) ID() byte { return TagCompound }
|
||||
|
||||
// NewCompound returns an empty compound ready for Set.
|
||||
func NewCompound() *Compound {
|
||||
return &Compound{m: make(map[string]Tag)}
|
||||
}
|
||||
|
||||
// Set inserts or replaces the tag for name, preserving first-insertion order,
|
||||
// and returns the compound for chaining.
|
||||
func (c *Compound) Set(name string, t Tag) *Compound {
|
||||
if _, ok := c.m[name]; !ok {
|
||||
c.keys = append(c.keys, name)
|
||||
}
|
||||
c.m[name] = t
|
||||
return c
|
||||
}
|
||||
|
||||
// Get returns the tag for name and whether it is present.
|
||||
func (c *Compound) Get(name string) (Tag, bool) {
|
||||
t, ok := c.m[name]
|
||||
return t, ok
|
||||
}
|
||||
|
||||
// Len returns the number of entries.
|
||||
func (c *Compound) Len() int { return len(c.keys) }
|
||||
|
||||
// Keys returns the entry names in insertion order. The slice must not be
|
||||
// mutated by callers.
|
||||
func (c *Compound) Keys() []string { return c.keys }
|
||||
119
internal/nbt/nbt_test.go
Normal file
119
internal/nbt/nbt_test.go
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
package nbt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestGoldenHelloWorld checks the canonical NBT example: a named-root compound
|
||||
// {"hello world": {"name": "Bananrama"}}.
|
||||
func TestGoldenHelloWorld(t *testing.T) {
|
||||
root := NewCompound().Set("name", String("Bananrama"))
|
||||
got := MarshalNamed("hello world", root)
|
||||
|
||||
want := []byte{
|
||||
0x0a, 0x00, 0x0b, // TAG_Compound, name len 11
|
||||
'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd',
|
||||
0x08, 0x00, 0x04, 'n', 'a', 'm', 'e', // TAG_String, name len 4
|
||||
0x00, 0x09, // string len 9
|
||||
'B', 'a', 'n', 'a', 'n', 'r', 'a', 'm', 'a',
|
||||
0x00, // TAG_End
|
||||
}
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Fatalf("golden mismatch:\n got=%x\nwant=%x", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestNetworkRootHasNoName verifies the 1.20.2+ network root omits the name.
|
||||
func TestNetworkRootHasNoName(t *testing.T) {
|
||||
got := Marshal(NewCompound().Set("a", Byte(1)))
|
||||
want := []byte{
|
||||
0x0a, // TAG_Compound (no name)
|
||||
0x01, 0x00, 0x01, 'a', // TAG_Byte, name len 1
|
||||
0x01, // value 1
|
||||
0x00, // TAG_End
|
||||
}
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Fatalf("network root mismatch:\n got=%x\nwant=%x", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRoundTrip encodes a richly-nested compound and decodes it back.
|
||||
func TestRoundTrip(t *testing.T) {
|
||||
root := NewCompound().
|
||||
Set("b", Byte(-7)).
|
||||
Set("s", Short(300)).
|
||||
Set("i", Int(-123456)).
|
||||
Set("l", Long(1<<40)).
|
||||
Set("f", Float(3.5)).
|
||||
Set("d", Double(-2.25)).
|
||||
Set("str", String("héllo")).
|
||||
Set("bytes", ByteArray{1, 2, 3}).
|
||||
Set("ints", IntArray{10, -20, 30}).
|
||||
Set("longs", LongArray{1, 2}).
|
||||
Set("list", List{ElemID: TagInt, Elems: []Tag{Int(1), Int(2), Int(3)}}).
|
||||
Set("empty", List{ElemID: TagString}).
|
||||
Set("nested", NewCompound().Set("x", String("y")))
|
||||
|
||||
enc := Marshal(root)
|
||||
dec, err := Unmarshal(enc)
|
||||
if err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
reenc := Marshal(dec)
|
||||
if !bytes.Equal(enc, reenc) {
|
||||
t.Fatalf("round-trip bytes differ:\n first=%x\nsecond=%x", enc, reenc)
|
||||
}
|
||||
}
|
||||
|
||||
// TestModifiedUTF8 round-trips strings including NUL and supplementary chars.
|
||||
func TestModifiedUTF8(t *testing.T) {
|
||||
cases := []string{
|
||||
"",
|
||||
"plain ascii",
|
||||
"with\x00nul",
|
||||
"café — français",
|
||||
"emoji 😀 supplementary",
|
||||
}
|
||||
for _, s := range cases {
|
||||
enc := encodeModifiedUTF8(nil, s)
|
||||
// NUL must be escaped as 0xC0 0x80, never a literal zero byte.
|
||||
if bytes.IndexByte(enc, 0) >= 0 {
|
||||
t.Fatalf("encoding of %q contains a literal NUL byte", s)
|
||||
}
|
||||
dec, err := decodeModifiedUTF8(enc)
|
||||
if err != nil {
|
||||
t.Fatalf("decode %q: %v", s, err)
|
||||
}
|
||||
if dec != s {
|
||||
t.Fatalf("round-trip mismatch: got %q want %q", dec, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestDecodeNamedRoundTrip checks the named-format decode path.
|
||||
func TestDecodeNamedRoundTrip(t *testing.T) {
|
||||
root := NewCompound().Set("k", Int(42))
|
||||
enc := MarshalNamed("root", root)
|
||||
name, dec, err := UnmarshalNamed(enc)
|
||||
if err != nil {
|
||||
t.Fatalf("unmarshal named: %v", err)
|
||||
}
|
||||
if name != "root" {
|
||||
t.Fatalf("root name = %q, want %q", name, "root")
|
||||
}
|
||||
if !reflect.DeepEqual(Marshal(dec), Marshal(root)) {
|
||||
t.Fatal("named decode produced different tree")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTruncatedInput ensures the decoder rejects short buffers without panic.
|
||||
func TestTruncatedInput(t *testing.T) {
|
||||
enc := Marshal(NewCompound().Set("x", Long(1)))
|
||||
for n := 0; n < len(enc); n++ {
|
||||
if _, err := Unmarshal(enc[:n]); err == nil {
|
||||
t.Fatalf("expected error decoding %d/%d bytes", n, len(enc))
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue