From 15ff47ccb7a8a1c5b80120965bfe89fe81a4396d Mon Sep 17 00:00:00 2001 From: Dewey Dunnington Date: Fri, 9 Oct 2026 18:12:12 -0500 Subject: [PATCH 1/3] fix(ipc): validate optional flatbuffer tables --- src/nanoarrow/ipc/decoder.c | 54 ++++++++---- src/nanoarrow/ipc/decoder_test.cc | 136 +++++++++++++++++++++++++----- src/nanoarrow/ipc/writer_test.cc | 3 + 3 files changed, 155 insertions(+), 38 deletions(-) diff --git a/src/nanoarrow/ipc/decoder.c b/src/nanoarrow/ipc/decoder.c index 8eb91fe52..e0086806b 100644 --- a/src/nanoarrow/ipc/decoder.c +++ b/src/nanoarrow/ipc/decoder.c @@ -1139,26 +1139,30 @@ static int ArrowIpcDecoderSetTypeUnion(struct ArrowSchema* schema, static int ArrowIpcDecoderSetType(struct ArrowSchema* schema, ns(Field_table_t) field, int64_t n_children, struct ArrowError* error) { - int type_type = ns(Field_type_type(field)); + ns(Type_union_type_t) type_type = ns(Field_type_type(field)); + flatbuffers_generic_t type = ns(Field_type_get(field)); + if (type == NULL) { + ArrowErrorSet(error, "Field type '%s' has no value", ns(Type_type_name(type_type))); + return EINVAL; + } + switch (type_type) { case ns(Type_Null): return ArrowIpcDecoderSetTypeSimple(schema, NANOARROW_TYPE_NA, error); case ns(Type_Bool): return ArrowIpcDecoderSetTypeSimple(schema, NANOARROW_TYPE_BOOL, error); case ns(Type_Int): - return ArrowIpcDecoderSetTypeInt(schema, ns(Field_type_get(field)), error); + return ArrowIpcDecoderSetTypeInt(schema, type, error); case ns(Type_FloatingPoint): - return ArrowIpcDecoderSetTypeFloatingPoint(schema, ns(Field_type_get(field)), - error); + return ArrowIpcDecoderSetTypeFloatingPoint(schema, type, error); case ns(Type_Decimal): - return ArrowIpcDecoderSetTypeDecimal(schema, ns(Field_type_get(field)), error); + return ArrowIpcDecoderSetTypeDecimal(schema, type, error); case ns(Type_Binary): return ArrowIpcDecoderSetTypeSimple(schema, NANOARROW_TYPE_BINARY, error); case ns(Type_LargeBinary): return ArrowIpcDecoderSetTypeSimple(schema, NANOARROW_TYPE_LARGE_BINARY, error); case ns(Type_FixedSizeBinary): - return ArrowIpcDecoderSetTypeFixedSizeBinary(schema, ns(Field_type_get(field)), - error); + return ArrowIpcDecoderSetTypeFixedSizeBinary(schema, type, error); case ns(Type_BinaryView): ArrowErrorSet(error, "BinaryView not yet supported in IPC reader"); return ENOTSUP; @@ -1170,15 +1174,15 @@ static int ArrowIpcDecoderSetType(struct ArrowSchema* schema, ns(Field_table_t) ArrowErrorSet(error, "Utf8View not yet supported in IPC reader"); return ENOTSUP; case ns(Type_Date): - return ArrowIpcDecoderSetTypeDate(schema, ns(Field_type_get(field)), error); + return ArrowIpcDecoderSetTypeDate(schema, type, error); case ns(Type_Time): - return ArrowIpcDecoderSetTypeTime(schema, ns(Field_type_get(field)), error); + return ArrowIpcDecoderSetTypeTime(schema, type, error); case ns(Type_Timestamp): - return ArrowIpcDecoderSetTypeTimestamp(schema, ns(Field_type_get(field)), error); + return ArrowIpcDecoderSetTypeTimestamp(schema, type, error); case ns(Type_Duration): - return ArrowIpcDecoderSetTypeDuration(schema, ns(Field_type_get(field)), error); + return ArrowIpcDecoderSetTypeDuration(schema, type, error); case ns(Type_Interval): - return ArrowIpcDecoderSetTypeInterval(schema, ns(Field_type_get(field)), error); + return ArrowIpcDecoderSetTypeInterval(schema, type, error); case ns(Type_Struct_): return ArrowIpcDecoderSetTypeSimpleNested(schema, "+s", error); case ns(Type_List): @@ -1186,17 +1190,15 @@ static int ArrowIpcDecoderSetType(struct ArrowSchema* schema, ns(Field_table_t) case ns(Type_LargeList): return ArrowIpcDecoderSetTypeSimpleNested(schema, "+L", error); case ns(Type_FixedSizeList): - return ArrowIpcDecoderSetTypeFixedSizeList(schema, ns(Field_type_get(field)), - error); + return ArrowIpcDecoderSetTypeFixedSizeList(schema, type, error); case ns(Type_ListView): case ns(Type_LargeListView): ArrowErrorSet(error, "ListView/LargeListView not yet supported in IPC reader"); return ENOTSUP; case ns(Type_Map): - return ArrowIpcDecoderSetTypeMap(schema, ns(Field_type_get(field)), error); + return ArrowIpcDecoderSetTypeMap(schema, type, error); case ns(Type_Union): - return ArrowIpcDecoderSetTypeUnion(schema, ns(Field_type_get(field)), n_children, - error); + return ArrowIpcDecoderSetTypeUnion(schema, type, n_children, error); case ns(Type_RunEndEncoded): ArrowErrorSet(error, "RunEndEncoded not yet supported in IPC reader"); return ENOTSUP; @@ -1229,6 +1231,9 @@ static int ArrowIpcSetDictionaryEncoding( return EINVAL; } + int64_t field_flags = schema->flags; + schema->flags |= ARROW_FLAG_NULLABLE; + struct ArrowSchema tmp; ArrowSchemaMove(schema, &tmp); @@ -1259,6 +1264,10 @@ static int ArrowIpcSetDictionaryEncoding( schema->flags |= ARROW_FLAG_DICTIONARY_ORDERED; } + if (!(field_flags & ARROW_FLAG_NULLABLE)) { + schema->flags &= ~ARROW_FLAG_NULLABLE; + } + // Sort out field metadata between the schema and the dictionary member NANOARROW_RETURN_NOT_OK_WITH_ERROR(ArrowIpcMoveDictionaryMetadataBackToField(schema), error); @@ -1424,6 +1433,11 @@ static int ArrowIpcDecoderDecodeDictionaryBatchHeader( (struct ArrowIpcDecoderPrivate*)decoder->private_data; ns(DictionaryBatch_table_t) dictionary = (ns(DictionaryBatch_table_t))message_header; + if (!ns(DictionaryBatch_data_is_present(dictionary))) { + ArrowErrorSet(error, "DictionaryBatch has no data"); + return EINVAL; + } + private_data->dictionary.id = ns(DictionaryBatch_id(dictionary)); private_data->dictionary.is_delta = ns(DictionaryBatch_isDelta(dictionary)); @@ -1730,6 +1744,12 @@ ArrowErrorCode ArrowIpcDecoderDecodeHeader(struct ArrowIpcDecoder* decoder, } flatbuffers_generic_t message_header = ns(Message_header_get(message)); + if (message_header == NULL) { + ArrowErrorSet(error, "Message header '%s' has no value", + ns(MessageHeader_type_name(decoder->message_type))); + return EINVAL; + } + switch (decoder->message_type) { case ns(MessageHeader_Schema): NANOARROW_RETURN_NOT_OK( diff --git a/src/nanoarrow/ipc/decoder_test.cc b/src/nanoarrow/ipc/decoder_test.cc index b2cbaf7dc..f8f37aac8 100644 --- a/src/nanoarrow/ipc/decoder_test.cc +++ b/src/nanoarrow/ipc/decoder_test.cc @@ -685,6 +685,7 @@ TEST(NanoarrowIpcTest, NanoarrowIpcDecodeDictionarySchemaWithoutIndexType) { struct ArrowIpcDecoder decoder; struct ArrowError error; struct ArrowSchema schema; + struct ArrowIpcDictionaryEncodings dictionary_encodings; struct ArrowBufferView data; data.data.as_uint8 = kDictionarySchemaNoIndexType; @@ -696,15 +697,19 @@ TEST(NanoarrowIpcTest, NanoarrowIpcDecodeDictionarySchemaWithoutIndexType) { << error.message; ASSERT_EQ(decoder.message_type, NANOARROW_IPC_MESSAGE_TYPE_SCHEMA); - ASSERT_EQ(ArrowIpcDecoderDecodeSchema(&decoder, &schema, &error), NANOARROW_OK) + ASSERT_EQ(ArrowIpcDecoderDecodeSchemaWithDictionaries(&decoder, &schema, + &dictionary_encodings, &error), + NANOARROW_OK) << error.message; ASSERT_EQ(schema.n_children, 1); EXPECT_STREQ(schema.children[0]->name, "some_col"); EXPECT_STREQ(schema.children[0]->format, "i"); ASSERT_NE(schema.children[0]->dictionary, nullptr); EXPECT_STREQ(schema.children[0]->dictionary->format, "u"); + ASSERT_NE(ArrowIpcDictionaryEncodingsFindById(&dictionary_encodings, 0), nullptr); ArrowSchemaRelease(&schema); + ArrowIpcDictionaryEncodingsReset(&dictionary_encodings); ArrowIpcDecoderReset(&decoder); } @@ -1430,6 +1435,30 @@ void AssertArrayViewIdentical(const struct ArrowArrayView* actual, } } +static void FinishEncapsulatedMessage(flatcc_builder_t* builder, + struct ArrowBuffer* out) { + // Encapsulate: continuation, little endian header size, header, padding to 8 bytes + size_t size = flatcc_builder_get_buffer_size(builder); + int64_t padded_size = _ArrowRoundUpToMultipleOf8(static_cast(size)); + ASSERT_EQ( + ArrowBufferReserve(out, 2 * static_cast(sizeof(int32_t)) + padded_size), + NANOARROW_OK); + ASSERT_EQ(ArrowBufferAppendInt32(out, -1), NANOARROW_OK); +#if defined(__BIG_ENDIAN__) + ASSERT_EQ(ArrowBufferAppendInt32( + out, static_cast(bswap32(static_cast(padded_size)))), + NANOARROW_OK); +#else + ASSERT_EQ(ArrowBufferAppendInt32(out, static_cast(padded_size)), NANOARROW_OK); +#endif + ASSERT_NE(flatcc_builder_copy_buffer(builder, out->data + out->size_bytes, size), + nullptr); + out->size_bytes += size; + while (out->size_bytes % 8 != 0) { + out->data[out->size_bytes++] = 0; + } +} + // Builds an encapsulated Schema message whose Message.custom_metadata and // Schema.custom_metadata each contain a KeyValue with no key. Both `key` and `value` // are optional fields of KeyValue in the IPC format, so this passes flatbuffer @@ -1462,30 +1491,95 @@ static void MakeKeylessMetadataSchemaMessage(struct ArrowBuffer* out) { ASSERT_EQ(ns(Message_bodyLength_add(&builder, 0)), 0); ASSERT_NE(ns(Message_end_as_root(&builder)), 0); - // Encapsulate: continuation, little endian header size, header, padding to 8 bytes - size_t size = flatcc_builder_get_buffer_size(&builder); - int64_t padded_size = _ArrowRoundUpToMultipleOf8(static_cast(size)); - ASSERT_EQ( - ArrowBufferReserve(out, 2 * static_cast(sizeof(int32_t)) + padded_size), - NANOARROW_OK); - ASSERT_EQ(ArrowBufferAppendInt32(out, -1), NANOARROW_OK); -#if defined(__BIG_ENDIAN__) - ASSERT_EQ(ArrowBufferAppendInt32( - out, static_cast(bswap32(static_cast(padded_size)))), - NANOARROW_OK); -#else - ASSERT_EQ(ArrowBufferAppendInt32(out, static_cast(padded_size)), NANOARROW_OK); -#endif - ASSERT_NE(flatcc_builder_copy_buffer(&builder, out->data + out->size_bytes, size), - nullptr); - out->size_bytes += size; - while (out->size_bytes % 8 != 0) { - out->data[out->size_bytes++] = 0; - } + ASSERT_NO_FATAL_FAILURE(FinishEncapsulatedMessage(&builder, out)); + flatcc_builder_clear(&builder); +} + +static void MakeSchemaWithMissingFieldType(struct ArrowBuffer* out) { + flatcc_builder_t builder; + flatcc_builder_init(&builder); + ASSERT_EQ(ns(Message_start_as_root(&builder)), 0); + ASSERT_EQ(ns(Message_version_add(&builder, ns(MetadataVersion_V5))), 0); + ASSERT_EQ(ns(Message_header_Schema_start(&builder)), 0); + ASSERT_EQ(ns(Schema_endianness_add(&builder, ns(Endianness_Little))), 0); + ASSERT_EQ(ns(Schema_fields_start(&builder)), 0); + ASSERT_EQ(ns(Schema_fields_push_start(&builder)), 0); + ASSERT_EQ(ns(Field_type_add_type(&builder, ns(Type_Int))), 0); + ASSERT_NE(ns(Schema_fields_push_end(&builder)), nullptr); + ASSERT_EQ(ns(Schema_fields_end(&builder)), 0); + ASSERT_EQ(ns(Message_header_Schema_end(&builder)), 0); + ASSERT_NE(ns(Message_end_as_root(&builder)), 0); + + ASSERT_NO_FATAL_FAILURE(FinishEncapsulatedMessage(&builder, out)); + flatcc_builder_clear(&builder); +} + +static void MakeMessageWithMissingHeader(struct ArrowBuffer* out) { + flatcc_builder_t builder; + flatcc_builder_init(&builder); + + ASSERT_EQ(ns(Message_start_as_root(&builder)), 0); + ASSERT_EQ(ns(Message_version_add(&builder, ns(MetadataVersion_V5))), 0); + ASSERT_EQ(ns(Message_header_add_type(&builder, ns(MessageHeader_Schema))), 0); + ASSERT_NE(ns(Message_end_as_root(&builder)), 0); + + ASSERT_NO_FATAL_FAILURE(FinishEncapsulatedMessage(&builder, out)); flatcc_builder_clear(&builder); } +static void MakeDictionaryBatchWithoutData(struct ArrowBuffer* out) { + flatcc_builder_t builder; + flatcc_builder_init(&builder); + + ASSERT_EQ(ns(Message_start_as_root(&builder)), 0); + ASSERT_EQ(ns(Message_version_add(&builder, ns(MetadataVersion_V5))), 0); + ASSERT_EQ(ns(Message_header_DictionaryBatch_start(&builder)), 0); + ASSERT_EQ(ns(DictionaryBatch_id_add(&builder, 0)), 0); + ASSERT_EQ(ns(Message_header_DictionaryBatch_end(&builder)), 0); + ASSERT_NE(ns(Message_end_as_root(&builder)), 0); + + ASSERT_NO_FATAL_FAILURE(FinishEncapsulatedMessage(&builder, out)); + flatcc_builder_clear(&builder); +} + +TEST(NanoarrowIpcTest, NanoarrowIpcDecodeMissingOptionalTables) { + struct ArrowError error; + nanoarrow::ipc::UniqueDecoder decoder; + ASSERT_EQ(ArrowIpcDecoderInit(decoder.get()), NANOARROW_OK); + + nanoarrow::UniqueBuffer missing_field_type; + ASSERT_NO_FATAL_FAILURE(MakeSchemaWithMissingFieldType(missing_field_type.get())); + struct ArrowBufferView data; + data.data.as_uint8 = missing_field_type->data; + data.size_bytes = missing_field_type->size_bytes; + ASSERT_EQ(ArrowIpcDecoderVerifyHeader(decoder.get(), data, &error), NANOARROW_OK) + << error.message; + ASSERT_EQ(ArrowIpcDecoderDecodeHeader(decoder.get(), data, &error), NANOARROW_OK) + << error.message; + nanoarrow::UniqueSchema schema; + EXPECT_EQ(ArrowIpcDecoderDecodeSchema(decoder.get(), schema.get(), &error), EINVAL); + EXPECT_STREQ(error.message, "Field type 'Int' has no value"); + + nanoarrow::UniqueBuffer missing_header; + ASSERT_NO_FATAL_FAILURE(MakeMessageWithMissingHeader(missing_header.get())); + data.data.as_uint8 = missing_header->data; + data.size_bytes = missing_header->size_bytes; + ASSERT_EQ(ArrowIpcDecoderVerifyHeader(decoder.get(), data, &error), NANOARROW_OK) + << error.message; + EXPECT_EQ(ArrowIpcDecoderDecodeHeader(decoder.get(), data, &error), EINVAL); + EXPECT_STREQ(error.message, "Message header 'Schema' has no value"); + + nanoarrow::UniqueBuffer missing_dictionary_data; + ASSERT_NO_FATAL_FAILURE(MakeDictionaryBatchWithoutData(missing_dictionary_data.get())); + data.data.as_uint8 = missing_dictionary_data->data; + data.size_bytes = missing_dictionary_data->size_bytes; + ASSERT_EQ(ArrowIpcDecoderVerifyHeader(decoder.get(), data, &error), NANOARROW_OK) + << error.message; + EXPECT_EQ(ArrowIpcDecoderDecodeHeader(decoder.get(), data, &error), EINVAL); + EXPECT_STREQ(error.message, "DictionaryBatch has no data"); +} + TEST(NanoarrowIpcTest, NanoarrowIpcDecodeMetadataWithoutKey) { nanoarrow::UniqueBuffer message; ASSERT_NO_FATAL_FAILURE(MakeKeylessMetadataSchemaMessage(message.get())); diff --git a/src/nanoarrow/ipc/writer_test.cc b/src/nanoarrow/ipc/writer_test.cc index 3b08d0c89..edcc8839c 100644 --- a/src/nanoarrow/ipc/writer_test.cc +++ b/src/nanoarrow/ipc/writer_test.cc @@ -339,6 +339,7 @@ TEST(NanoarrowIpcWriter, RoundtripDictionaryStream) { nanoarrow::UniqueSchema schema; nanoarrow::UniqueArray array; MakeDictionaryStructArray(array.get(), schema.get()); + schema->children[0]->flags &= ~ARROW_FLAG_NULLABLE; nanoarrow::UniqueArrayStream array_stream; ASSERT_EQ(ArrowBasicArrayStreamInit(array_stream.get(), schema.get(), 1), NANOARROW_OK); @@ -366,7 +367,9 @@ TEST(NanoarrowIpcWriter, RoundtripDictionaryStream) { NANOARROW_OK) << error.message; ASSERT_EQ(roundtrip_schema->n_children, 1); + EXPECT_EQ(roundtrip_schema->children[0]->flags & ARROW_FLAG_NULLABLE, 0); ASSERT_NE(roundtrip_schema->children[0]->dictionary, nullptr); + EXPECT_NE(roundtrip_schema->children[0]->dictionary->flags & ARROW_FLAG_NULLABLE, 0); EXPECT_STREQ(roundtrip_schema->children[0]->dictionary->format, "u"); nanoarrow::UniqueArray roundtrip_array; From 7a547a555d2d407bffee50cb325997188a7cbd4a Mon Sep 17 00:00:00 2001 From: Dewey Dunnington Date: Fri, 9 Oct 2026 20:16:25 -0500 Subject: [PATCH 2/3] fix(ipc): avoid narrowing message type --- src/nanoarrow/ipc/decoder.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/nanoarrow/ipc/decoder.c b/src/nanoarrow/ipc/decoder.c index e0086806b..3d9eacf46 100644 --- a/src/nanoarrow/ipc/decoder.c +++ b/src/nanoarrow/ipc/decoder.c @@ -1724,7 +1724,8 @@ ArrowErrorCode ArrowIpcDecoderDecodeHeader(struct ArrowIpcDecoder* decoder, // Read some basic information from the message decoder->metadata_version = ns(Message_version(message)); - decoder->message_type = ns(Message_header_type(message)); + ns(MessageHeader_union_type_t) message_type = ns(Message_header_type(message)); + decoder->message_type = message_type; decoder->body_size_bytes = ns(Message_bodyLength(message)); switch (decoder->metadata_version) { @@ -1746,11 +1747,11 @@ ArrowErrorCode ArrowIpcDecoderDecodeHeader(struct ArrowIpcDecoder* decoder, flatbuffers_generic_t message_header = ns(Message_header_get(message)); if (message_header == NULL) { ArrowErrorSet(error, "Message header '%s' has no value", - ns(MessageHeader_type_name(decoder->message_type))); + ns(MessageHeader_type_name(message_type))); return EINVAL; } - switch (decoder->message_type) { + switch (message_type) { case ns(MessageHeader_Schema): NANOARROW_RETURN_NOT_OK( ArrowIpcDecoderDecodeSchemaHeader(decoder, message_header, error)); From 37ccebf235f94ffd657fd312f773b316b3c239fb Mon Sep 17 00:00:00 2001 From: Dewey Dunnington Date: Sat, 10 Oct 2026 21:11:52 -0500 Subject: [PATCH 3/3] fix(ipc): validate dictionary batch vector counts --- src/nanoarrow/ipc/decoder.c | 7 ++- src/nanoarrow/ipc/decoder_test.cc | 101 ++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 2 deletions(-) diff --git a/src/nanoarrow/ipc/decoder.c b/src/nanoarrow/ipc/decoder.c index 3d9eacf46..8953cb297 100644 --- a/src/nanoarrow/ipc/decoder.c +++ b/src/nanoarrow/ipc/decoder.c @@ -2797,11 +2797,14 @@ static ArrowErrorCode ArrowIpcDecoderDecodeDictionaryInternal( struct ArrowIpcDecoderPrivate* dictionary_decoder_private_data = (struct ArrowIpcDecoderPrivate*)dictionary->decoder.private_data; dictionary->decoder.message_type = NANOARROW_IPC_MESSAGE_TYPE_RECORD_BATCH; - dictionary_decoder_private_data->last_message = record_batch; + dictionary->decoder.metadata_version = decoder->metadata_version; // Transfer the endianness and compression settings so that buffers are byte-swapped // and decompressed if needed (the nested decoder uses a default decompressor) dictionary_decoder_private_data->endianness = private_data->endianness; - dictionary->decoder.codec = decoder->codec; + // Validate the nested batch's node and buffer counts before accessing its vectors. + NANOARROW_RETURN_NOT_OK( + ArrowIpcDecoderDecodeRecordBatchHeader(&dictionary->decoder, record_batch, error)); + dictionary_decoder_private_data->last_message = record_batch; struct ArrowArrayView* array_view; NANOARROW_RETURN_NOT_OK(ArrowIpcDecoderDecodeArrayViewInternal( diff --git a/src/nanoarrow/ipc/decoder_test.cc b/src/nanoarrow/ipc/decoder_test.cc index f8f37aac8..3fb2bf6e0 100644 --- a/src/nanoarrow/ipc/decoder_test.cc +++ b/src/nanoarrow/ipc/decoder_test.cc @@ -1580,6 +1580,107 @@ TEST(NanoarrowIpcTest, NanoarrowIpcDecodeMissingOptionalTables) { EXPECT_STREQ(error.message, "DictionaryBatch has no data"); } +static void MakeDictionaryBatchWithVectorCounts(int64_t n_nodes, int64_t n_buffers, + struct ArrowBuffer* out) { + flatcc_builder_t builder; + flatcc_builder_init(&builder); + + ASSERT_EQ(ns(Message_start_as_root(&builder)), 0); + ASSERT_EQ(ns(Message_version_add(&builder, ns(MetadataVersion_V5))), 0); + ASSERT_EQ(ns(Message_header_DictionaryBatch_start(&builder)), 0); + ASSERT_EQ(ns(DictionaryBatch_data_start(&builder)), 0); + // Negative counts omit the optional vectors entirely. + if (n_nodes >= 0) { + std::vector nodes(n_nodes); + ASSERT_EQ(ns(RecordBatch_nodes_create(&builder, nodes.data(), nodes.size())), 0); + } + if (n_buffers >= 0) { + std::vector buffers(n_buffers); + ASSERT_EQ(ns(RecordBatch_buffers_create(&builder, buffers.data(), buffers.size())), + 0); + } + ASSERT_EQ(ns(DictionaryBatch_data_end(&builder)), 0); + ASSERT_EQ(ns(Message_header_DictionaryBatch_end(&builder)), 0); + ASSERT_NE(ns(Message_end_as_root(&builder)), 0); + + ASSERT_NO_FATAL_FAILURE(FinishEncapsulatedMessage(&builder, out)); + flatcc_builder_clear(&builder); +} + +TEST(NanoarrowIpcTest, NanoarrowIpcDecodeDictionaryBatchInvalidVectorCounts) { + struct ArrowError error; + nanoarrow::ipc::UniqueDecoder decoder; + ASSERT_EQ(ArrowIpcDecoderInit(decoder.get()), NANOARROW_OK); + struct ArrowBufferView data; + data.data.as_uint8 = kDictionarySchema; + data.size_bytes = sizeof(kDictionarySchema); + ASSERT_EQ(ArrowIpcDecoderVerifyHeader(decoder.get(), data, &error), NANOARROW_OK); + ASSERT_EQ(ArrowIpcDecoderDecodeHeader(decoder.get(), data, &error), NANOARROW_OK); + + nanoarrow::UniqueSchema schema; + nanoarrow::ipc::UniqueDictionaryEncodings encodings; + ASSERT_EQ(ArrowIpcDecoderDecodeSchemaWithDictionaries(decoder.get(), schema.get(), + encodings.get(), &error), + NANOARROW_OK); + nanoarrow::ipc::UniqueDictionaries dictionaries; + ASSERT_EQ(ArrowIpcDictionariesInit(dictionaries.get(), encodings.get(), &error), + NANOARROW_OK); + ASSERT_EQ(ArrowIpcDecoderSetEndianness(decoder.get(), NANOARROW_IPC_ENDIANNESS_LITTLE), + NANOARROW_OK); + + const int64_t counts[][2] = {{-1, 3}, {0, 3}, {2, 3}, {1, -1}, + {1, 0}, {1, 1}, {1, 2}, {1, 4}}; + for (const auto& count : counts) { + SCOPED_TRACE(::testing::Message() + << "nodes=" << count[0] << ", buffers=" << count[1]); + nanoarrow::UniqueBuffer message; + ASSERT_NO_FATAL_FAILURE( + MakeDictionaryBatchWithVectorCounts(count[0], count[1], message.get())); + data.data.as_uint8 = message->data; + data.size_bytes = message->size_bytes; + ASSERT_EQ(ArrowIpcDecoderVerifyHeader(decoder.get(), data, &error), NANOARROW_OK) + << error.message; + ASSERT_EQ(ArrowIpcDecoderDecodeHeader(decoder.get(), data, &error), NANOARROW_OK) + << error.message; + + struct ArrowBufferView body = {}; + std::string expected; + if (count[0] != 1) { + expected = "Expected 1 field nodes in message but found " + + std::to_string(count[0] < 0 ? 0 : count[0]); + } else { + expected = "Expected 3 buffers in message but found " + + std::to_string(count[1] < 0 ? 0 : count[1]); + } + EXPECT_EQ(ArrowIpcDecoderDecodeDictionary(decoder.get(), body, + NANOARROW_VALIDATION_LEVEL_FULL, + dictionaries.get(), &error), + EINVAL); + EXPECT_STREQ(error.message, expected.c_str()); + + nanoarrow::UniqueBuffer shared_body; + EXPECT_EQ(ArrowIpcDecoderDecodeDictionaryFromShared(decoder.get(), shared_body.get(), + NANOARROW_VALIDATION_LEVEL_FULL, + dictionaries.get(), &error), + EINVAL); + EXPECT_STREQ(error.message, expected.c_str()); + } + + // A rejected dictionary batch must not prevent decoding the next valid batch. + data.data.as_uint8 = kDictionaryBatch; + data.size_bytes = sizeof(kDictionaryBatch); + ASSERT_EQ(ArrowIpcDecoderVerifyHeader(decoder.get(), data, &error), NANOARROW_OK); + ASSERT_EQ(ArrowIpcDecoderDecodeHeader(decoder.get(), data, &error), NANOARROW_OK); + struct ArrowBufferView body; + body.data.as_uint8 = data.data.as_uint8 + decoder->header_size_bytes; + body.size_bytes = decoder->body_size_bytes; + ASSERT_EQ(ArrowIpcDecoderDecodeDictionary(decoder.get(), body, + NANOARROW_VALIDATION_LEVEL_FULL, + dictionaries.get(), &error), + NANOARROW_OK) + << error.message; +} + TEST(NanoarrowIpcTest, NanoarrowIpcDecodeMetadataWithoutKey) { nanoarrow::UniqueBuffer message; ASSERT_NO_FATAL_FAILURE(MakeKeylessMetadataSchemaMessage(message.get()));