This is an automated email from the ASF dual-hosted git repository.
kou pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow.git
The following commit(s) were added to refs/heads/main by this push:
new f3f65df830 GH-50240: [C++] Make IPC message decoding stricter (#50235)
f3f65df830 is described below
commit f3f65df830fdb35965b3ce38983ebcc2e82c22a6
Author: Antoine Pitrou <[email protected]>
AuthorDate: Wed Jun 24 03:00:28 2026 +0200
GH-50240: [C++] Make IPC message decoding stricter (#50235)
### Rationale for this change
An IPC file has a footer listing the exact locations in the file of the
various IPC messages, such as RecordBatch messages.
However, we currently don't notice if a message size advertised in the IPC
footer is larger than the actual serialized message size, therefore we happily
accept invalid IPC files.
Found by OSS-Fuzz in https://issues.oss-fuzz.com/issues/524437775
### What changes are included in this PR?
1. Error out when a message metadata size doesn't match the advertised value
2. Also fix a bug where `ReadFieldsSubset` did not properly handle legacy
IPC encapsulation (without a continuation indicator)
3. Add a test suite for `MessageDecoder` and the various `ReadMessage`
functions
### Are these changes tested?
Yes, by new test suite and by additional fuzz regression file.
### Are there any user-facing changes?
Being stricter implies that some IPC files _might_ be rejected that were
accepted before. Hopefully such files don't exist, but some IPC writers might
have emitted them anyway.
* GitHub Issue: #50240
Authored-by: Antoine Pitrou <[email protected]>
Signed-off-by: Sutou Kouhei <[email protected]>
---
cpp/src/arrow/buffer.cc | 2 +-
cpp/src/arrow/buffer.h | 2 +-
cpp/src/arrow/ipc/message.cc | 214 +++++++++---------
cpp/src/arrow/ipc/message.h | 16 +-
cpp/src/arrow/ipc/message_internal_test.cc | 342 ++++++++++++++++++++++++++++-
cpp/src/arrow/ipc/options.h | 4 +-
cpp/src/arrow/ipc/reader.cc | 2 +-
cpp/src/arrow/testing/gtest_util.cc | 14 +-
cpp/src/arrow/testing/gtest_util.h | 3 +-
testing | 2 +-
10 files changed, 487 insertions(+), 114 deletions(-)
diff --git a/cpp/src/arrow/buffer.cc b/cpp/src/arrow/buffer.cc
index ab20ce7fb9..f069277e7c 100644
--- a/cpp/src/arrow/buffer.cc
+++ b/cpp/src/arrow/buffer.cc
@@ -87,7 +87,7 @@ Result<std::shared_ptr<Buffer>>
SliceMutableBufferSafe(std::shared_ptr<Buffer> b
return SliceMutableBuffer(std::move(buffer), offset, length);
}
-std::string Buffer::ToHexString() {
+std::string Buffer::ToHexString() const {
return HexEncode(data(), static_cast<size_t>(size()));
}
diff --git a/cpp/src/arrow/buffer.h b/cpp/src/arrow/buffer.h
index a2c8412082..07f9931eba 100644
--- a/cpp/src/arrow/buffer.h
+++ b/cpp/src/arrow/buffer.h
@@ -121,7 +121,7 @@ class ARROW_EXPORT Buffer {
/// \brief Construct a new std::string with a hexadecimal representation of
the buffer.
/// \return std::string
- std::string ToHexString();
+ std::string ToHexString() const;
/// Return true if both buffers are the same size and contain the same bytes
/// up to the number of compared bytes
diff --git a/cpp/src/arrow/ipc/message.cc b/cpp/src/arrow/ipc/message.cc
index 1fef961ff8..8522475782 100644
--- a/cpp/src/arrow/ipc/message.cc
+++ b/cpp/src/arrow/ipc/message.cc
@@ -285,17 +285,21 @@ std::string FormatMessageType(MessageType type) {
namespace {
-Status ReadFieldsSubset(int64_t offset, int32_t metadata_length,
- io::RandomAccessFile* file,
+Status ReadFieldsSubset(int64_t offset, io::RandomAccessFile* file,
const FieldsLoaderFunction& fields_loader,
const std::shared_ptr<Buffer>& metadata, int64_t
required_size,
std::shared_ptr<Buffer>& body) {
+ DCHECK_GE(static_cast<size_t>(metadata->size()), sizeof(int32_t));
+ const auto continuation = util::SafeLoadAs<int32_t>(metadata->data());
+ // Either 8 bytes (32-bit continuation indicator + 32-bit little-endian
length prefix)
+ // or 4 bytes for legacy IPC without continuation indicator
+ const auto continuation_size = (continuation ==
internal::kIpcContinuationToken)
+ ? 2 * sizeof(int32_t)
+ : sizeof(int32_t);
+
const flatbuf::Message* message = nullptr;
- uint8_t continuation_metadata_size = sizeof(int32_t) + sizeof(int32_t);
- // skip 8 bytes (32-bit continuation indicator + 32-bit little-endian length
prefix)
- RETURN_NOT_OK(internal::VerifyMessage(metadata->data() +
continuation_metadata_size,
- metadata->size() -
continuation_metadata_size,
- &message));
+ RETURN_NOT_OK(internal::VerifyMessage(metadata->data() + continuation_size,
+ metadata->size() - continuation_size,
&message));
auto batch = message->header_as_RecordBatch();
if (batch == nullptr) {
return Status::IOError(
@@ -305,8 +309,8 @@ Status ReadFieldsSubset(int64_t offset, int32_t
metadata_length,
RETURN_NOT_OK(fields_loader(batch, &io_recorded_random_access_file));
const auto& read_ranges = io_recorded_random_access_file.GetReadRanges();
for (const auto& range : read_ranges) {
- auto read_result = file->ReadAt(offset + metadata_length + range.offset,
range.length,
- body->mutable_data() + range.offset);
+ auto read_result = file->ReadAt(offset + metadata->size() + range.offset,
+ range.length, body->mutable_data() +
range.offset);
if (!read_result.ok()) {
return Status::IOError("Failed to read message body, error ",
read_result.status().ToString());
@@ -315,6 +319,86 @@ Status ReadFieldsSubset(int64_t offset, int32_t
metadata_length,
return Status::OK();
}
+struct ReadMessageState {
+ std::unique_ptr<Message> result;
+ std::shared_ptr<MessageDecoderListener> listener;
+ std::shared_ptr<MessageDecoder> decoder;
+
+ ReadMessageState()
+ : listener(std::make_shared<AssignMessageDecoderListener>(&result)),
+ decoder(std::make_shared<MessageDecoder>(listener)) {}
+
+ // ReadMessageState points into itself, so it shouldn't be moved
+ ReadMessageState(ReadMessageState&&) = delete;
+ ReadMessageState& operator=(ReadMessageState&&) = delete;
+};
+
+// A common continuation callback for ReadMessage and ReadMessageAsync
overloads
+static Result<std::unique_ptr<Message>> ReadMessageContinued(
+ int64_t offset, int32_t metadata_length, std::optional<int64_t>
body_length,
+ std::shared_ptr<Buffer> metadata, io::RandomAccessFile* file,
+ const FieldsLoaderFunction& fields_loader, ReadMessageState* state) {
+ MessageDecoder* decoder = state->decoder.get();
+ if (body_length.has_value()) {
+ // If body length was known, the given buffer should contain exactly the
metadata
+ // followed by the body.
+ DCHECK_EQ(metadata->size(), metadata_length + *body_length);
+ }
+ ARROW_RETURN_NOT_OK(decoder->Consume(SliceBuffer(metadata, 0,
metadata_length)));
+ if (decoder->buffered_size() > 0) {
+ return Status::Invalid("Message metadata too long by ",
decoder->buffered_size(),
+ " bytes");
+ }
+ switch (decoder->state()) {
+ case MessageDecoder::State::INITIAL:
+ return std::move(state->result);
+ case MessageDecoder::State::METADATA_LENGTH:
+ return Status::Invalid("metadata length is missing. File offset: ",
offset,
+ ", metadata length: ", metadata_length);
+ case MessageDecoder::State::METADATA:
+ return Status::Invalid("flatbuffer size ", decoder->next_required_size(),
+ " invalid. File offset: ", offset,
+ ", metadata length: ", metadata_length);
+ case MessageDecoder::State::BODY: {
+ std::shared_ptr<Buffer> body;
+ if (fields_loader) {
+ // Selective field loading: allocate a body buffer and read only the
+ // requested field ranges into it.
+ DCHECK_NE(file, nullptr);
+ ARROW_ASSIGN_OR_RAISE(
+ body, AllocateBuffer(decoder->next_required_size(),
default_memory_pool()));
+ RETURN_NOT_OK(ReadFieldsSubset(offset, file, fields_loader,
+ SliceBuffer(metadata, 0,
metadata_length),
+ decoder->next_required_size(), body));
+ } else if (body_length.has_value()) {
+ // Body was already read as part of the combined IO; just slice it out.
+ if (*body_length != decoder->next_required_size()) {
+ // The streaming decoder got out of sync with the actual advertised
+ // metadata and body size, which signals an invalid IPC file.
+ return Status::IOError("Invalid IPC file: advertised body size is ",
+ *body_length, ", but message decoder expects
to read ",
+ decoder->next_required_size(), " bytes
instead");
+ }
+ body = SliceBuffer(metadata, metadata_length,
+ std::min(*body_length, metadata->size() -
metadata_length));
+ } else {
+ // Body length was unknown; do a separate IO to read the body.
+ DCHECK_NE(file, nullptr);
+ ARROW_ASSIGN_OR_RAISE(
+ body, file->ReadAt(offset + metadata_length,
decoder->next_required_size(),
+ /*allow_short_read=*/false));
+ }
+
+ RETURN_NOT_OK(decoder->Consume(body));
+ return std::move(state->result);
+ }
+ case MessageDecoder::State::EOS:
+ return Status::Invalid("Unexpected empty message in IPC file format");
+ default:
+ return Status::Invalid("Unexpected state: ", state->decoder->state());
+ }
+}
+
} // namespace
Result<std::unique_ptr<Message>> ReadMessage(std::shared_ptr<Buffer> metadata,
@@ -330,6 +414,10 @@ Result<std::unique_ptr<Message>>
ReadMessage(std::shared_ptr<Buffer> metadata,
}
ARROW_RETURN_NOT_OK(decoder.Consume(metadata));
+ if (decoder.buffered_size() > 0) {
+ return Status::Invalid("Message metadata too long by ",
decoder.buffered_size(),
+ " bytes");
+ }
switch (decoder.state()) {
case MessageDecoder::State::INITIAL:
@@ -368,69 +456,20 @@ Result<std::unique_ptr<Message>>
ReadMessage(std::shared_ptr<Buffer> metadata,
static Result<std::unique_ptr<Message>> ReadMessageInternal(
int64_t offset, int32_t metadata_length, std::optional<int64_t>
body_length,
io::RandomAccessFile* file, const FieldsLoaderFunction& fields_loader) {
- std::unique_ptr<Message> result;
- auto listener = std::make_shared<AssignMessageDecoderListener>(&result);
- MessageDecoder decoder(listener);
+ ReadMessageState state;
- if (metadata_length < decoder.next_required_size()) {
+ if (metadata_length < state.decoder->next_required_size()) {
return Status::Invalid("metadata_length should be at least ",
- decoder.next_required_size());
+ state.decoder->next_required_size());
}
-
// When body_length is known, read metadata + body in one IO call.
// Otherwise, read only metadata first.
ARROW_ASSIGN_OR_RAISE(std::shared_ptr<Buffer> metadata,
file->ReadAt(offset, metadata_length +
body_length.value_or(0),
/*allow_short_read=*/false));
- ARROW_RETURN_NOT_OK(decoder.Consume(SliceBuffer(metadata, 0,
metadata_length)));
-
- switch (decoder.state()) {
- case MessageDecoder::State::INITIAL:
- return result;
- case MessageDecoder::State::METADATA_LENGTH:
- return Status::Invalid("metadata length is missing. File offset: ",
offset,
- ", metadata length: ", metadata_length);
- case MessageDecoder::State::METADATA:
- return Status::Invalid("flatbuffer size ", decoder.next_required_size(),
- " invalid. File offset: ", offset,
- ", metadata length: ", metadata_length);
- case MessageDecoder::State::BODY: {
- std::shared_ptr<Buffer> body;
- if (fields_loader) {
- // Selective field loading: allocate a body buffer and read only the
- // requested field ranges into it.
- ARROW_ASSIGN_OR_RAISE(
- body, AllocateBuffer(decoder.next_required_size(),
default_memory_pool()));
- RETURN_NOT_OK(ReadFieldsSubset(offset, metadata_length, file,
fields_loader,
- SliceBuffer(metadata, 0,
metadata_length),
- decoder.next_required_size(), body));
- } else if (body_length.has_value()) {
- // Body was already read as part of the combined IO; just slice it out.
- if (*body_length != decoder.next_required_size()) {
- // The streaming decoder got out of sync with the actual advertised
- // metadata and body size, which signals an invalid IPC file.
- return Status::IOError("Invalid IPC file: advertised body size is ",
- *body_length, ", but message decoder expects
to read ",
- decoder.next_required_size(), " bytes
instead");
- }
- body = SliceBuffer(metadata, metadata_length,
- std::min(*body_length, metadata->size() -
metadata_length));
- } else {
- // Body length was unknown; do a separate IO to read the body.
- ARROW_ASSIGN_OR_RAISE(
- body, file->ReadAt(offset + metadata_length,
decoder.next_required_size(),
- /*allow_short_read=*/false));
- }
-
- RETURN_NOT_OK(decoder.Consume(body));
- return result;
- }
- case MessageDecoder::State::EOS:
- return Status::Invalid("Unexpected empty message in IPC file format");
- default:
- return Status::Invalid("Unexpected state: ", decoder.state());
- }
+ return ReadMessageContinued(offset, metadata_length, body_length, metadata,
file,
+ fields_loader, &state);
}
Result<std::unique_ptr<Message>> ReadMessage(int64_t offset, int32_t
metadata_length,
@@ -452,14 +491,9 @@ Future<std::shared_ptr<Message>> ReadMessageAsync(int64_t
offset, int32_t metada
int64_t body_length,
io::RandomAccessFile* file,
const io::IOContext&
context) {
- struct State {
- std::unique_ptr<Message> result;
- std::shared_ptr<MessageDecoderListener> listener;
- std::shared_ptr<MessageDecoder> decoder;
- };
- auto state = std::make_shared<State>();
- state->listener =
std::make_shared<AssignMessageDecoderListener>(&state->result);
- state->decoder = std::make_shared<MessageDecoder>(state->listener);
+ // Make a std::shared_ptr so as to have a stable ReadMessageState pointer,
+ // since state->listener will point back to state->result.
+ auto state = std::make_shared<ReadMessageState>();
if (metadata_length < state->decoder->next_required_size()) {
return Status::Invalid("metadata_length should be at least ",
@@ -469,35 +503,11 @@ Future<std::shared_ptr<Message>> ReadMessageAsync(int64_t
offset, int32_t metada
->ReadAsync(context, offset, metadata_length + body_length,
/*allow_short_read=*/false)
.Then([=](std::shared_ptr<Buffer> metadata) ->
Result<std::shared_ptr<Message>> {
- DCHECK_EQ(metadata->size(), metadata_length + body_length);
- ARROW_RETURN_NOT_OK(
- state->decoder->Consume(SliceBuffer(metadata, 0,
metadata_length)));
- switch (state->decoder->state()) {
- case MessageDecoder::State::INITIAL:
- return std::move(state->result);
- case MessageDecoder::State::METADATA_LENGTH:
- return Status::Invalid("metadata length is missing. File offset:
", offset,
- ", metadata length: ", metadata_length);
- case MessageDecoder::State::METADATA:
- return Status::Invalid("flatbuffer size ",
- state->decoder->next_required_size(),
- " invalid. File offset: ", offset,
- ", metadata length: ", metadata_length);
- case MessageDecoder::State::BODY: {
- auto body = SliceBuffer(metadata, metadata_length, body_length);
- if (body->size() < state->decoder->next_required_size()) {
- return Status::IOError("Expected to be able to read ",
- state->decoder->next_required_size(),
- " bytes for message body, got ",
body->size());
- }
- RETURN_NOT_OK(state->decoder->Consume(body));
- return std::move(state->result);
- }
- case MessageDecoder::State::EOS:
- return Status::Invalid("Unexpected empty message in IPC file
format");
- default:
- return Status::Invalid("Unexpected state: ",
state->decoder->state());
- }
+ // Pass a nullptr file to ensure that no further IO occurs
+ // (we have fetched all the required bytes).
+ return ReadMessageContinued(offset, metadata_length, body_length,
metadata,
+ /*file=*/nullptr,
+ /*fields_loader=*/{}, state.get());
});
}
@@ -755,6 +765,8 @@ class MessageDecoder::MessageDecoderImpl {
int64_t next_required_size() const { return next_required_size_ -
buffered_size_; }
+ int64_t buffered_size() const { return buffered_size_; }
+
MessageDecoder::State state() const { return state_; }
private:
@@ -1020,6 +1032,8 @@ Status MessageDecoder::Consume(std::shared_ptr<Buffer>
buffer) {
int64_t MessageDecoder::next_required_size() const { return
impl_->next_required_size(); }
+int64_t MessageDecoder::buffered_size() const { return impl_->buffered_size();
}
+
MessageDecoder::State MessageDecoder::state() const { return impl_->state(); }
// ----------------------------------------------------------------------
diff --git a/cpp/src/arrow/ipc/message.h b/cpp/src/arrow/ipc/message.h
index df80b0eba2..c6701f200f 100644
--- a/cpp/src/arrow/ipc/message.h
+++ b/cpp/src/arrow/ipc/message.h
@@ -419,6 +419,11 @@ class ARROW_EXPORT MessageDecoder {
/// \return the current state
State state() const;
+ /// \brief Return the number of bytes buffered in the decoder.
+ ///
+ /// This method is mainly useful for testing and debugging.
+ int64_t buffered_size() const;
+
private:
class MessageDecoderImpl;
std::unique_ptr<MessageDecoderImpl> impl_;
@@ -453,9 +458,12 @@ using FieldsLoaderFunction = std::function<Status(const
void*, io::RandomAccessF
///
/// Read a length-prefixed message flatbuffer starting at the indicated file
/// offset. If the message has a body with non-zero length, it will also be
-/// read
+/// read.
///
-/// The metadata_length includes at least the length prefix and the flatbuffer
+/// The metadata_length includes the IPC encapsulation prefix and the
+/// Flatbuffers-serialized message.
+///
+/// This function should only be used when a RecordBatch message is expected.
///
/// \param[in] offset the position in the file where the message starts. The
/// first 4 bytes after the offset are the message length
@@ -474,7 +482,8 @@ Result<std::unique_ptr<Message>> ReadMessage(
/// Read a length-prefixed message flatbuffer starting at the indicated file
/// offset.
///
-/// The metadata_length includes at least the length prefix and the flatbuffer
+/// The metadata_length includes the IPC encapsulation prefix and the
+/// Flatbuffers-serialized message.
///
/// \param[in] offset the position in the file where the message starts. The
/// first 4 bytes after the offset are the message length
@@ -578,6 +587,7 @@ Status DecodeMessage(MessageDecoder* decoder,
io::InputStream* stream);
/// \param[out] message_length the total size of the payload written including
/// padding
/// \return Status
+ARROW_EXPORT
Status WriteMessage(const Buffer& message, const IpcWriteOptions& options,
io::OutputStream* file, int32_t* message_length);
diff --git a/cpp/src/arrow/ipc/message_internal_test.cc
b/cpp/src/arrow/ipc/message_internal_test.cc
index 87ce3de39c..bc4506eaae 100644
--- a/cpp/src/arrow/ipc/message_internal_test.cc
+++ b/cpp/src/arrow/ipc/message_internal_test.cc
@@ -15,15 +15,27 @@
// specific language governing permissions and limitations
// under the License.
+#include <cstdint>
+#include <iosfwd>
+#include <memory>
+#include <sstream>
+
#include <flatbuffers/flatbuffers.h>
+
#include <gtest/gtest.h>
-#include <memory>
+
+#include "Message_generated.h"
#include "arrow/buffer.h"
+#include "arrow/io/memory.h"
#include "arrow/ipc/dictionary.h"
+#include "arrow/ipc/message.h"
#include "arrow/ipc/metadata_internal.h"
#include "arrow/ipc/options.h"
#include "arrow/ipc/reader.h"
+#include "arrow/ipc/type_fwd.h"
+#include "arrow/result.h"
+#include "arrow/status.h"
#include "arrow/testing/gtest_util.h"
#include "arrow/util/key_value_metadata.h"
@@ -103,4 +115,332 @@ TEST(TestMessageInternal, TestEndiannessRoundtrip) {
}
}
+struct SampleMessageParams {
+ std::shared_ptr<const KeyValueMetadata> custom_metadata = {};
+ int64_t body_length = 0;
+ IpcWriteOptions options = {};
+
+ std::string ToString() const {
+ std::stringstream ss;
+ ss << *this;
+ return std::move(ss).str();
+ }
+
+ friend std::ostream& operator<<(std::ostream& os, const SampleMessageParams&
p) {
+ os << "legacy IPC = " << p.options.write_legacy_ipc_format << ", "
+ << "body length = " << p.body_length
+ << ", metadata length = " << (p.custom_metadata ?
p.custom_metadata->size() : 0);
+ return os;
+ }
+};
+
+struct SampleMessage {
+ std::shared_ptr<Buffer> metadata_bytes; // encapsulated with IPC framing
+ std::shared_ptr<Buffer> body_bytes;
+ MessageType message_type;
+ int64_t num_rows = 0;
+ std::shared_ptr<const KeyValueMetadata> custom_metadata;
+};
+
+class MessageDecodingTest : public ::testing::Test {
+ public:
+ static constexpr int64_t kNumRows = 5;
+
+ std::vector<IpcWriteOptions> write_options() {
+ return {IpcWriteOptions{}, IpcWriteOptions{.write_legacy_ipc_format =
true},
+ IpcWriteOptions{.alignment = 32}};
+ }
+
+ std::vector<SampleMessageParams> message_params() {
+ std::vector<SampleMessageParams> params;
+ for (const auto& options : write_options()) {
+ for (int64_t body_length : {0, 24}) {
+ params.push_back(SampleMessageParams{
+ .custom_metadata = nullptr, .body_length = body_length, .options =
options});
+ params.push_back(SampleMessageParams{.custom_metadata =
GetCustomMetadata(),
+ .body_length = body_length,
+ .options = options});
+ }
+ }
+ return params;
+ }
+
+ // Return the serialized metadata encapsulated in IPC framing
+ Result<std::shared_ptr<Buffer>> EncapsulateMetadata(
+ const std::shared_ptr<Buffer>& metadata_bytes, const IpcWriteOptions&
options) {
+ ARROW_ASSIGN_OR_RAISE(auto out_stream,
::arrow::io::BufferOutputStream::Create());
+ int32_t written_bytes = 0;
+ RETURN_NOT_OK(
+ WriteMessage(*metadata_bytes, options, out_stream.get(),
&written_bytes));
+ ARROW_ASSIGN_OR_RAISE(auto out, out_stream->Finish());
+ return out;
+ }
+
+ Result<SampleMessage> GetSampleMessage(const SampleMessageParams& params) {
+ // Create a dummy RecordBatch message
+ auto field_md =
+ std::vector{FieldMetadata{.length = kNumRows, .null_count = 1, .offset
= 0}};
+ auto buffer_md = std::vector{BufferMetadata{.offset = 64, .length = 1},
+ BufferMetadata{.offset = 72, .length = 10}};
+ std::shared_ptr<Buffer> out;
+ RETURN_NOT_OK(WriteRecordBatchMessage(/*length=*/kNumRows,
params.body_length,
+ params.custom_metadata, field_md,
buffer_md,
+ /*variadic_counts=*/{},
params.options, &out));
+ ARROW_ASSIGN_OR_RAISE(out, EncapsulateMetadata(out, params.options));
+ // Generate a dummy body of the advertised length
+ ARROW_ASSIGN_OR_RAISE(auto body_bytes, AllocateBuffer(params.body_length));
+ memset(body_bytes->mutable_data(), '!', body_bytes->size());
+ return SampleMessage{.metadata_bytes = out,
+ .body_bytes = std::move(body_bytes),
+ .message_type = MessageType::RECORD_BATCH,
+ .num_rows = kNumRows,
+ .custom_metadata = params.custom_metadata};
+ }
+
+ std::shared_ptr<const KeyValueMetadata> GetCustomMetadata() {
+ return KeyValueMetadata::Make(/*keys=*/{"key1", "key2"},
/*values=*/{"foo", "bar"});
+ }
+
+ void CheckSampleMessage(const Message& message, const SampleMessage&
sample_message) {
+ ASSERT_EQ(message.type(), sample_message.message_type);
+ const auto batch = reinterpret_cast<const
flatbuf::RecordBatch*>(message.header());
+ ASSERT_EQ(batch->length(), sample_message.num_rows);
+ ASSERT_EQ(message.body_length(), sample_message.body_bytes->size());
+ if (message.body_length() > 0) {
+ AssertBufferEqual(*message.body(), *sample_message.body_bytes,
/*verbose=*/true);
+ }
+ if (sample_message.custom_metadata &&
sample_message.custom_metadata->size() > 0) {
+ ASSERT_NE(message.custom_metadata(), nullptr);
+
ASSERT_TRUE(message.custom_metadata()->Equals(*sample_message.custom_metadata));
+ } else {
+ ASSERT_EQ(message.custom_metadata(), nullptr);
+ }
+ }
+
+ // Return concatenated metadata and body bytes
+ Result<std::shared_ptr<Buffer>> ConcatenateMessage(const SampleMessage&
sample_message,
+ int64_t padding_size = 0)
{
+ auto padding_buffer = Buffer::FromString(std::string(padding_size, 'x'));
+ return ConcatenateBuffers({padding_buffer, sample_message.metadata_bytes,
+ sample_message.body_bytes, padding_buffer});
+ }
+
+ void CheckDecoding(const std::shared_ptr<Buffer>& buffer, int64_t chunk_size,
+ const SampleMessage& sample_message) {
+ std::unique_ptr<Message> message;
+ auto listener = std::make_shared<AssignMessageDecoderListener>(&message);
+ MessageDecoder decoder(listener);
+ int64_t offset = 0;
+ ASSERT_EQ(decoder.buffered_size(), 0);
+ while (offset < buffer->size()) {
+ // No message was decoded yet
+ ASSERT_EQ(message, nullptr);
+ // The decoder is expecting more data, but not more than remaining in
our buffer
+ ASSERT_GT(decoder.next_required_size(), 0);
+ ASSERT_LE(decoder.next_required_size(), buffer->size() - offset);
+ const auto to_consume = std::min(chunk_size, buffer->size() - offset);
+ ASSERT_OK(decoder.Consume(SliceBuffer(buffer, offset, to_consume)));
+ offset += to_consume;
+ if (offset >= 4 && offset < buffer->size()) {
+ // We went past the initial 4-byte continuation
+ ASSERT_NE(decoder.state(), MessageDecoder::INITIAL);
+ if (offset >= buffer->size() - sample_message.body_bytes->size()) {
+ // The offset points in the body
+ ASSERT_EQ(decoder.state(), MessageDecoder::BODY);
+ }
+ }
+ }
+ ASSERT_EQ(decoder.buffered_size(), 0);
+ ASSERT_EQ(decoder.state(), MessageDecoder::INITIAL);
+ ASSERT_NE(message, nullptr);
+ CheckSampleMessage(*message, sample_message);
+ }
+
+ void TestDecoding(const SampleMessage& sample_message) {
+ ASSERT_OK_AND_ASSIGN(auto buffer, ConcatenateMessage(sample_message));
+ for (const auto chunk_size : std::vector<int64_t>{
+ 1, 2, 3, buffer->size() / 3, buffer->size() - 1, buffer->size()})
{
+ ARROW_SCOPED_TRACE("chunk_size = ", chunk_size);
+ CheckDecoding(buffer, chunk_size, sample_message);
+ }
+ }
+
+ void TestDecoding(const SampleMessageParams& params) {
+ ASSERT_OK_AND_ASSIGN(auto message, GetSampleMessage(params));
+ TestDecoding(message);
+ }
+
+ template <typename ReadMessageFunc>
+ void CheckReadMessageOk(ReadMessageFunc read_message,
+ const SampleMessageParams& params) {
+ ASSERT_OK_AND_ASSIGN(auto sample_message, GetSampleMessage(params));
+ ASSERT_OK_AND_ASSIGN(auto message, read_message(sample_message));
+ CheckSampleMessage(*message, sample_message);
+ }
+
+ template <typename ReadMessageFunc>
+ void CheckReadMessageTruncated(ReadMessageFunc read_message,
+ const SampleMessageParams& params,
+ bool force_truncate_metadata = false) {
+ ASSERT_OK_AND_ASSIGN(auto sample_message, GetSampleMessage(params));
+ if (force_truncate_metadata || sample_message.body_bytes->size() == 0) {
+ sample_message.metadata_bytes =
+ SliceBuffer(sample_message.metadata_bytes, /*offset=*/0,
+ sample_message.metadata_bytes->size() - 1);
+ sample_message.body_bytes = SliceBuffer(sample_message.body_bytes,
/*offset=*/0,
+ /*length=*/0);
+ } else {
+ sample_message.body_bytes = SliceBuffer(sample_message.body_bytes,
/*offset=*/0,
+
sample_message.body_bytes->size() - 1);
+ }
+ Status status = read_message(sample_message).status();
+ ASSERT_TRUE(status.IsInvalid() || status.IsIOError())
+ << "Unexpected status: " << status.ToString();
+ }
+
+ template <typename ReadMessageFunc>
+ void CheckReadMessageOversized(ReadMessageFunc read_message,
+ const SampleMessageParams& params) {
+ ASSERT_OK_AND_ASSIGN(auto sample_message, GetSampleMessage(params));
+ auto trailing_bytes = Buffer::FromString("x");
+ if (sample_message.body_bytes->size() > 0) {
+ ASSERT_OK_AND_ASSIGN(
+ sample_message.body_bytes,
+ ConcatenateBuffers({sample_message.body_bytes, trailing_bytes}));
+ } else {
+ ASSERT_OK_AND_ASSIGN(
+ sample_message.metadata_bytes,
+ ConcatenateBuffers({sample_message.metadata_bytes, trailing_bytes}));
+ }
+ Status status = read_message(sample_message).status();
+ ASSERT_TRUE(status.IsInvalid() || status.IsIOError())
+ << "Unexpected status: " << status.ToString();
+ }
+};
+
+TEST_F(MessageDecodingTest, MessageDecoder) {
+ for (const auto& params : message_params()) {
+ ARROW_SCOPED_TRACE("Params: ", params);
+ TestDecoding(params);
+ }
+}
+
+TEST_F(MessageDecodingTest, ReadMessage1) {
+ auto read_message = [&](const SampleMessage& sample_message) {
+ std::shared_ptr<Buffer> body =
+ sample_message.body_bytes->size() > 0 ? sample_message.body_bytes :
nullptr;
+ return ReadMessage(sample_message.metadata_bytes, body);
+ };
+ for (const auto& params : message_params()) {
+ ARROW_SCOPED_TRACE("Params: ", params);
+ CheckReadMessageOk(read_message, params);
+ CheckReadMessageTruncated(read_message, params);
+ CheckReadMessageOversized(read_message, params);
+ }
+}
+
+TEST_F(MessageDecodingTest, ReadMessage2) {
+ auto read_message =
+ [&](const SampleMessage& sample_message) ->
Result<std::unique_ptr<Message>> {
+ const int kStreamOffset = 42;
+ ARROW_ASSIGN_OR_RAISE(
+ auto stream_buf,
+ ConcatenateMessage(sample_message, /*padding_size=*/kStreamOffset));
+ io::BufferReader reader(stream_buf);
+ return ReadMessage(kStreamOffset,
+
static_cast<int32_t>(sample_message.metadata_bytes->size()),
+
static_cast<int64_t>(sample_message.body_bytes->size()), &reader);
+ };
+ for (const auto& params : message_params()) {
+ ARROW_SCOPED_TRACE("Params: ", params);
+ CheckReadMessageOk(read_message, params);
+ CheckReadMessageTruncated(read_message, params);
+ CheckReadMessageOversized(read_message, params);
+ }
+}
+
+TEST_F(MessageDecodingTest, ReadMessageAsync) {
+ auto read_message =
+ [&](const SampleMessage& sample_message) ->
Result<std::shared_ptr<Message>> {
+ const int kStreamOffset = 42;
+ ARROW_ASSIGN_OR_RAISE(
+ auto stream_buf,
+ ConcatenateMessage(sample_message, /*padding_size=*/kStreamOffset));
+ io::BufferReader reader(stream_buf);
+ return ReadMessageAsync(
+ kStreamOffset,
static_cast<int32_t>(sample_message.metadata_bytes->size()),
+ static_cast<int64_t>(sample_message.body_bytes->size()),
&reader)
+ .result();
+ };
+ for (const auto& params : message_params()) {
+ ARROW_SCOPED_TRACE("Params: ", params);
+ CheckReadMessageOk(read_message, params);
+ CheckReadMessageTruncated(read_message, params);
+ CheckReadMessageOversized(read_message, params);
+ }
+}
+
+TEST_F(MessageDecodingTest, ReadMessage3) {
+ int padding_size;
+
+ auto read_message =
+ [&](const SampleMessage& sample_message) ->
Result<std::unique_ptr<Message>> {
+ // No padding, so that reading the truncated message actually fails
+ ARROW_ASSIGN_OR_RAISE(auto stream_buf,
+ ConcatenateMessage(sample_message, padding_size));
+ io::BufferReader reader(stream_buf);
+ return ReadMessage(/*offset=*/padding_size,
+
static_cast<int32_t>(sample_message.metadata_bytes->size()),
+ &reader, /*fields_loader=*/{});
+ };
+ padding_size = 0;
+ for (const auto& params : message_params()) {
+ ARROW_SCOPED_TRACE("Params: ", params);
+ CheckReadMessageOk(read_message, params);
+ CheckReadMessageTruncated(read_message, params);
+ }
+ // With a non-zero padding, a truncated message wouldn't fail
+ padding_size = 42;
+ for (const auto& params : message_params()) {
+ ARROW_SCOPED_TRACE("Params: ", params);
+ CheckReadMessageOk(read_message, params);
+ }
+}
+
+TEST_F(MessageDecodingTest, ReadMessage4) {
+ FieldsLoaderFunction fields_loader = [&](const void* void_batch,
+ io::RandomAccessFile* file) ->
Status {
+ auto* batch = reinterpret_cast<const flatbuf::RecordBatch*>(void_batch);
+ // Check something about the message header
+ EXPECT_EQ(batch->length(), kNumRows);
+ // Read the entire body range from the file
+ ARROW_ASSIGN_OR_RAISE(auto read_size, file->GetSize());
+ return file->ReadAt(/*position=*/0, read_size,
/*allow_short_read=*/false).status();
+ };
+
+ int padding_size;
+ auto read_message =
+ [&](const SampleMessage& sample_message) ->
Result<std::unique_ptr<Message>> {
+ // No padding, so that reading the truncated message actually fails
+ ARROW_ASSIGN_OR_RAISE(auto stream_buf,
+ ConcatenateMessage(sample_message, padding_size));
+ io::BufferReader reader(stream_buf);
+ return ReadMessage(/*offset=*/padding_size,
+
static_cast<int32_t>(sample_message.metadata_bytes->size()),
+ &reader, fields_loader);
+ };
+ padding_size = 0;
+ for (const auto& params : message_params()) {
+ ARROW_SCOPED_TRACE("Params: ", params);
+ CheckReadMessageOk(read_message, params);
+ CheckReadMessageTruncated(read_message, params,
/*force_truncate_metadata=*/true);
+ }
+ // With a non-zero padding, a truncated message wouldn't fail
+ padding_size = 42;
+ for (const auto& params : message_params()) {
+ ARROW_SCOPED_TRACE("Params: ", params);
+ CheckReadMessageOk(read_message, params);
+ }
+}
+
} // namespace arrow::ipc::internal
diff --git a/cpp/src/arrow/ipc/options.h b/cpp/src/arrow/ipc/options.h
index ec0e2a5b6f..254a469386 100644
--- a/cpp/src/arrow/ipc/options.h
+++ b/cpp/src/arrow/ipc/options.h
@@ -67,7 +67,7 @@ struct ARROW_EXPORT IpcWriteOptions {
/// \brief Compression codec to use for record batch body buffers
///
/// May only be UNCOMPRESSED, LZ4_FRAME and ZSTD.
- std::shared_ptr<util::Codec> codec;
+ std::shared_ptr<util::Codec> codec = {};
/// \brief Minimum space savings percentage required for compression to be
applied
///
@@ -82,7 +82,7 @@ struct ARROW_EXPORT IpcWriteOptions {
///
/// Note that enabling this option may result in unreadable data for Arrow
C++ versions
/// prior to 12.0.0.
- std::optional<double> min_space_savings;
+ std::optional<double> min_space_savings = {};
/// \brief Use global CPU thread pool to parallelize any computational tasks
/// like compression
diff --git a/cpp/src/arrow/ipc/reader.cc b/cpp/src/arrow/ipc/reader.cc
index 22a96111f4..76d286dbdc 100644
--- a/cpp/src/arrow/ipc/reader.cc
+++ b/cpp/src/arrow/ipc/reader.cc
@@ -2875,7 +2875,7 @@ Status FuzzIpcFile(const uint8_t* data, int64_t size) {
}
}
- if (maybe_read_result.has_value()) {
+ if (final_status.ok()) {
// IPC file read successful: compare results with IPC stream reader,
// if possible.
// NOTE: some valid IPC files may not be readable as IPC streams,
diff --git a/cpp/src/arrow/testing/gtest_util.cc
b/cpp/src/arrow/testing/gtest_util.cc
index 0e2cbdb644..3ea7d9bb22 100644
--- a/cpp/src/arrow/testing/gtest_util.cc
+++ b/cpp/src/arrow/testing/gtest_util.cc
@@ -237,9 +237,17 @@ void AssertBufferEqual(const Buffer& buffer,
std::string_view expected) {
}
}
-void AssertBufferEqual(const Buffer& buffer, const Buffer& expected) {
- ASSERT_EQ(buffer.size(), expected.size()) << "Mismatching buffer size";
- ASSERT_TRUE(buffer.Equals(expected));
+void AssertBufferEqual(const Buffer& buffer, const Buffer& expected, bool
verbose) {
+ ASSERT_EQ(buffer.size(), expected.size())
+ << "Mismatching buffer size, got " << buffer.size() << ", expected "
+ << expected.size();
+ if (verbose) {
+ ASSERT_TRUE(buffer.Equals(expected))
+ << "Mismatching buffers, got : " << buffer.ToHexString()
+ << " but expected: " << expected.ToHexString();
+ } else {
+ ASSERT_TRUE(buffer.Equals(expected));
+ }
}
template <typename T>
diff --git a/cpp/src/arrow/testing/gtest_util.h
b/cpp/src/arrow/testing/gtest_util.h
index b84d253a89..a0f433d3a9 100644
--- a/cpp/src/arrow/testing/gtest_util.h
+++ b/cpp/src/arrow/testing/gtest_util.h
@@ -253,7 +253,8 @@ ARROW_TESTING_EXPORT void AssertBufferEqual(const Buffer&
buffer,
const std::vector<uint8_t>&
expected);
ARROW_TESTING_EXPORT void AssertBufferEqual(const Buffer& buffer,
std::string_view expected);
-ARROW_TESTING_EXPORT void AssertBufferEqual(const Buffer& buffer, const
Buffer& expected);
+ARROW_TESTING_EXPORT void AssertBufferEqual(const Buffer& buffer, const
Buffer& expected,
+ bool verbose = false);
ARROW_TESTING_EXPORT void AssertTypeEqual(const DataType& lhs, const DataType&
rhs,
bool check_metadata = false);
diff --git a/testing b/testing
index 1d74fce2b6..9ff285c885 160000
--- a/testing
+++ b/testing
@@ -1 +1 @@
-Subproject commit 1d74fce2b6bb30158f254fc292252f4a87fc67a3
+Subproject commit 9ff285c88565f0f6abc855918c6a342e70e4909c