diff --git a/go.mod b/go.mod index 2906a27..1cda09f 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,10 @@ module github.com/tailscale/sqlite -go 1.20 +go 1.22 + +require golang.org/x/tools v0.21.0 + +require ( + golang.org/x/mod v0.17.0 // indirect + golang.org/x/sync v0.7.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..05680b3 --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw= +golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= diff --git a/jsonb/jsonb.go b/jsonb/jsonb.go new file mode 100644 index 0000000..a54e64f --- /dev/null +++ b/jsonb/jsonb.go @@ -0,0 +1,273 @@ +// Package jsonb handles SQLite's JSONB format. +// +// See https://sqlite.org/draft/jsonb.html. +package jsonb + +//go:generate go run golang.org/x/tools/cmd/stringer -type=Type + +import ( + "encoding/binary" + "errors" + "fmt" + "math" + "strconv" +) + +// Value is a JSONB value. +// +// The methods on Value report whether it's valid, its type, length, +// and so on. +type Value []byte + +func (v Value) HeaderLen() int { + if len(v) == 0 { + return 0 + } + switch v[0] >> 4 { + default: + return 1 + case 0xc: + return 2 + case 0xd: + return 3 + case 0xe: + return 5 + case 0xf: + return 9 + } +} + +func (v Value) Type() Type { + if len(v) == 0 { + panic("Type called on invalid Value") + } + return Type(v[0] & 0xf) +} + +func (v Value) PayloadLen() int { + switch v.HeaderLen() { + default: + return 0 + case 1: + return int(v[0] >> 4) + case 2: + return int(v[1]) + case 3: + return int(binary.BigEndian.Uint16(v[1:])) + case 5: + n := binary.BigEndian.Uint32(v[1:]) + if int64(n) > math.MaxInt { + return 0 + } + return int(n) + case 9: + n := binary.BigEndian.Uint64(v[1:]) + if n > math.MaxInt { + return 0 + } + return int(n) + } +} + +// Payload returns the payload of the element. +// +// Depending on v's element type, the payload may be a series of zero+ +// concatenated valid Value elements. +func (v Value) Payload() []byte { + return v[v.HeaderLen():][:v.PayloadLen()] +} + +// RangeArray calls f for each element in v, which must be an array. It returns +// an error if v is not a valid array, or if f returns an error. +func (v Value) RangeArray(f func(Value) error) error { + if !v.Valid() { + return fmt.Errorf("not valid") + } + if v.Type() != Array { + return fmt.Errorf("got type %v; not an array", v.Type()) + } + pay := v.Payload() + for len(pay) > 0 { + v, rest, ok := Cut(pay) + pay = rest + if !ok { + return errors.New("malformed array payload") + } + if err := f(v); err != nil { + return err + } + } + return nil +} + +// RangeObject calls f for each pair in v, which must be an object. It returns +// an error if v is not a valid object, or if f returns an error. +func (v Value) RangeObject(f func(k, v Value) error) error { + if !v.Valid() { + return fmt.Errorf("not valid") + } + if v.Type() != Object { + return fmt.Errorf("got type %v; not an object", v.Type()) + } + pay := v.Payload() + for len(pay) > 0 { + key, rest, ok := Cut(pay) + pay = rest + if !ok { + return errors.New("malformed array payload") + } + val, rest, ok := Cut(pay) + pay = rest + if !ok { + return errors.New("malformed array payload") + } + if !key.Type().CanText() { + return errors.New("object key is not text") + } + if err := f(key, val); err != nil { + return err + } + } + return nil +} + +// Cut returns the first valid JSONB element in v, the rest of v, and whether +// the cut was successful. When ok is true, v is Valid. +func Cut(b []byte) (v Value, rest []byte, ok bool) { + if len(b) == 0 { + return nil, nil, false + } + v = Value(b) + hlen := v.HeaderLen() + if hlen == 0 { + return nil, nil, false + } + plen := v.PayloadLen() + if len(v) < hlen+plen { + return nil, nil, false + } + return v[:hlen+plen], b[hlen+plen:], true +} + +// Valid reports whether v contains a single valid JSONB value. +func (v Value) Valid() bool { + h := v.HeaderLen() + p := v.PayloadLen() + return h > 0 && len(v) == h+p +} + +// Text returns the unescaped text of v, which must be a text element. +func (v Value) Text() string { + t := v.Type() + if !t.CanText() { + panic("Text called on non-text Value") + } + switch t { + case Text: + return string(v.Payload()) + case TextJ: + got, err := appendUnquote(nil, v.Payload()) + if err != nil { + // TODO: add TextErr variant? + panic(err) + } + return string(got) + case TextRaw: + return string(v.Payload()) // TODO: escape stuff? + case Text5: + got, err := appendUnquote(nil, v.Payload()) + if err != nil { + // TODO: add TextErr variant? + panic(err) + } + return string(got) + } + panic("unreachable") +} + +// Int returns the integer value of v. +// It panics if v is not an integer type or can't fit in an int64. +// TODO(bradfitz): add IntOk for a non-panicking out-of-bounds version? +func (v Value) Int() int64 { + t := v.Type() + if !t.CanInt() { + panic("Int called on non-int Value") + } + switch t { + case Int: + n, err := strconv.ParseInt(string(v.Payload()), 10, 64) + if err != nil { + panic(err) + } + return n + default: + panic(fmt.Sprintf("TODO: handle %v", t)) + } +} + +// Float returns the float64 value of v. +// It panics if v is not an integer type or can't fit in an float64. +// TODO(bradfitz): add IntOk for a non-panicking out-of-bounds version? +func (v Value) Float() float64 { + t := v.Type() + if !t.CanFloat() { + panic("Float called on non-float Value") + } + switch t { + case Float: + n, err := strconv.ParseFloat(string(v.Payload()), 64) + if err != nil { + panic(err) + } + return n + default: + panic(fmt.Sprintf("TODO: handle %v", t)) + } +} + +// Type is a JSONB element type. +type Type byte + +const ( + Null Type = 0x0 + True Type = 0x1 + False Type = 0x2 + Int Type = 0x3 + Int5 Type = 0x4 + Float Type = 0x5 + Float5 Type = 0x6 + + // Text is a JSON string value that does not contain any escapes nor any + // characters that need to be escaped for either SQL or JSON + Text Type = 0x7 + // TextJ is a JSON string value that contains RFC 8259 character escapes + // (such as "\n" or "\u0020"). Those escapes will need to be translated into + // actual UTF8 if this element is extracted into SQL. The payload is the + // UTF8 text representation of the escaped string value. + TextJ Type = 0x8 + Text5 Type = 0x9 + TextRaw Type = 0xa + + Array Type = 0xb + Object Type = 0xc // pairs of key/value + + Reserved13 Type = 0xd + Reserved14 Type = 0xe + Reserved15 Type = 0xf +) + +func (t Type) CanText() bool { + return t >= Text && t <= TextRaw +} + +func (t Type) CanInt() bool { + return t == Int || t == Int5 +} + +func (t Type) CanBool() bool { + return t == True || t == False +} + +func (t Type) CanFloat() bool { + return t == Float || t == Float5 +} diff --git a/jsonb/jsonb_test.go b/jsonb/jsonb_test.go new file mode 100644 index 0000000..bd42fe6 --- /dev/null +++ b/jsonb/jsonb_test.go @@ -0,0 +1,234 @@ +package jsonb + +import ( + "database/sql" + "fmt" + "reflect" + "runtime" + "testing" + + _ "github.com/tailscale/sqlite" + _ "github.com/tailscale/sqlite/sqliteh" +) + +func TestJSONB(t *testing.T) { + tests := []struct { + v Value + + // want values + hdrLen int + payLen int + valid bool + }{ + {valid: false}, + {Value{0x13, 0x31}, 1, 1, true}, + {Value{0xc3, 0x01, 0x31}, 2, 1, true}, + {Value{0xd3, 0x00, 0x01, 0x31}, 3, 1, true}, + {Value{0xe3, 0x00, 0x00, 0x00, 0x01, 0x31}, 5, 1, true}, + {Value{0xf3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x31}, 9, 1, true}, + {Value{0xc3, 0x01, 0x31, 'e', 'x', 't', 'r', 'a'}, 2, 1, false}, + } + for i, tt := range tests { + t.Run(fmt.Sprintf("test-%d-% 02x", i, []byte(tt.v)), func(t *testing.T) { + if got := tt.v.HeaderLen(); got != tt.hdrLen { + t.Errorf("HeaderLen = %v, want %v", got, tt.hdrLen) + } + if got := tt.v.PayloadLen(); got != tt.payLen { + t.Errorf("PayloadLen = %v, want %v", got, tt.payLen) + } + if got := tt.v.Valid(); got != tt.valid { + t.Errorf("Valid = %v, want %v", got, tt.valid) + } + }) + } +} + +func toJSONB(t testing.TB, json string) Value { + return valueFromDB(t, "SELECT jsonb(?)", json) +} + +func valueFromDB(t testing.TB, sqlExpr string, args ...any) Value { + db := openTestDB(t) + defer db.Close() + t.Helper() + var b []byte + if err := db.QueryRow(sqlExpr, args...).Scan(&b); err != nil { + t.Fatal(err) + } + v := Value(b) + if !v.Valid() { + t.Fatalf("JSONB Value from DB was not valid") + } + return v +} + +func TestFromSQLite(t *testing.T) { + checks := func(fns ...func(testing.TB, Value)) []func(testing.TB, Value) { + return fns + } + isType := func(want Type) func(t testing.TB, v Value) { + return func(t testing.TB, v Value) { + t.Helper() + if got := v.Type(); got != want { + t.Fatalf("Type = %v, want %v", got, want) + } + } + } + hasText := func(want string) func(t testing.TB, v Value) { + return func(t testing.TB, v Value) { + t.Helper() + if got := v.Text(); got != want { + t.Fatalf("Text = %q, want %q", got, want) + } + } + } + tests := []struct { + name string + + // one of json or expr must be set + json string // if non-empty, the JSON to pass to toJSONB + expr string // if non-empty, the SQL expression to pass to valueFromDB + + checks []func(testing.TB, Value) + }{ + {name: "null", json: "null", checks: checks(isType(Null))}, + {name: "true", json: "true", checks: checks(isType(True))}, + {name: "false", json: "false", checks: checks(isType(False))}, + {name: "int", json: "123", checks: checks(isType(Int))}, + {name: "int5", json: "0x20", checks: checks(isType(Int5))}, + {name: "float", json: "0.5", checks: checks(isType(Float))}, + {name: "float5", json: ".5", checks: checks(isType(Float5))}, + {name: "text", json: `"foo"`, checks: checks(isType(Text))}, + {name: "text5", json: `"\0"`, checks: checks(isType(Text5), hasText("\x00"))}, + {name: "textj", json: `"foo\nbar"`, checks: checks(isType(TextJ))}, + {name: "textraw", expr: `SELECT jsonb_replace('null','$','example')`, checks: checks( + isType(TextRaw), + hasText("example"), + )}, + {name: "textraw", expr: `SELECT jsonb_replace('null','$','exam"ple')`, checks: checks( + isType(TextRaw), + hasText("exam\"ple"), + )}, + {name: "array", json: `[1,2,3]`, checks: checks(isType(Array))}, + {name: "object", json: `{"foo":123}`, checks: checks(isType(Object))}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var v Value + if tt.json != "" { + v = Value(toJSONB(t, tt.json)) + } else if tt.expr != "" { + v = valueFromDB(t, tt.expr) + } else { + t.Fatal("json or expr must be set") + } + for _, check := range tt.checks { + check(t, v) + } + }) + } + + t.Run("array", func(t *testing.T) { + in := `[1, 2, 3.5, {"foo":[3,4,false,5]}, "six", true, null, "foo\nbar"]` + v := Value(toJSONB(t, in)) + + if v.Type() != Array { + t.Fatalf("want array; got %v", v.Type()) + } + got := []string{} + want := []string{ + "Int-1", + "Int-2", + "Float-3.5", + "Object", + "Text-six", + "True", + "Null", + "TextJ-foo\nbar"} + if err := v.RangeArray(func(v Value) error { + s := v.Type().String() + if v.Type().CanText() { + s += "-" + v.Text() + } + if v.Type().CanInt() { + s += "-" + fmt.Sprint(v.Int()) + } + if v.Type().CanFloat() { + s += "-" + fmt.Sprint(v.Float()) + } + got = append(got, s) + return nil + }); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Fatalf("got %q; want %q", got, want) + } + }) + + t.Run("obj", func(t *testing.T) { + in := `{"null":null,"int":123,"array":[],"t":true,"f":false,"float":123.45,"obj":{}}` + v := Value(toJSONB(t, in)) + if v.Type() != Object { + t.Fatalf("want array; got %v", v.Type()) + } + got := map[string]string{} + want := map[string]string{ + "null": "Null", + "int": "Int", + "array": "Array", + "t": "True", + "f": "False", + "float": "Float", + "obj": "Object", + } + if err := v.RangeObject(func(k, v Value) error { + var ks string + if k.Type().CanText() { + ks = k.Text() + } + got[ks] = v.Type().String() + return nil + }); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Fatalf("got %v; want %v", got, want) + } + }) + + t.Run("json5", func(t *testing.T) { + v := Value(toJSONB(t, `.5`)) + t.Logf("type: %v, %q, % 02x", v.Type(), string(v), v) + t.Logf("payload: %q", v.Payload()) + + v = Value(toJSONB(t, `0x20`)) + t.Logf("type: %v, %q, % 02x", v.Type(), string(v), v) + t.Logf("payload: %q", v.Payload()) + }) +} + +func openTestDB(t testing.TB) *sql.DB { + t.Helper() + db, err := sql.Open("sqlite3", "file:"+t.TempDir()+"/test.db") + if err != nil { + t.Fatal(err) + } + configDB(t, db) + return db +} + +func configDB(t testing.TB, db *sql.DB) { + if _, err := db.Exec("PRAGMA journal_mode=WAL"); err != nil { + t.Fatal(err) + } + if _, err := db.Exec("PRAGMA synchronous=OFF"); err != nil { + t.Fatal(err) + } + numConns := runtime.GOMAXPROCS(0) + db.SetMaxOpenConns(numConns) + db.SetMaxIdleConns(numConns) + db.SetConnMaxLifetime(0) + db.SetConnMaxIdleTime(0) + t.Cleanup(func() { db.Close() }) +} diff --git a/jsonb/jsonwire.go b/jsonb/jsonwire.go new file mode 100644 index 0000000..16ab52c --- /dev/null +++ b/jsonb/jsonwire.go @@ -0,0 +1,227 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Stuff stolen from https://github.com/go-json-experiment/json/blob/af2d5061e6c2/internal/jsonwire/decode.go#L255 + +package jsonb + +import ( + "errors" + "io" + "slices" + "strconv" + "strings" + "unicode" + "unicode/utf16" + "unicode/utf8" +) + +// appendUnquote appends the unescaped form of a JSON string in src to dst. +// Any invalid UTF-8 within the string will be replaced with utf8.RuneError, +// but the error will be specified as having encountered such an error. +// The input must be an entire JSON string with no surrounding whitespace. +func appendUnquote[Bytes ~[]byte | ~string](dst []byte, src Bytes) (v []byte, err error) { + dst = slices.Grow(dst, len(src)) + + // Consume the leading double quote. + var i, n int + + // Consume every character in the string. + for uint(len(src)) > uint(n) { + // Optimize for long sequences of unescaped characters. + noEscape := func(c byte) bool { + return c < utf8.RuneSelf && ' ' <= c && c != '\\' && c != '"' + } + for uint(len(src)) > uint(n) && noEscape(src[n]) { + n++ + } + if uint(len(src)) <= uint(n) { + dst = append(dst, src[i:n]...) + return dst, nil + } + + switch r, rn := utf8.DecodeRuneInString(string(truncateMaxUTF8(src[n:]))); { + // Handle UTF-8 encoded byte sequence. + // Due to specialized handling of ASCII above, we know that + // all normal sequences at this point must be 2 bytes or larger. + case rn > 1: + n += rn + // Handle escape sequence. + case r == '\\': + dst = append(dst, src[i:n]...) + + // Handle escape sequence. + if uint(len(src)) < uint(n+2) { + return dst, io.ErrUnexpectedEOF + } + switch r := src[n+1]; r { + case '"', '\\', '/': + dst = append(dst, r) + n += 2 + case 'b': + dst = append(dst, '\b') + n += 2 + case 'f': + dst = append(dst, '\f') + n += 2 + case 'n': + dst = append(dst, '\n') + n += 2 + case 'r': + dst = append(dst, '\r') + n += 2 + case 't': + dst = append(dst, '\t') + n += 2 + case '0': + dst = append(dst, '\x00') + n += 2 + case 'u': + if uint(len(src)) < uint(n+6) { + if hasEscapedUTF16Prefix(src[n:], false) { + return dst, io.ErrUnexpectedEOF + } + return dst, newInvalidEscapeSequenceError(src[n:]) + } + v1, ok := parseHexUint16(src[n+2 : n+6]) + if !ok { + return dst, newInvalidEscapeSequenceError(src[n : n+6]) + } + n += 6 + + // Check whether this is a surrogate half. + r := rune(v1) + if utf16.IsSurrogate(r) { + r = utf8.RuneError // assume failure unless the following succeeds + if uint(len(src)) < uint(n+6) { + if hasEscapedUTF16Prefix(src[n:], true) { + return utf8.AppendRune(dst, r), io.ErrUnexpectedEOF + } + err = newInvalidEscapeSequenceError(src[n-6:]) + } else if v2, ok := parseHexUint16(src[n+2 : n+6]); src[n] != '\\' || src[n+1] != 'u' || !ok { + err = newInvalidEscapeSequenceError(src[n-6 : n+6]) + } else if r = utf16.DecodeRune(rune(v1), rune(v2)); r == utf8.RuneError { + err = newInvalidEscapeSequenceError(src[n-6 : n+6]) + } else { + n += 6 + } + } + + dst = utf8.AppendRune(dst, r) + default: + return dst, newInvalidEscapeSequenceError(src[n : n+2]) + } + i = n + // Handle invalid UTF-8. + case r == utf8.RuneError: + dst = append(dst, src[i:n]...) + if !utf8.FullRuneInString(string(truncateMaxUTF8(src[n:]))) { + return dst, io.ErrUnexpectedEOF + } + // NOTE: An unescaped string may be longer than the escaped string + // because invalid UTF-8 bytes are being replaced. + dst = append(dst, "\uFFFD"...) + n += rn + i = n + err = errInvalidUTF8 + // Handle invalid control characters. + case r < ' ': + dst = append(dst, src[i:n]...) + return dst, newInvalidCharacterError(src[n:], "within string (expecting non-control character)") + default: + panic("BUG: unhandled character " + quoteRune(src[n:])) + } + } + dst = append(dst, src[i:n]...) + return dst, io.ErrUnexpectedEOF +} + +func truncateMaxUTF8[Bytes ~[]byte | ~string](b Bytes) Bytes { + // TODO(https://go.dev/issue/56948): Remove this function and + // instead directly call generic utf8 functions wherever used. + if len(b) > utf8.UTFMax { + return b[:utf8.UTFMax] + } + return b +} + +// newError and errInvalidUTF8 are injected by the "jsontext" package, +// so that these error types use the jsontext.SyntacticError type. +var ( + newError = errors.New + errInvalidUTF8 = errors.New("invalid UTF-8 within string") +) + +// quoteRune quotes the first rune in the input. +func quoteRune[Bytes ~[]byte | ~string](b Bytes) string { + r, n := utf8.DecodeRuneInString(string(truncateMaxUTF8(b))) + if r == utf8.RuneError && n == 1 { + return `'\x` + strconv.FormatUint(uint64(b[0]), 16) + `'` + } + return strconv.QuoteRune(r) +} + +func newInvalidCharacterError[Bytes ~[]byte | ~string](prefix Bytes, where string) error { + what := quoteRune(prefix) + return newError("invalid character " + what + " " + where) +} + +func newInvalidEscapeSequenceError[Bytes ~[]byte | ~string](what Bytes) error { + label := "escape sequence" + if len(what) > 6 { + label = "surrogate pair" + } + needEscape := strings.IndexFunc(string(what), func(r rune) bool { + return r == '`' || r == utf8.RuneError || unicode.IsSpace(r) || !unicode.IsPrint(r) + }) >= 0 + if needEscape { + return newError("invalid " + label + " " + strconv.Quote(string(what)) + " within string") + } else { + return newError("invalid " + label + " `" + string(what) + "` within string") + } +} + +// hasEscapedUTF16Prefix reports whether b is possibly +// the truncated prefix of a \uFFFF escape sequence. +func hasEscapedUTF16Prefix[Bytes ~[]byte | ~string](b Bytes, lowerSurrogateHalf bool) bool { + for i := 0; i < len(b); i++ { + switch c := b[i]; { + case i == 0 && c != '\\': + return false + case i == 1 && c != 'u': + return false + case i == 2 && lowerSurrogateHalf && c != 'd' && c != 'D': + return false // not within ['\uDC00':'\uDFFF'] + case i == 3 && lowerSurrogateHalf && !('c' <= c && c <= 'f') && !('C' <= c && c <= 'F'): + return false // not within ['\uDC00':'\uDFFF'] + case i >= 2 && i < 6 && !('0' <= c && c <= '9') && !('a' <= c && c <= 'f') && !('A' <= c && c <= 'F'): + return false + } + } + return true +} + +// parseHexUint16 is similar to strconv.ParseUint, +// but operates directly on []byte and is optimized for base-16. +// See https://go.dev/issue/42429. +func parseHexUint16[Bytes ~[]byte | ~string](b Bytes) (v uint16, ok bool) { + if len(b) != 4 { + return 0, false + } + for i := 0; i < 4; i++ { + c := b[i] + switch { + case '0' <= c && c <= '9': + c = c - '0' + case 'a' <= c && c <= 'f': + c = 10 + c - 'a' + case 'A' <= c && c <= 'F': + c = 10 + c - 'A' + default: + return 0, false + } + v = v*16 + uint16(c) + } + return v, true +} diff --git a/jsonb/tools.go b/jsonb/tools.go new file mode 100644 index 0000000..ca6e69a --- /dev/null +++ b/jsonb/tools.go @@ -0,0 +1,7 @@ +//go:build for_go_mod_tidy + +package jsonb + +import ( + _ "golang.org/x/tools/cmd/stringer" // for go generate +) diff --git a/jsonb/type_string.go b/jsonb/type_string.go new file mode 100644 index 0000000..ab9e904 --- /dev/null +++ b/jsonb/type_string.go @@ -0,0 +1,38 @@ +// Code generated by "stringer -type=Type"; DO NOT EDIT. + +package jsonb + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[Null-0] + _ = x[True-1] + _ = x[False-2] + _ = x[Int-3] + _ = x[Int5-4] + _ = x[Float-5] + _ = x[Float5-6] + _ = x[Text-7] + _ = x[TextJ-8] + _ = x[Text5-9] + _ = x[TextRaw-10] + _ = x[Array-11] + _ = x[Object-12] + _ = x[Reserved13-13] + _ = x[Reserved14-14] + _ = x[Reserved15-15] +} + +const _Type_name = "NullTrueFalseIntInt5FloatFloat5TextTextJText5TextRawArrayObjectReserved13Reserved14Reserved15" + +var _Type_index = [...]uint8{0, 4, 8, 13, 16, 20, 25, 31, 35, 40, 45, 52, 57, 63, 73, 83, 93} + +func (i Type) String() string { + if i >= Type(len(_Type_index)-1) { + return "Type(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _Type_name[_Type_index[i]:_Type_index[i+1]] +}