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
- func OnPath(only, path []uint16) bool
- type Change
- 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) UnionHint(slot uint16) (UnionShape, bool)
- func (t Table) UnionType(typeSlot uint16) byte
- func (t Table) UnionVectorLen(typeSlot uint16) int
- func (t Table) VectorLen(slot uint16) int
- type UnionShape
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.
func OnPath ¶ added in v0.6.0
OnPath reports whether the slot at path is on the way to only, or inside it. An empty only matches everything.
This is the rule DumpOptions.Only follows: above the target, only the one branch leading to it; at or below it, everything, because Only picks a starting point and not a budget.
It is exported because a caller writing its own walk, as cmd/flatdump does for its JSON output, needs the same answer. A second copy of a rule like this is how two views of one buffer quietly start disagreeing.
Types ¶
type Change ¶ added in v0.4.0
type Change struct {
// Path is the slot path from the root, using dots for nested tables and
// brackets for elements of a table vector: "4", "10.4", "12[0].4".
Path string
// A and B are the rendered values on each side. An empty string means the
// slot is not present on that side, which [Change.String] shows as absent.
A, B string
}
Change is one difference between two buffers.
func Diff ¶ added in v0.4.0
Diff reports the slots that differ between two tables, walking nested tables and table vectors.
This is the workflow the tool exists for: capture the same request twice, or the same event before and after something changed, and ask which slots moved. It is far faster than reading two dumps side by side, and it does not need a schema, because a slot path is a stable handle whether or not you know what the slot means.
Values are compared as rendered text rather than as bytes. That is deliberate: two slots holding the same number are equal even if one buffer padded differently, and a change of kind (a string becoming a table) shows up as a change rather than as an error.
A nil result means the two tables agree everywhere Diff looked. Depth is capped, so it means "no difference within reach", not "the buffers are identical".
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
// StructSize, when set, is asked how wide the elements of a struct vector
// are. Return 0 for any slot that does not hold one.
//
// It has to be asked because nothing in the buffer answers it. A vector of
// structs and a vector of bytes are stored identically, a count followed by
// inline data with no vtable and no offsets, so [Table.Guess] reports
// [KindBytes] for both. Without this a vector of three 8-byte structs dumps
// as "bytes 3" and prints three bytes of the first one, which looks like a
// tiny byte vector rather than 24 bytes of records.
//
// The path is the slot numbers from the root down to and including the slot
// in question: root slot 6 is {6}, and slot 6 of the table in root slot 4
// is {4, 6}. Indices into a table vector are not part of it, because every
// element of a vector shares one schema, so a path names a position in the
// schema rather than in the buffer.
StructSize func(path []uint16) int
// Only, when set, restricts the dump to one slot and everything under it.
// It is a slot path from the root, spelled the same way as the one
// StructSize is asked about.
//
// This is where to start, not how much to print. MaxDepth and
// MaxVectorElems still apply below it, and MaxDepth is counted from the
// root rather than from here, so reaching deep into a buffer wants both.
//
// A path that is not in the buffer says so rather than printing nothing.
// An empty dump is indistinguishable from a buffer with nothing in it, and
// telling those two apart is most of what this package is for.
Only []uint16
}
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. // // A nested table and a short byte vector look alike, because a table begins // with the offset back to its vtable and a vector begins with its length. // Both readings usually fit, so the structure of the target decides. // Measured against buffers built from a known schema, that recovers every // nested table and calls about 2% of genuine byte vectors tables, the // misses being short vectors whose bytes happen to read as a small 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) UnionHint ¶ added in v0.4.0
func (t Table) UnionHint(slot uint16) (UnionShape, bool)
UnionHint reports whether a slot looks like the tag half of a union.
Nothing in a buffer says "these two slots are a union". This checks the shape: a small tag next to a value that resolves, or a vector of tags the same length as a vector of values. Like Table.Guess it is a starting point for reading an unknown payload, not a fact to decode on, and the tools that use it say "maybe" rather than naming it.
Use Table.Union and Table.UnionAt once you know from the schema which slots really are unions.
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.
type UnionShape ¶ added in v0.4.0
type UnionShape struct {
TagSlot uint16
ValueSlot uint16
// Elements is 0 for a single union, or the element count for a vector of
// unions.
Elements int
}
UnionShape describes a pair of slots that look like the two halves of a union: the tag and the value it selects.