mirror of
https://github.com/apache/arrow.git
synced 2026-10-28 08:35:46 +08:00
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 <antoine@python.org> Signed-off-by: Sutou Kouhei <kou@clear-code.com>
This commit is contained in:
1 parent
11721e7cd2
commit
f3f65df830
10 files changed
+488
-115
No files matched your search
@@ -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()));
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+114
-100
@@ -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(); }
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -15,15 +15,27 @@
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
#include <flatbuffers/flatbuffers.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <cstdint>
|
||||
#include <iosfwd>
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
|
||||
#include <flatbuffers/flatbuffers.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#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
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
+1
-1
Submodule testing updated: 1d74fce2b6...9ff285c885.
Reference in new issue
Block a user