mirror of
https://github.com/apple/foundationdb.git
synced 2025-05-14 01:42:37 +08:00
409 lines
10 KiB
Go
409 lines
10 KiB
Go
/*
|
|
* tuple.go
|
|
*
|
|
* This source file is part of the FoundationDB open source project
|
|
*
|
|
* Copyright 2013-2018 Apple Inc. and the FoundationDB project authors
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
// FoundationDB Go Tuple Layer
|
|
|
|
// Package tuple provides a layer for encoding and decoding multi-element tuples
|
|
// into keys usable by FoundationDB. The encoded key maintains the same sort
|
|
// order as the original tuple: sorted first by the first element, then by the
|
|
// second element, etc. This makes the tuple layer ideal for building a variety
|
|
// of higher-level data models.
|
|
//
|
|
// For general guidance on tuple usage, see the Tuple section of Data Modeling
|
|
// (https://apple.github.io/foundationdb/data-modeling.html#tuples).
|
|
//
|
|
// FoundationDB tuples can currently encode byte and unicode strings, integers
|
|
// and NULL values. In Go these are represented as []byte, string, int64 and
|
|
// nil.
|
|
package tuple
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"github.com/apple/foundationdb/bindings/go/src/fdb"
|
|
)
|
|
|
|
// A TupleElement is one of the types that may be encoded in FoundationDB
|
|
// tuples. Although the Go compiler cannot enforce this, it is a programming
|
|
// error to use an unsupported types as a TupleElement (and will typically
|
|
// result in a runtime panic).
|
|
//
|
|
// The valid types for TupleElement are []byte (or fdb.KeyConvertible), string,
|
|
// int64 (or int), float, double, bool, UUID, Tuple, and nil.
|
|
type TupleElement interface{}
|
|
|
|
// Tuple is a slice of objects that can be encoded as FoundationDB tuples. If
|
|
// any of the TupleElements are of unsupported types, a runtime panic will occur
|
|
// when the Tuple is packed.
|
|
//
|
|
// Given a Tuple T containing objects only of these types, then T will be
|
|
// identical to the Tuple returned by unpacking the byte slice obtained by
|
|
// packing T (modulo type normalization to []byte and int64).
|
|
type Tuple []TupleElement
|
|
|
|
// UUID wraps a basic byte array as a UUID. We do not provide any special
|
|
// methods for accessing or generating the UUID, but as Go does not provide
|
|
// a built-in UUID type, this simple wrapper allows for other libraries
|
|
// to write the output of their UUID type as a 16-byte array into
|
|
// an instance of this type.
|
|
type UUID [16]byte
|
|
|
|
// Type codes: These prefix the different elements in a packed Tuple
|
|
// to indicate what type they are.
|
|
const nilCode = 0x00
|
|
const bytesCode = 0x01
|
|
const stringCode = 0x02
|
|
const nestedCode = 0x05
|
|
const intZeroCode = 0x14
|
|
const posIntEnd = 0x1c
|
|
const negIntStart = 0x0c
|
|
const floatCode = 0x20
|
|
const doubleCode = 0x21
|
|
const falseCode = 0x26
|
|
const trueCode = 0x27
|
|
const uuidCode = 0x30
|
|
|
|
var sizeLimits = []uint64{
|
|
1<<(0*8) - 1,
|
|
1<<(1*8) - 1,
|
|
1<<(2*8) - 1,
|
|
1<<(3*8) - 1,
|
|
1<<(4*8) - 1,
|
|
1<<(5*8) - 1,
|
|
1<<(6*8) - 1,
|
|
1<<(7*8) - 1,
|
|
1<<(8*8) - 1,
|
|
}
|
|
|
|
func adjustFloatBytes(b []byte, encode bool) {
|
|
if (encode && b[0]&0x80 != 0x00) || (!encode && b[0]&0x80 == 0x00) {
|
|
// Negative numbers: flip all of the bytes.
|
|
for i := 0; i < len(b); i++ {
|
|
b[i] = b[i] ^ 0xff
|
|
}
|
|
} else {
|
|
// Positive number: flip just the sign bit.
|
|
b[0] = b[0] ^ 0x80
|
|
}
|
|
}
|
|
|
|
func encodeBytes(buf *bytes.Buffer, code byte, b []byte) {
|
|
buf.WriteByte(code)
|
|
buf.Write(bytes.Replace(b, []byte{0x00}, []byte{0x00, 0xFF}, -1))
|
|
buf.WriteByte(0x00)
|
|
}
|
|
|
|
func bisectLeft(u uint64) int {
|
|
var n int
|
|
for sizeLimits[n] < u {
|
|
n += 1
|
|
}
|
|
return n
|
|
}
|
|
|
|
func encodeInt(buf *bytes.Buffer, i int64) {
|
|
if i == 0 {
|
|
buf.WriteByte(0x14)
|
|
return
|
|
}
|
|
|
|
var n int
|
|
var ibuf bytes.Buffer
|
|
|
|
switch {
|
|
case i > 0:
|
|
n = bisectLeft(uint64(i))
|
|
buf.WriteByte(byte(intZeroCode + n))
|
|
binary.Write(&ibuf, binary.BigEndian, i)
|
|
case i < 0:
|
|
n = bisectLeft(uint64(-i))
|
|
buf.WriteByte(byte(0x14 - n))
|
|
binary.Write(&ibuf, binary.BigEndian, int64(sizeLimits[n])+i)
|
|
}
|
|
|
|
buf.Write(ibuf.Bytes()[8-n:])
|
|
}
|
|
|
|
func encodeFloat(buf *bytes.Buffer, f float32) {
|
|
var ibuf bytes.Buffer
|
|
binary.Write(&ibuf, binary.BigEndian, f)
|
|
buf.WriteByte(floatCode)
|
|
out := ibuf.Bytes()
|
|
adjustFloatBytes(out, true)
|
|
buf.Write(out)
|
|
}
|
|
|
|
func encodeDouble(buf *bytes.Buffer, d float64) {
|
|
var ibuf bytes.Buffer
|
|
binary.Write(&ibuf, binary.BigEndian, d)
|
|
buf.WriteByte(doubleCode)
|
|
out := ibuf.Bytes()
|
|
adjustFloatBytes(out, true)
|
|
buf.Write(out)
|
|
}
|
|
|
|
func encodeUUID(buf *bytes.Buffer, u UUID) {
|
|
buf.WriteByte(uuidCode)
|
|
buf.Write(u[:])
|
|
}
|
|
|
|
func encodeTuple(buf *bytes.Buffer, t Tuple, nested bool) {
|
|
if nested {
|
|
buf.WriteByte(nestedCode)
|
|
}
|
|
|
|
for i, e := range t {
|
|
switch e := e.(type) {
|
|
case Tuple:
|
|
encodeTuple(buf, e, true)
|
|
case nil:
|
|
buf.WriteByte(nilCode)
|
|
if nested {
|
|
buf.WriteByte(0xff)
|
|
}
|
|
case int64:
|
|
encodeInt(buf, e)
|
|
case int:
|
|
encodeInt(buf, int64(e))
|
|
case []byte:
|
|
encodeBytes(buf, bytesCode, e)
|
|
case fdb.KeyConvertible:
|
|
encodeBytes(buf, bytesCode, []byte(e.FDBKey()))
|
|
case string:
|
|
encodeBytes(buf, stringCode, []byte(e))
|
|
case float32:
|
|
encodeFloat(buf, e)
|
|
case float64:
|
|
encodeDouble(buf, e)
|
|
case bool:
|
|
if e {
|
|
buf.WriteByte(trueCode)
|
|
} else {
|
|
buf.WriteByte(falseCode)
|
|
}
|
|
case UUID:
|
|
encodeUUID(buf, e)
|
|
default:
|
|
panic(fmt.Sprintf("unencodable element at index %d (%v, type %T)", i, t[i], t[i]))
|
|
}
|
|
}
|
|
|
|
if nested {
|
|
buf.WriteByte(0x00)
|
|
}
|
|
}
|
|
|
|
// Pack returns a new byte slice encoding the provided tuple. Pack will panic if
|
|
// the tuple contains an element of any type other than []byte,
|
|
// fdb.KeyConvertible, string, int64, int, float32, float64, bool, tuple.UUID,
|
|
// nil, or a Tuple with elements of valid types.
|
|
//
|
|
// Tuple satisfies the fdb.KeyConvertible interface, so it is not necessary to
|
|
// call Pack when using a Tuple with a FoundationDB API function that requires a
|
|
// key.
|
|
func (t Tuple) Pack() []byte {
|
|
buf := new(bytes.Buffer)
|
|
encodeTuple(buf, t, false)
|
|
return buf.Bytes()
|
|
}
|
|
|
|
func findTerminator(b []byte) int {
|
|
bp := b
|
|
var length int
|
|
|
|
for {
|
|
idx := bytes.IndexByte(bp, 0x00)
|
|
length += idx
|
|
if idx+1 == len(bp) || bp[idx+1] != 0xFF {
|
|
break
|
|
}
|
|
length += 2
|
|
bp = bp[idx+2:]
|
|
}
|
|
|
|
return length
|
|
}
|
|
|
|
func decodeBytes(b []byte) ([]byte, int) {
|
|
idx := findTerminator(b[1:])
|
|
return bytes.Replace(b[1:idx+1], []byte{0x00, 0xFF}, []byte{0x00}, -1), idx + 2
|
|
}
|
|
|
|
func decodeString(b []byte) (string, int) {
|
|
bp, idx := decodeBytes(b)
|
|
return string(bp), idx
|
|
}
|
|
|
|
func decodeInt(b []byte) (int64, int) {
|
|
if b[0] == intZeroCode {
|
|
return 0, 1
|
|
}
|
|
|
|
var neg bool
|
|
|
|
n := int(b[0]) - intZeroCode
|
|
if n < 0 {
|
|
n = -n
|
|
neg = true
|
|
}
|
|
|
|
bp := make([]byte, 8)
|
|
copy(bp[8-n:], b[1:n+1])
|
|
|
|
var ret int64
|
|
|
|
binary.Read(bytes.NewBuffer(bp), binary.BigEndian, &ret)
|
|
|
|
if neg {
|
|
ret -= int64(sizeLimits[n])
|
|
}
|
|
|
|
return ret, n + 1
|
|
}
|
|
|
|
func decodeFloat(b []byte) (float32, int) {
|
|
bp := make([]byte, 4)
|
|
copy(bp, b[1:])
|
|
adjustFloatBytes(bp, false)
|
|
var ret float32
|
|
binary.Read(bytes.NewBuffer(bp), binary.BigEndian, &ret)
|
|
return ret, 5
|
|
}
|
|
|
|
func decodeDouble(b []byte) (float64, int) {
|
|
bp := make([]byte, 8)
|
|
copy(bp, b[1:])
|
|
adjustFloatBytes(bp, false)
|
|
var ret float64
|
|
binary.Read(bytes.NewBuffer(bp), binary.BigEndian, &ret)
|
|
return ret, 9
|
|
}
|
|
|
|
func decodeUUID(b []byte) (UUID, int) {
|
|
var u UUID
|
|
copy(u[:], b[1:])
|
|
return u, 17
|
|
}
|
|
|
|
func decodeTuple(b []byte, nested bool) (Tuple, int, error) {
|
|
var t Tuple
|
|
|
|
var i int
|
|
|
|
for i < len(b) {
|
|
var el interface{}
|
|
var off int
|
|
|
|
switch {
|
|
case b[i] == nilCode:
|
|
if !nested {
|
|
el = nil
|
|
off = 1
|
|
} else if i+1 < len(b) && b[i+1] == 0xff {
|
|
el = nil
|
|
off = 2
|
|
} else {
|
|
return t, i + 1, nil
|
|
}
|
|
case b[i] == bytesCode:
|
|
el, off = decodeBytes(b[i:])
|
|
case b[i] == stringCode:
|
|
el, off = decodeString(b[i:])
|
|
case negIntStart <= b[i] && b[i] <= posIntEnd:
|
|
el, off = decodeInt(b[i:])
|
|
case b[i] == floatCode:
|
|
if i+5 > len(b) {
|
|
return nil, i, fmt.Errorf("insufficient bytes to decode float starting at position %d of byte array for tuple", i)
|
|
}
|
|
el, off = decodeFloat(b[i:])
|
|
case b[i] == doubleCode:
|
|
if i+9 > len(b) {
|
|
return nil, i, fmt.Errorf("insufficient bytes to decode double starting at position %d of byte array for tuple", i)
|
|
}
|
|
el, off = decodeDouble(b[i:])
|
|
case b[i] == trueCode:
|
|
el = true
|
|
off = 1
|
|
case b[i] == falseCode:
|
|
el = false
|
|
off = 1
|
|
case b[i] == uuidCode:
|
|
if i+17 > len(b) {
|
|
return nil, i, fmt.Errorf("insufficient bytes to decode UUID starting at position %d of byte array for tuple", i)
|
|
}
|
|
el, off = decodeUUID(b[i:])
|
|
case b[i] == nestedCode:
|
|
var err error
|
|
el, off, err = decodeTuple(b[i+1:], true)
|
|
if err != nil {
|
|
return nil, i, err
|
|
}
|
|
off += 1
|
|
default:
|
|
return nil, i, fmt.Errorf("unable to decode tuple element with unknown typecode %02x", b[i])
|
|
}
|
|
|
|
t = append(t, el)
|
|
i += off
|
|
}
|
|
|
|
return t, i, nil
|
|
}
|
|
|
|
// Unpack returns the tuple encoded by the provided byte slice, or an error if
|
|
// the key does not correctly encode a FoundationDB tuple.
|
|
func Unpack(b []byte) (Tuple, error) {
|
|
t, _, err := decodeTuple(b, false)
|
|
return t, err
|
|
}
|
|
|
|
// FDBKey returns the packed representation of a Tuple, and allows Tuple to
|
|
// satisfy the fdb.KeyConvertible interface. FDBKey will panic in the same
|
|
// circumstances as Pack.
|
|
func (t Tuple) FDBKey() fdb.Key {
|
|
return t.Pack()
|
|
}
|
|
|
|
// FDBRangeKeys allows Tuple to satisfy the fdb.ExactRange interface. The range
|
|
// represents all keys that encode tuples strictly starting with a Tuple (that
|
|
// is, all tuples of greater length than the Tuple of which the Tuple is a
|
|
// prefix).
|
|
func (t Tuple) FDBRangeKeys() (fdb.KeyConvertible, fdb.KeyConvertible) {
|
|
p := t.Pack()
|
|
return fdb.Key(concat(p, 0x00)), fdb.Key(concat(p, 0xFF))
|
|
}
|
|
|
|
// FDBRangeKeySelectors allows Tuple to satisfy the fdb.Range interface. The
|
|
// range represents all keys that encode tuples strictly starting with a Tuple
|
|
// (that is, all tuples of greater length than the Tuple of which the Tuple is a
|
|
// prefix).
|
|
func (t Tuple) FDBRangeKeySelectors() (fdb.Selectable, fdb.Selectable) {
|
|
b, e := t.FDBRangeKeys()
|
|
return fdb.FirstGreaterOrEqual(b), fdb.FirstGreaterOrEqual(e)
|
|
}
|
|
|
|
func concat(a []byte, b ...byte) []byte {
|
|
r := make([]byte, len(a)+len(b))
|
|
copy(r, a)
|
|
copy(r[len(a):], b)
|
|
return r
|
|
}
|