Documentation
¶
Overview ¶
Package flatread reads FlatBuffers buffers for which you have no schema.
The normal FlatBuffers workflow compiles a .fbs schema into generated accessors, and field names live in that generated code. When you are handed a binary payload without its schema (reverse engineering a protocol, inspecting a capture, triaging a corrupt file) there is nothing to generate from, and the standard library is of no help.
This package reads a buffer positionally instead: fields are addressed by their vtable slot number rather than by name. That is enough to walk an unknown buffer, and enough to write a decoder once you have worked out which slot means what.
Buffer layout ¶
Everything is little-endian. Reading the code below is much easier with this to hand:
buf[0:4] uoffset to the root table
table[-soffset] the table's vtable (soffset is a SIGNED int32
stored at the table's own position)
vtable[0:2] vtable size in bytes
vtable[2:4] inline table size in bytes
vtable[4+2*i : 6+2*i] byte offset of field i inside the table,
0 meaning the field is absent
So field i lives at slot number 4+2*i: the first field is slot 4, the second slot 6, and so on. Slot numbers, not indices, are what you pass here.
Offsets to strings, vectors and nested tables are uoffsets: unsigned, and relative to the position of the offset itself.
Safety ¶
Every accessor is bounds-checked and returns a zero value rather than panicking. That is deliberate: this package exists to be pointed at bytes nobody has validated, so malformed input is the expected case and not an exceptional one. The fuzz test in this package asserts it.
The cost of that choice is that a zero value is ambiguous. It means either "the field holds zero" or "there is no such field". Use Table.Has when the difference matters.
Index ¶
- Constants
- func Dump(buf []byte) string
- func DumpWith(buf []byte, opts DumpOptions) string
- func FileIdentifier(buf []byte) (string, bool)
- func IsSizePrefixed(buf []byte) bool
- func LooksRoaring(b []byte) bool
- type DumpOptions
- type Kind
- type Table
- func (t Table) Bool(slot uint16) bool
- func (t Table) Buffer() []byte
- func (t Table) Byte(slot uint16) uint8
- func (t Table) Bytes(slot uint16) []byte
- func (t Table) BytesAt(slot uint16, i int) []byte
- func (t Table) Float32(slot uint16) float32
- func (t Table) Float32Vector(slot uint16) []float32
- func (t Table) Float64(slot uint16) float64
- func (t Table) Float64Vector(slot uint16) []float64
- func (t Table) Guess(slot uint16) Kind
- func (t Table) Has(slot uint16) bool
- func (t Table) Int8(slot uint16) int8
- func (t Table) Int16(slot uint16) int16
- func (t Table) Int32(slot uint16) int32
- func (t Table) Int32Vector(slot uint16) []int32
- func (t Table) Int64(slot uint16) int64
- func (t Table) Int64Vector(slot uint16) []int64
- func (t Table) Pos() uint32
- func (t Table) Slots() []uint16
- func (t Table) String(slot uint16) string
- func (t Table) StringAt(slot uint16, i int) string
- func (t Table) StringVector(slot uint16) []string
- func (t Table) StructAt(slot uint16, i, size int) []byte
- func (t Table) StructVectorLen(slot uint16, size int) int
- func (t Table) Table(slot uint16) (Table, bool)
- func (t Table) TableAt(slot uint16, i int) (Table, bool)
- func (t Table) Uint16(slot uint16) uint16
- func (t Table) Uint32(slot uint16) uint32
- func (t Table) Uint32Vector(slot uint16) []uint32
- func (t Table) Uint64(slot uint16) uint64
- func (t Table) Union(typeSlot uint16) (byte, Table, bool)
- func (t Table) UnionAt(typeSlot uint16, i int) (byte, Table, bool)
- func (t Table) UnionType(typeSlot uint16) byte
- func (t Table) UnionVectorLen(typeSlot uint16) int
- func (t Table) VectorLen(slot uint16) int
Examples ¶
Constants ¶
const UnionNone byte = 0
UnionNone is the type tag a union carries when it holds nothing. FlatBuffers reserves 0 for it in every union enum.
Variables ¶
This section is empty.
Functions ¶
func Dump ¶
Dump walks a buffer from its root and describes every populated field, guessing each one's type from its bytes.
This is the tool you reach for first when handed an unknown payload: it turns an opaque blob into a list of slot numbers with plausible types, which is what you need in order to start matching them against whatever documentation, generated code or client bundle you do have.
A malformed buffer produces a description of the error rather than a panic.
Example ¶
Dump is the first thing to run against an unfamiliar payload: it turns an opaque blob into a list of slot numbers with plausible types.
Note slot 8, which really is a vector of uint32 {1, 2, 3} and is reported as three bytes. Nothing in a FlatBuffers buffer records a vector's element width, so that is the honest answer rather than a bug.
fmt.Print(Dump(sample()))
Output: root @0x40 (168 bytes) slot 4 scalar u32=4242 u16=4242 u8=146 slot 6 string "hello" slot 8 bytes 3 slot 10 table slot 4 scalar u32=7 u16=7 u8=7 slot 12 vector 2 tables [0] slot 4 scalar u32=11 u16=11 u8=11 [1] slot 4 scalar u32=22 u16=22 u8=22
func DumpWith ¶
func DumpWith(buf []byte, opts DumpOptions) string
DumpWith is Dump with control over depth, breadth and annotation.
func FileIdentifier ¶
FileIdentifier reads the optional four-byte identifier a schema can attach to its buffers, which sits directly after the root offset.
The bool reports whether those four bytes look like an identifier at all, meaning printable ASCII with no spaces. Nothing in the buffer records whether the field is present: a schema without a file_identifier puts padding or the start of a vtable in the same place. So a false here means "these bytes are not a plausible identifier", not "this schema has none".
When it is present it is usually the fastest way to work out what you have been handed, because it is chosen to be human-readable and often appears in the schema, the docs or the code that produced the buffer.
func IsSizePrefixed ¶
IsSizePrefixed guesses whether buf carries a size prefix, by checking whether its first uint32 accounts for the rest of the buffer exactly.
Like Table.Guess this reads structure rather than a recorded fact, and it can be fooled: a plain buffer whose root offset happens to equal len(buf)-4 looks identical. It is a starting point for an unknown payload, not something to branch on in production once you know the format.
func LooksRoaring ¶
LooksRoaring reports whether b starts with a RoaringBitmap serial cookie: 12346 for a plain bitmap, or 12347 in the low 16 bits when run containers are present.
This only inspects the header. To actually decode one, hand the bytes to a Roaring library. This package deliberately has no dependencies.
Types ¶
type DumpOptions ¶
type DumpOptions struct {
// MaxDepth limits how far nested tables are followed. 0 means the default
// of 5, which is deep enough to see the shape of most payloads without
// printing a megabyte.
MaxDepth int
// MaxVectorElems limits how many elements of a table vector are expanded.
// 0 means the default of 3. The remainder is summarised as "... N more".
MaxVectorElems int
// Annotate, when set, is called for every byte vector and its result is
// appended to that line. This is the hook for anything that needs a
// dependency: counting bits in a RoaringBitmap, sniffing a compression
// header, decoding a nested buffer. Return "" to add nothing.
Annotate func(b []byte) string
}
DumpOptions controls DumpWith. The zero value is what Dump uses.
type Kind ¶
type Kind int
Kind is a guess at what a field holds.
FlatBuffers does not record field types in the buffer. The schema carries them, and a schema is exactly what you do not have here. So Table.Guess leans entirely on structure: a length prefix that fits, bytes that are all printable, a known magic number, an offset that resolves to a readable vtable.
It is right often enough to be the fastest way into an unknown buffer, and it is wrong often enough that you should never build a decoder on it. Use it to find the slots you care about, confirm each one against real data, then read them with the typed accessors.
const ( // KindScalar is the fallback: an inline number, or a field whose offset // resolves to nothing recognisable. Note that KindScalar is what an unknown // field degrades to, so it means "no better guess" rather than "definitely // a number". KindScalar Kind = iota // KindString is a length-prefixed run of printable ASCII. KindString // KindBytes is a length-prefixed run that is not printable and not // recognised. A vector of any fixed-width numeric type also lands here, and // so does a vector of structs: nothing in the buffer distinguishes a byte // vector from an int32 vector or from packed fixed-size records. Read those // with [Table.StructAt] once the schema tells you the element size. KindBytes // KindRoaring is a byte vector carrying a RoaringBitmap serial cookie. // Roaring is common in search and analytics payloads, compact enough to be // worth recognising, and its cookie is distinctive. KindRoaring // KindTable is an offset resolving to a plausible vtable. KindTable // KindVectorOfTables is a vector whose elements all resolve to plausible // vtables. KindVectorOfTables )
type Table ¶
type Table struct {
// contains filtered or unexported fields
}
Table is a FlatBuffers table at a position inside a buffer. The zero Table is not usable; get one from Root.
A Table is a view, not a copy: it holds a reference to the underlying buffer and no accessor allocates unless it must (Table.String and the vector accessors do; the scalar ones do not).
Example ¶
Once Dump has told you which slots exist, read them with the typed accessors. Slot numbers are the schema you did not get.
root, err := Root(sample())
if err != nil {
panic(err)
}
fmt.Println("slots: ", root.Slots())
fmt.Println("string:", root.String(6))
fmt.Println("vector:", root.Int32Vector(8))
if nested, ok := root.Table(10); ok {
fmt.Println("nested:", nested.Uint32(4))
}
for i := 0; i < root.VectorLen(12); i++ {
if sub, ok := root.TableAt(12, i); ok {
fmt.Printf("elem %d: %d\n", i, sub.Uint32(4))
}
}
Output: slots: [4 6 8 10 12] string: hello vector: [1 2 3] nested: 7 elem 0: 11 elem 1: 22
func Root ¶
Root returns the root table of a FlatBuffers buffer.
It fails only when the buffer is too small to hold a root offset, or when that offset points outside the buffer. It does NOT validate the rest of the buffer. That is what the accessors' bounds checks are for.
func RootSizePrefixed ¶
RootSizePrefixed returns the root table of a size-prefixed buffer.
The size-prefixed layout puts a uint32 byte count in front of an otherwise ordinary buffer, so that a reader taking bytes off a stream knows where one message ends. gRPC uses it, and so does anything that stores several buffers back to back in one file.
Passing such a buffer to Root does not fail loudly. Root reads the size as the root offset, and what happens next depends on the number: usually an error, sometimes a table full of nonsense. If you do not know which layout you have, IsSizePrefixed guesses.
Trailing bytes past the declared size are ignored, so this can be called on a stream buffer that holds more than one message.
func TableAtOffset ¶
TableAtOffset returns the table at an absolute byte position in buf.
Use it when you already know where a table starts, for instance when a size-prefixed buffer puts the root somewhere other than offset 0, or when you are re-entering a buffer at a position Table.Pos gave you earlier.
func (Table) Bytes ¶
Bytes reads a vector of bytes, returned as a sub-slice of the buffer without copying. Do not retain it past the lifetime of the buffer, and do not write to it.
func (Table) BytesAt ¶
BytesAt reads element i of a vector of strings or byte vectors, without copying.
func (Table) Float32Vector ¶ added in v0.3.0
Float32Vector reads a vector of float32.
func (Table) Float64Vector ¶ added in v0.3.0
Float64Vector reads a vector of float64.
func (Table) Guess ¶
Guess reports what a field most likely holds. See Kind for how much to trust it.
func (Table) Has ¶
Has reports whether the field is present, as distinct from present and zero.
FlatBuffers omits fields equal to their default, so a writer that stored 0 may well have written nothing at all. Has cannot recover that distinction. It reports what is in the buffer, which is the most anyone can know.
func (Table) Int32Vector ¶
Int32Vector reads a vector of int32.
func (Table) Int64Vector ¶
Int64Vector reads a vector of int64.
func (Table) Slots ¶
Slots lists the vtable slots this table actually populates, ascending.
This is the entry point for exploring an unknown buffer: it tells you which fields are present without knowing what any of them mean.
func (Table) String ¶
String reads a string field. FlatBuffers strings are length-prefixed and NUL-terminated; the terminator is not included.
The bytes are not validated as UTF-8, because a schema-less buffer is exactly where a "string" often turns out to be something else.
func (Table) StringVector ¶
StringVector reads a vector of strings.
An element that is out of range comes back as "", the same as Table.StringAt would give, so the length of the result always matches the vector's declared length.
func (Table) StructAt ¶ added in v0.2.0
StructAt returns the bytes of element i in a vector of structs, without copying.
FlatBuffers structs are not tables. They are fixed-size records stored inline in the vector, one after another, with no vtable and no offsets. That makes them unreadable through Table.TableAt, which treats every element as an offset: on a struct vector it adds a number that was never an offset to the element position and hands back a Table pointing at whatever sits there.
The element size comes from the schema. Nothing in the buffer records it, which is also why Table.Guess cannot tell a struct vector from a byte vector and reports KindBytes for both.
Fields inside the returned bytes are at fixed offsets, again from the schema, and are read with encoding/binary directly.
func (Table) StructVectorLen ¶ added in v0.2.0
StructVectorLen reports how many structs of the given size a vector holds, or 0 when the declared count does not fit the buffer.
This differs from Table.VectorLen, which trusts the count without knowing the element width. A truncated buffer shows up here and not there.
func (Table) Table ¶
Table reads a nested table field. The bool reports whether it was present and in range.
func (Table) Uint32Vector ¶
Uint32Vector reads a vector of uint32.
func (Table) Union ¶ added in v0.3.0
Union reads a union: its type tag and the table it points at. The bool is false when the tag is UnionNone or the value is missing.
Union members are usually tables, which is what this returns. A schema can also put a string or a struct in a union; for those, read typeSlot+2 directly with Table.String or Table.StructAt, since the value slot is an ordinary slot.
func (Table) UnionAt ¶ added in v0.3.0
UnionAt reads element i of a vector of unions.
An element whose tag is UnionNone reports false. That is not an error and not the end of the vector: a union vector can hold empty slots between full ones, so keep walking to Table.UnionVectorLen.
func (Table) UnionType ¶ added in v0.3.0
UnionType reads the type tag of a union without resolving its value.
A union in a schema becomes two fields in the table, in consecutive slots: the tag at typeSlot, saying which member is present, and the value at typeSlot+2. Without the schema the tag is just a number, but it is the number the schema's enum is keyed on, so it is what you match against once you know what the members are.
func (Table) UnionVectorLen ¶ added in v0.3.0
UnionVectorLen reports how many elements a vector of unions holds.
A union vector is two parallel vectors: the tags, as a vector of bytes at typeSlot, and the values at typeSlot+2. An encoder keeps them the same length. This reports the shorter of the two, so any index it accepts is in range for both, which matters on a truncated buffer where they are not.