blob: 47b20c3ced92c211a7f0170503687288ddecf825 [file]
/*
* Copyright (C) 2026 The Android Open Source Project
*
* 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.
*/
#include "src/trace_processor/util/flatbuffer_reader.h"
#include <cstdint>
#include <cstring>
#include <optional>
#include <string_view>
#include <vector>
#include "src/trace_processor/util/flatbuffer_writer.h"
#include "test/gtest_and_gmock.h"
namespace perfetto::trace_processor::util {
namespace {
using W = FlatBufferWriter;
std::vector<uint8_t> Build(W::Offset root, FlatBufferWriter& w) {
w.Finish(root);
return w.Release();
}
std::optional<FlatBufferReader> GetRoot(const std::vector<uint8_t>& buf) {
return FlatBufferReader::GetRoot(buf.data(),
static_cast<uint32_t>(buf.size()));
}
TEST(FlatBufferRoundTripTest, ScalarFields) {
FlatBufferWriter w;
w.StartTable();
w.FieldI32(0, 42);
w.FieldI16(1, 7);
w.FieldBool(2, true);
w.FieldU8(3, 200);
w.FieldI64(4, 0x123456789ABCDEF0LL);
auto root = w.EndTable();
auto buf = Build(root, w);
auto reader = GetRoot(buf);
ASSERT_TRUE(reader.has_value());
EXPECT_EQ(reader->Scalar<int32_t>(0), 42);
EXPECT_EQ(reader->Scalar<int16_t>(1), 7);
EXPECT_EQ(reader->Scalar<uint8_t>(2), 1); // bool stored as u8.
EXPECT_EQ(reader->Scalar<uint8_t>(3), 200);
EXPECT_EQ(reader->Scalar<int64_t>(4), 0x123456789ABCDEF0LL);
}
// Regression test: EndTable() must survive a buffer grow between prepending
// the soffset placeholder and patching it (the vtable prepend in between can
// reallocate and shift the buffer contents).
TEST(FlatBufferRoundTripTest, GrowDuringEndTable) {
for (uint32_t capacity = 4; capacity <= 64; capacity += 4) {
FlatBufferWriter w(capacity);
w.StartTable();
for (uint32_t i = 0; i < 24; i++) {
w.FieldI32(i, static_cast<int32_t>(i * 3));
}
auto root = w.EndTable();
auto buf = Build(root, w);
auto reader = GetRoot(buf);
ASSERT_TRUE(reader.has_value()) << "capacity " << capacity;
for (uint32_t i = 0; i < 24; i++) {
EXPECT_EQ(reader->Scalar<int32_t>(i), static_cast<int32_t>(i * 3))
<< "capacity " << capacity << " field " << i;
}
}
}
TEST(FlatBufferRoundTripTest, AbsentFieldReturnsDefault) {
FlatBufferWriter w;
w.StartTable();
w.FieldI32(1, 55);
auto root = w.EndTable();
auto buf = Build(root, w);
auto reader = GetRoot(buf);
ASSERT_TRUE(reader.has_value());
EXPECT_EQ(reader->Scalar<int32_t>(0, -1), -1);
EXPECT_EQ(reader->Scalar<int32_t>(1), 55);
// Field index well beyond the vtable's extent is also absent.
EXPECT_EQ(reader->Scalar<int32_t>(50, -7), -7);
}
TEST(FlatBufferRoundTripTest, StringField) {
FlatBufferWriter w;
auto hello = w.WriteString("hello");
w.StartTable();
w.FieldOffset(0, hello);
auto root = w.EndTable();
auto buf = Build(root, w);
auto reader = GetRoot(buf);
ASSERT_TRUE(reader.has_value());
EXPECT_EQ(reader->String(0), "hello");
}
TEST(FlatBufferRoundTripTest, SubTable) {
FlatBufferWriter w;
w.StartTable();
w.FieldI32(0, 99);
auto child = w.EndTable();
w.StartTable();
w.FieldOffset(0, child);
auto root = w.EndTable();
auto buf = Build(root, w);
auto reader = GetRoot(buf);
ASSERT_TRUE(reader.has_value());
auto child_reader = reader->Table(0);
ASSERT_TRUE(child_reader);
EXPECT_EQ(child_reader.Scalar<int32_t>(0), 99);
// Absent sub-table is an invalid reader.
EXPECT_FALSE(reader->Table(5));
}
TEST(FlatBufferRoundTripTest, VecTable) {
FlatBufferWriter w;
w.StartTable();
w.FieldI32(0, 10);
auto c0 = w.EndTable();
w.StartTable();
w.FieldI32(0, 20);
auto c1 = w.EndTable();
W::Offset offs[] = {c0, c1};
auto vec = w.WriteVecOffsets(offs, 2);
w.StartTable();
w.FieldOffset(0, vec);
auto root = w.EndTable();
auto buf = Build(root, w);
auto reader = GetRoot(buf);
ASSERT_TRUE(reader.has_value());
auto tv = reader->VecTable(0);
ASSERT_EQ(tv.size(), 2u);
EXPECT_EQ(tv[0].Scalar<int32_t>(0), 10);
EXPECT_EQ(tv[1].Scalar<int32_t>(0), 20);
}
TEST(FlatBufferRoundTripTest, VecString) {
FlatBufferWriter w;
auto s0 = w.WriteString("alpha");
auto s1 = w.WriteString("beta");
W::Offset offs[] = {s0, s1};
auto vec = w.WriteVecOffsets(offs, 2);
w.StartTable();
w.FieldOffset(0, vec);
auto root = w.EndTable();
auto buf = Build(root, w);
auto reader = GetRoot(buf);
ASSERT_TRUE(reader.has_value());
auto sv = reader->VecString(0);
ASSERT_EQ(sv.size(), 2u);
EXPECT_EQ(sv[0], "alpha");
EXPECT_EQ(sv[1], "beta");
}
TEST(FlatBufferRoundTripTest, VecScalar) {
int32_t vals[] = {10, 20, 30};
FlatBufferWriter w;
auto vec_off = w.WriteVecStruct(vals, sizeof(int32_t), 3, alignof(int32_t));
w.StartTable();
w.FieldOffset(0, vec_off);
auto root = w.EndTable();
auto buf = Build(root, w);
auto reader = GetRoot(buf);
ASSERT_TRUE(reader.has_value());
auto sv = reader->VecScalar<int32_t>(0);
ASSERT_EQ(sv.size(), 3u);
EXPECT_EQ(sv[0], 10);
EXPECT_EQ(sv[1], 20);
EXPECT_EQ(sv[2], 30);
}
TEST(FlatBufferRoundTripTest, EmptyVec) {
FlatBufferWriter w;
auto vec = w.WriteVecOffsets(nullptr, 0);
w.StartTable();
w.FieldOffset(0, vec);
auto root = w.EndTable();
auto buf = Build(root, w);
auto reader = GetRoot(buf);
ASSERT_TRUE(reader.has_value());
EXPECT_EQ(reader->VecTable(0).size(), 0u);
EXPECT_EQ(reader->VecString(0).size(), 0u);
EXPECT_EQ(reader->VecScalar<int32_t>(0).size(), 0u);
}
TEST(FlatBufferRoundTripTest, EmptyVecStruct) {
// A zero-element struct vector is the first thing written, so the writer's
// buffer is still empty when the (empty) element payload is prepended.
FlatBufferWriter w;
auto vec = w.WriteVecStruct(nullptr, sizeof(int32_t), 0, alignof(int32_t));
w.StartTable();
w.FieldOffset(0, vec);
auto root = w.EndTable();
auto buf = Build(root, w);
auto reader = GetRoot(buf);
ASSERT_TRUE(reader.has_value());
EXPECT_EQ(reader->VecScalar<int32_t>(0).size(), 0u);
}
TEST(FlatBufferRoundTripTest, VecIndexOutOfBoundsReturnsDefault) {
// Indexing any vector type at or past size() must return a default value,
// never read out of bounds. The last valid index still reads correctly.
int64_t i64s[] = {10, 20};
double f64s[] = {0.5, 1.5};
uint8_t u8s[] = {7};
FlatBufferWriter w;
auto i64_off = w.WriteVecStruct(i64s, sizeof(int64_t), 2, alignof(int64_t));
auto f64_off = w.WriteVecStruct(f64s, sizeof(double), 2, alignof(double));
auto u8_off = w.WriteVecStruct(u8s, sizeof(uint8_t), 1, alignof(uint8_t));
auto s0 = w.WriteString("only");
auto str_vec = w.WriteVecOffsets(&s0, 1);
w.StartTable();
w.FieldI32(0, 42);
auto c0 = w.EndTable();
auto tbl_vec = w.WriteVecOffsets(&c0, 1);
w.StartTable();
w.FieldOffset(0, i64_off);
w.FieldOffset(1, f64_off);
w.FieldOffset(2, u8_off);
w.FieldOffset(3, str_vec);
w.FieldOffset(4, tbl_vec);
auto root = w.EndTable();
auto buf = Build(root, w);
auto reader = GetRoot(buf);
ASSERT_TRUE(reader.has_value());
auto i64v = reader->VecScalar<int64_t>(0);
ASSERT_EQ(i64v.size(), 2u);
EXPECT_EQ(i64v[1], 20);
EXPECT_EQ(i64v[2], 0);
EXPECT_EQ(i64v[0xFFFFFFFF], 0);
auto f64v = reader->VecScalar<double>(1);
ASSERT_EQ(f64v.size(), 2u);
EXPECT_DOUBLE_EQ(f64v[1], 1.5);
EXPECT_DOUBLE_EQ(f64v[2], 0.0);
auto u8v = reader->VecScalar<uint8_t>(2);
ASSERT_EQ(u8v.size(), 1u);
EXPECT_EQ(u8v[0], 7);
EXPECT_EQ(u8v[1], 0);
auto strv = reader->VecString(3);
ASSERT_EQ(strv.size(), 1u);
EXPECT_EQ(strv[0], "only");
EXPECT_EQ(strv[1], "");
EXPECT_EQ(strv[0xFFFFFFFF], "");
auto tv = reader->VecTable(4);
ASSERT_EQ(tv.size(), 1u);
EXPECT_EQ(tv[0].Scalar<int32_t>(0), 42);
EXPECT_FALSE(tv[1]);
EXPECT_FALSE(tv[0xFFFFFFFF]);
}
TEST(FlatBufferRoundTripTest, GetRootTooSmall) {
uint8_t tiny[] = {0, 0};
EXPECT_FALSE(FlatBufferReader::GetRoot(tiny, sizeof(tiny)).has_value());
EXPECT_FALSE(FlatBufferReader::GetRoot(nullptr, 0).has_value());
}
// Simulate a minimal Arrow Schema: Schema { endianness: i16, fields: [Field] }
// where Field { name: string, nullable: bool, type_type: u8 }.
TEST(FlatBufferRoundTripTest, ArrowSchemaLike) {
FlatBufferWriter w;
auto name0 = w.WriteString("col_a");
auto name1 = w.WriteString("col_b");
w.StartTable();
w.FieldOffset(0, name0);
w.FieldBool(1, false);
w.FieldU8(2, 2); // kTypeInt
auto field0 = w.EndTable();
w.StartTable();
w.FieldOffset(0, name1);
w.FieldBool(1, true);
w.FieldU8(2, 5); // kTypeUtf8
auto field1 = w.EndTable();
W::Offset field_offs[] = {field0, field1};
auto fields_vec = w.WriteVecOffsets(field_offs, 2);
w.StartTable();
w.FieldI16(0, 0); // little-endian
w.FieldOffset(1, fields_vec);
auto schema = w.EndTable();
auto buf = Build(schema, w);
auto reader = GetRoot(buf);
ASSERT_TRUE(reader.has_value());
EXPECT_EQ(reader->Scalar<int16_t>(0), 0);
auto fields = reader->VecTable(1);
ASSERT_EQ(fields.size(), 2u);
EXPECT_EQ(fields[0].String(0), "col_a");
EXPECT_EQ(fields[0].Scalar<uint8_t>(1), 0);
EXPECT_EQ(fields[0].Scalar<uint8_t>(2), 2);
EXPECT_EQ(fields[1].String(0), "col_b");
EXPECT_EQ(fields[1].Scalar<uint8_t>(1), 1);
EXPECT_EQ(fields[1].Scalar<uint8_t>(2), 5);
}
// ---------------------------------------------------------------------------
// Malformed / untrusted input tests.
// ---------------------------------------------------------------------------
TEST(FlatBufferMalformedTest, TruncatedBufferNeverCrashes) {
FlatBufferWriter w;
auto name0 = w.WriteString("col_a");
auto name1 = w.WriteString("col_b");
w.StartTable();
w.FieldOffset(0, name0);
w.FieldBool(1, false);
w.FieldU8(2, 2);
auto field0 = w.EndTable();
w.StartTable();
w.FieldOffset(0, name1);
w.FieldBool(1, true);
w.FieldU8(2, 5);
auto field1 = w.EndTable();
W::Offset field_offs[] = {field0, field1};
auto fields_vec = w.WriteVecOffsets(field_offs, 2);
w.StartTable();
w.FieldI16(0, 0);
w.FieldOffset(1, fields_vec);
auto schema = w.EndTable();
auto full = Build(schema, w);
// Truncate at every possible length and verify accessors return defaults
// rather than reading out of bounds (ASan/MSan builds will catch OOB
// accesses; here we also sanity check the returned values are inert).
for (uint32_t len = 0; len < full.size(); len++) {
auto reader = FlatBufferReader::GetRoot(full.data(), len);
if (!reader.has_value()) {
continue;
}
EXPECT_EQ(reader->Scalar<int32_t>(99, -1), -1);
auto fields = reader->VecTable(1);
for (uint32_t i = 0; i < fields.size(); i++) {
auto f = fields[i];
if (f) {
f.String(0);
f.Scalar<uint8_t>(1);
}
}
reader->String(5);
reader->Table(5);
reader->VecString(1);
reader->VecScalar<int32_t>(1);
}
}
TEST(FlatBufferMalformedTest, VecScalarHugeCountIsRejected) {
// Hand-craft a buffer: root offset -> table with one vtable field pointing
// at a "vector" whose declared count is enormous but the buffer itself is
// tiny. VecScalar must return an empty view, not read out of bounds.
//
// Layout (all offsets relative, little endian):
// [0..4) root offset -> table at offset 4
// [4..8) table soffset -> vtable at offset 8 (back from 4: 4-8=-4)
// [8..12) vtable: size=6, table_size=6
// [12..14) vtable slot for field 0: offset 6 (points to table+6=10)
// table field data at offset 10: u32 offset to the "vector"
// vector location: count = 0xFFFFFFFF, no actual elements follow.
std::vector<uint8_t> buf(24, 0);
auto put_u32 = [&](uint32_t pos, uint32_t v) {
memcpy(buf.data() + pos, &v, 4);
};
auto put_u16 = [&](uint32_t pos, uint16_t v) {
memcpy(buf.data() + pos, &v, 2);
};
auto put_i32 = [&](uint32_t pos, int32_t v) {
memcpy(buf.data() + pos, &v, 4);
};
put_u32(0, 4); // root offset: table at 4.
put_i32(4, -4); // table soffset: vtable at 4-(-4)=8.
put_u16(8, 6); // vtable_size.
put_u16(10, 6); // table_size.
put_u16(12, 6); // slot 0 -> table + 6 = offset 10.
put_u32(10, 20 - 10); // field: relative offset to "vector" at 20.
put_u32(20, 0xFFFFFFFF); // vector count: enormous, buffer ends at 24.
auto reader =
FlatBufferReader::GetRoot(buf.data(), static_cast<uint32_t>(buf.size()));
ASSERT_TRUE(reader.has_value());
auto sv = reader->VecScalar<int32_t>(0);
EXPECT_EQ(sv.size(), 0u);
auto tv = reader->VecTable(0);
EXPECT_EQ(tv.size(), 0u);
auto strv = reader->VecString(0);
EXPECT_EQ(strv.size(), 0u);
}
TEST(FlatBufferMalformedTest, OffsetPastEndIsRejected) {
// Same skeleton as above, but the field holds an offset that resolves
// past the end of the buffer entirely.
std::vector<uint8_t> buf(16, 0);
auto put_u32 = [&](uint32_t pos, uint32_t v) {
memcpy(buf.data() + pos, &v, 4);
};
auto put_u16 = [&](uint32_t pos, uint16_t v) {
memcpy(buf.data() + pos, &v, 2);
};
auto put_i32 = [&](uint32_t pos, int32_t v) {
memcpy(buf.data() + pos, &v, 4);
};
put_u32(0, 4);
put_i32(4, -4);
put_u16(8, 6);
put_u16(10, 6);
put_u16(12, 6);
// Field holds a relative offset that points miles past the 16-byte buffer.
put_u32(10, 0x7FFFFFFF);
auto reader =
FlatBufferReader::GetRoot(buf.data(), static_cast<uint32_t>(buf.size()));
ASSERT_TRUE(reader.has_value());
EXPECT_EQ(reader->String(0), "");
EXPECT_FALSE(reader->Table(0));
EXPECT_EQ(reader->VecScalar<int32_t>(0).size(), 0u);
EXPECT_EQ(reader->VecTable(0).size(), 0u);
EXPECT_EQ(reader->VecString(0).size(), 0u);
}
} // namespace
} // namespace perfetto::trace_processor::util