| /* |
| * 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 |