Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit fc7e6e19 authored by Treehugger Robot's avatar Treehugger Robot Committed by Automerger Merge Worker
Browse files

Merge "Add chunk-level checksum to RecordedTransaction" am: bcb91683 am:...

Merge "Add chunk-level checksum to RecordedTransaction" am: bcb91683 am: 53cc415e am: 473ef33c

Original change: https://android-review.googlesource.com/c/platform/frameworks/native/+/2380712



Change-Id: Id13f4dd20380a1cb8a53b518f72e0a856a1e3331
Signed-off-by: default avatarAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
parents f25101a2 473ef33c
Loading
Loading
Loading
Loading
+133 −110
Original line number Original line Diff line number Diff line
@@ -18,6 +18,7 @@
#include <android-base/logging.h>
#include <android-base/logging.h>
#include <android-base/unique_fd.h>
#include <android-base/unique_fd.h>
#include <binder/BinderRecordReplay.h>
#include <binder/BinderRecordReplay.h>
#include <sys/mman.h>
#include <algorithm>
#include <algorithm>


using android::Parcel;
using android::Parcel;
@@ -42,34 +43,38 @@ static_assert(PADDING8(8) == 0);
//
//
// A RecordedTransaction is written to a file as a sequence of Chunks.
// A RecordedTransaction is written to a file as a sequence of Chunks.
//
//
// A Chunk consists of a ChunkDescriptor, Data, and Padding.
// A Chunk consists of a ChunkDescriptor, Data, Padding, and a Checksum.
//
//
// Data and Padding may each be zero-length as specified by the
// The ChunkDescriptor identifies the type of Data in the chunk, and the size
// ChunkDescriptor.
// of the Data.
//
//
// The ChunkDescriptor identifies the type of data in the chunk, the size of
// The Data may be any uint32 number of bytes in length in [0-0xfffffff0].
// the data in bytes, and the number of zero-bytes padding to land on an
//
// 8-byte boundary by the end of the Chunk.
// Padding is between [0-7] zero-bytes after the Data such that the Chunk ends
// on an 8-byte boundary. The ChunkDescriptor's dataSize does not include the
// size of Padding.
//
// The checksum is a 64-bit wide XOR of all previous data from the start of the
// ChunkDescriptor to the end of Padding.
//
//
// ┌───────────────────────────┐
// ┌───────────────────────────┐
// │Chunk                      │
// │Chunk                      │
// │┌────────────────────────┐│
// │┌────────────────────────┐ 
// ││ChunkDescriptor         │ │
// ││ChunkDescriptor         │ │
// ││┌───────────┬───────────┐││
// ││┌───────────┬──────────┐│ │
// │││chunkType  │paddingSize│││
// │││chunkType  │dataSize  ├┼─┼─┐
// │││uint32_t   │uint32_t   ├┼┼───┐
// │││uint32_t   │uint32_t  ││ │ │
// ││├───────────┴───────────┤││   │
// ││└───────────┴──────────┘│ │ │
// │││dataSize               │││   │
// │└────────────────────────┘ │ │
// │││uint64_t               ├┼┼─┐ │
// ││└───────────────────────┘││ │ │
// │└─────────────────────────┘│ │ │
// │┌─────────────────────────┐│ │ │
// ││Data                     ││ │ │
// ││bytes * dataSize         │◀─┘ │
// │└─────────────────────────┘│   │
// │┌─────────────────────────┐│ │
// │┌─────────────────────────┐│ │
// ││Padding                  ││   │
// ││Data                     ││ │
// ││bytes * paddingSize      │◀───┘
// ││bytes * dataSize         │◀─┘
// ││   ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┤│
// ││           Padding       ││
// │└───┴─────────────────────┘│
// │┌─────────────────────────┐│
// ││checksum                 ││
// ││uint64_t                 ││
// │└─────────────────────────┘│
// │└─────────────────────────┘│
// └───────────────────────────┘
// └───────────────────────────┘
//
//
@@ -85,20 +90,20 @@ static_assert(PADDING8(8) == 0);
// ║      End Chunk       ║
// ║      End Chunk       ║
// ╚══════════════════════╝
// ╚══════════════════════╝
//
//
// On reading a RecordedTransaction, an unrecognized chunk is skipped using
// On reading a RecordedTransaction, an unrecognized chunk is checksummed
// the size information in the ChunkDescriptor. Chunks are read and either
// then skipped according to size information in the ChunkDescriptor. Chunks
// assimilated or skipped until an End Chunk is encountered. This has three
// are read and either assimilated or skipped until an End Chunk is
// notable implications:
// encountered. This has three notable implications:
//
//
// 1. Older and newer implementations should be able to read one another's
// 1. Older and newer implementations should be able to read one another's
//    Transactions, though there will be loss of information.
//    Transactions, though there will be loss of information.
// 2. With the exception of the End Chunk, Chunks can appear in any
// 2. With the exception of the End Chunk, Chunks can appear in any order
//    order and even repeat, though this is not recommended.
//    and even repeat, though this is not recommended.
// 3. If any Chunk is repeated, old values will be overwritten by versions
// 3. If any Chunk is repeated, old values will be overwritten by versions
//    encountered later in the file.
//    encountered later in the file.
//
//
// No effort is made to ensure the expected chunks are present. A single
// No effort is made to ensure the expected chunks are present. A single
// End Chunk may therefore produce a empty, meaningless RecordedTransaction.
// End Chunk may therefore produce an empty, meaningless RecordedTransaction.


RecordedTransaction::RecordedTransaction(RecordedTransaction&& t) noexcept {
RecordedTransaction::RecordedTransaction(RecordedTransaction&& t) noexcept {
    mHeader = t.mHeader;
    mHeader = t.mHeader;
@@ -121,12 +126,12 @@ std::optional<RecordedTransaction> RecordedTransaction::fromDetails(uint32_t cod
                 0};
                 0};


    if (t.mSent.setData(dataParcel.data(), dataParcel.dataSize()) != android::NO_ERROR) {
    if (t.mSent.setData(dataParcel.data(), dataParcel.dataSize()) != android::NO_ERROR) {
        LOG(INFO) << "Failed to set sent parcel data.";
        LOG(ERROR) << "Failed to set sent parcel data.";
        return std::nullopt;
        return std::nullopt;
    }
    }


    if (t.mReply.setData(replyParcel.data(), replyParcel.dataSize()) != android::NO_ERROR) {
    if (t.mReply.setData(replyParcel.data(), replyParcel.dataSize()) != android::NO_ERROR) {
        LOG(INFO) << "Failed to set reply parcel data.";
        LOG(ERROR) << "Failed to set reply parcel data.";
        return std::nullopt;
        return std::nullopt;
    }
    }


@@ -134,94 +139,111 @@ std::optional<RecordedTransaction> RecordedTransaction::fromDetails(uint32_t cod
}
}


enum {
enum {
    HEADER_CHUNK = 0x00000001,
    HEADER_CHUNK = 1,
    DATA_PARCEL_CHUNK = 0x00000002,
    DATA_PARCEL_CHUNK = 2,
    REPLY_PARCEL_CHUNK = 0x00000003,
    REPLY_PARCEL_CHUNK = 3,
    INVALID_CHUNK = 0x00fffffe,
    END_CHUNK = 0x00ffffff,
    END_CHUNK = 0x00ffffff,
};
};


struct ChunkDescriptor {
struct ChunkDescriptor {
    uint32_t chunkType = 0;
    uint32_t chunkType = 0;
    uint32_t padding = 0;
    uint32_t dataSize = 0;
    uint32_t dataSize = 0;
    uint32_t reserved = 0; // Future checksum
};
};
static_assert(sizeof(ChunkDescriptor) % 8 == 0);

constexpr uint32_t kMaxChunkDataSize = 0xfffffff0;
typedef uint64_t transaction_checksum_t;


static android::status_t readChunkDescriptor(borrowed_fd fd, ChunkDescriptor* chunkOut) {
static android::status_t readChunkDescriptor(borrowed_fd fd, ChunkDescriptor* chunkOut,
                                             transaction_checksum_t* sum) {
    if (!android::base::ReadFully(fd, chunkOut, sizeof(ChunkDescriptor))) {
    if (!android::base::ReadFully(fd, chunkOut, sizeof(ChunkDescriptor))) {
        LOG(INFO) << "Failed to read Chunk Descriptor from fd " << fd.get();
        LOG(ERROR) << "Failed to read Chunk Descriptor from fd " << fd.get();
        return android::UNKNOWN_ERROR;
        return android::UNKNOWN_ERROR;
    }
    }
    if (PADDING8(chunkOut->dataSize) != chunkOut->padding) {

        chunkOut->chunkType = INVALID_CHUNK;
    *sum ^= *reinterpret_cast<transaction_checksum_t*>(chunkOut);
        LOG(INFO) << "Chunk data and padding sizes do not align." << fd.get();
        return android::BAD_VALUE;
    }
    return android::NO_ERROR;
    return android::NO_ERROR;
}
}


std::optional<RecordedTransaction> RecordedTransaction::fromFile(const unique_fd& fd) {
std::optional<RecordedTransaction> RecordedTransaction::fromFile(const unique_fd& fd) {
    RecordedTransaction t;
    RecordedTransaction t;
    ChunkDescriptor chunk;
    ChunkDescriptor chunk;

    const long pageSize = sysconf(_SC_PAGE_SIZE);
    do {
    do {
        if (NO_ERROR != readChunkDescriptor(fd, &chunk)) {
        transaction_checksum_t checksum = 0;
            LOG(INFO) << "Failed to read chunk descriptor.";
        if (NO_ERROR != readChunkDescriptor(fd, &chunk, &checksum)) {
            LOG(ERROR) << "Failed to read chunk descriptor.";
            return std::nullopt;
        }
        off_t fdCurrentPosition = lseek(fd.get(), 0, SEEK_CUR);
        off_t mmapPageAlignedStart = (fdCurrentPosition / pageSize) * pageSize;
        off_t mmapPayloadStartOffset = fdCurrentPosition - mmapPageAlignedStart;

        if (chunk.dataSize > kMaxChunkDataSize) {
            LOG(ERROR) << "Chunk data exceeds maximum size.";
            return std::nullopt;
        }

        size_t chunkPayloadSize =
                chunk.dataSize + PADDING8(chunk.dataSize) + sizeof(transaction_checksum_t);

        if (PADDING8(chunkPayloadSize) != 0) {
            LOG(ERROR) << "Invalid chunk size, not aligned " << chunkPayloadSize;
            return std::nullopt;
        }

        transaction_checksum_t* payloadMap = reinterpret_cast<transaction_checksum_t*>(
                mmap(NULL, chunkPayloadSize + mmapPayloadStartOffset, PROT_READ, MAP_SHARED,
                     fd.get(), mmapPageAlignedStart));
        payloadMap += mmapPayloadStartOffset /
                sizeof(transaction_checksum_t); // Skip chunk descriptor and required mmap
                                                // page-alignment
        if (payloadMap == MAP_FAILED) {
            LOG(ERROR) << "Memory mapping failed for fd " << fd.get() << ": " << errno << " "
                       << strerror(errno);
            return std::nullopt;
        }
        for (size_t checksumIndex = 0;
             checksumIndex < chunkPayloadSize / sizeof(transaction_checksum_t); checksumIndex++) {
            checksum ^= payloadMap[checksumIndex];
        }
        if (checksum != 0) {
            LOG(ERROR) << "Checksum failed.";
            return std::nullopt;
            return std::nullopt;
        }
        }
        lseek(fd.get(), chunkPayloadSize, SEEK_CUR);

        switch (chunk.chunkType) {
        switch (chunk.chunkType) {
            case HEADER_CHUNK: {
            case HEADER_CHUNK: {
                if (chunk.dataSize != static_cast<uint32_t>(sizeof(TransactionHeader))) {
                if (chunk.dataSize != static_cast<uint32_t>(sizeof(TransactionHeader))) {
                    LOG(INFO) << "Header Chunk indicated size " << chunk.dataSize << "; Expected "
                    LOG(ERROR) << "Header Chunk indicated size " << chunk.dataSize << "; Expected "
                               << sizeof(TransactionHeader) << ".";
                               << sizeof(TransactionHeader) << ".";
                    return std::nullopt;
                    return std::nullopt;
                }
                }
                if (!android::base::ReadFully(fd, &t.mHeader, chunk.dataSize)) {
                t.mHeader = *reinterpret_cast<TransactionHeader*>(payloadMap);
                    LOG(INFO) << "Failed to read transactionHeader from fd " << fd.get();
                    return std::nullopt;
                }
                lseek(fd.get(), chunk.padding, SEEK_CUR);
                break;
                break;
            }
            }
            case DATA_PARCEL_CHUNK: {
            case DATA_PARCEL_CHUNK: {
                std::vector<uint8_t> bytes;
                if (t.mSent.setData(reinterpret_cast<const unsigned char*>(payloadMap),
                bytes.resize(chunk.dataSize);
                                    chunk.dataSize) != android::NO_ERROR) {
                if (!android::base::ReadFully(fd, bytes.data(), chunk.dataSize)) {
                    LOG(ERROR) << "Failed to set sent parcel data.";
                    LOG(INFO) << "Failed to read sent parcel data from fd " << fd.get();
                    return std::nullopt;
                    return std::nullopt;
                }
                }
                if (t.mSent.setData(bytes.data(), chunk.dataSize) != android::NO_ERROR) {
                    LOG(INFO) << "Failed to set sent parcel data.";
                    return std::nullopt;
                }
                lseek(fd.get(), chunk.padding, SEEK_CUR);
                break;
                break;
            }
            }
            case REPLY_PARCEL_CHUNK: {
            case REPLY_PARCEL_CHUNK: {
                std::vector<uint8_t> bytes;
                if (t.mReply.setData(reinterpret_cast<const unsigned char*>(payloadMap),
                bytes.resize(chunk.dataSize);
                                     chunk.dataSize) != android::NO_ERROR) {
                if (!android::base::ReadFully(fd, bytes.data(), chunk.dataSize)) {
                    LOG(ERROR) << "Failed to set reply parcel data.";
                    LOG(INFO) << "Failed to read reply parcel data from fd " << fd.get();
                    return std::nullopt;
                    return std::nullopt;
                }
                }
                if (t.mReply.setData(bytes.data(), chunk.dataSize) != android::NO_ERROR) {
                    LOG(INFO) << "Failed to set reply parcel data.";
                    return std::nullopt;
                }
                lseek(fd.get(), chunk.padding, SEEK_CUR);
                break;
                break;
            }
            }
            case INVALID_CHUNK:
                LOG(INFO) << "Invalid chunk.";
                return std::nullopt;
            case END_CHUNK:
            case END_CHUNK:
                LOG(INFO) << "Read end chunk";
                FALLTHROUGH_INTENDED;
            default:
                // Unrecognized or skippable chunk
                lseek(fd.get(), chunk.dataSize + chunk.padding, SEEK_CUR);
                break;
                break;
            default:
                LOG(INFO) << "Unrecognized chunk.";
                continue;
        }
        }
    } while (chunk.chunkType != END_CHUNK);
    } while (chunk.chunkType != END_CHUNK);


@@ -230,36 +252,37 @@ std::optional<RecordedTransaction> RecordedTransaction::fromFile(const unique_fd


android::status_t RecordedTransaction::writeChunk(borrowed_fd fd, uint32_t chunkType,
android::status_t RecordedTransaction::writeChunk(borrowed_fd fd, uint32_t chunkType,
                                                  size_t byteCount, const uint8_t* data) const {
                                                  size_t byteCount, const uint8_t* data) const {
    // Write Chunk Descriptor
    if (byteCount > kMaxChunkDataSize) {
    // - Chunk Type
        LOG(ERROR) << "Chunk data exceeds maximum size";
    if (!android::base::WriteFully(fd, &chunkType, sizeof(uint32_t))) {
        return BAD_VALUE;
        LOG(INFO) << "Failed to write chunk header to fd " << fd.get();
    }
        return UNKNOWN_ERROR;
    ChunkDescriptor descriptor = {.chunkType = chunkType,
    }
                                  .dataSize = static_cast<uint32_t>(byteCount)};
    // - Chunk Data Padding Size
    // Prepare Chunk content as byte *
    uint32_t additionalPaddingCount = static_cast<uint32_t>(PADDING8(byteCount));
    const std::byte* descriptorBytes = reinterpret_cast<const std::byte*>(&descriptor);
    if (!android::base::WriteFully(fd, &additionalPaddingCount, sizeof(uint32_t))) {
    const std::byte* dataBytes = reinterpret_cast<const std::byte*>(data);
        LOG(INFO) << "Failed to write chunk padding size to fd " << fd.get();
        return UNKNOWN_ERROR;
    }
    // - Chunk Data Size
    uint64_t byteCountToWrite = (uint64_t)byteCount;
    if (!android::base::WriteFully(fd, &byteCountToWrite, sizeof(uint64_t))) {
        LOG(INFO) << "Failed to write chunk size to fd " << fd.get();
        return UNKNOWN_ERROR;
    }
    if (byteCount == 0) {
        return NO_ERROR;
    }


    if (!android::base::WriteFully(fd, data, byteCount)) {
    // Add Chunk to intermediate buffer, except checksum
        LOG(INFO) << "Failed to write chunk data to fd " << fd.get();
    std::vector<std::byte> buffer;
        return UNKNOWN_ERROR;
    buffer.insert(buffer.end(), descriptorBytes, descriptorBytes + sizeof(ChunkDescriptor));
    buffer.insert(buffer.end(), dataBytes, dataBytes + byteCount);
    std::byte zero{0};
    buffer.insert(buffer.end(), PADDING8(byteCount), zero);

    // Calculate checksum from buffer
    transaction_checksum_t* checksumData = reinterpret_cast<transaction_checksum_t*>(buffer.data());
    transaction_checksum_t checksumValue = 0;
    for (size_t idx = 0; idx < (buffer.size() / sizeof(transaction_checksum_t)); idx++) {
        checksumValue ^= checksumData[idx];
    }
    }


    const uint8_t zeros[7] = {0};
    // Write checksum to buffer
    if (!android::base::WriteFully(fd, zeros, additionalPaddingCount)) {
    std::byte* checksumBytes = reinterpret_cast<std::byte*>(&checksumValue);
        LOG(INFO) << "Failed to write chunk padding to fd " << fd.get();
    buffer.insert(buffer.end(), checksumBytes, checksumBytes + sizeof(transaction_checksum_t));

    // Write buffer to file
    if (!android::base::WriteFully(fd, buffer.data(), buffer.size())) {
        LOG(ERROR) << "Failed to write chunk fd " << fd.get();
        return UNKNOWN_ERROR;
        return UNKNOWN_ERROR;
    }
    }
    return NO_ERROR;
    return NO_ERROR;
@@ -269,19 +292,19 @@ android::status_t RecordedTransaction::dumpToFile(const unique_fd& fd) const {
    if (NO_ERROR !=
    if (NO_ERROR !=
        writeChunk(fd, HEADER_CHUNK, sizeof(TransactionHeader),
        writeChunk(fd, HEADER_CHUNK, sizeof(TransactionHeader),
                   reinterpret_cast<const uint8_t*>(&mHeader))) {
                   reinterpret_cast<const uint8_t*>(&mHeader))) {
        LOG(INFO) << "Failed to write transactionHeader to fd " << fd.get();
        LOG(ERROR) << "Failed to write transactionHeader to fd " << fd.get();
        return UNKNOWN_ERROR;
        return UNKNOWN_ERROR;
    }
    }
    if (NO_ERROR != writeChunk(fd, DATA_PARCEL_CHUNK, mSent.dataSize(), mSent.data())) {
    if (NO_ERROR != writeChunk(fd, DATA_PARCEL_CHUNK, mSent.dataSize(), mSent.data())) {
        LOG(INFO) << "Failed to write sent Parcel to fd " << fd.get();
        LOG(ERROR) << "Failed to write sent Parcel to fd " << fd.get();
        return UNKNOWN_ERROR;
        return UNKNOWN_ERROR;
    }
    }
    if (NO_ERROR != writeChunk(fd, REPLY_PARCEL_CHUNK, mReply.dataSize(), mReply.data())) {
    if (NO_ERROR != writeChunk(fd, REPLY_PARCEL_CHUNK, mReply.dataSize(), mReply.data())) {
        LOG(INFO) << "Failed to write reply Parcel to fd " << fd.get();
        LOG(ERROR) << "Failed to write reply Parcel to fd " << fd.get();
        return UNKNOWN_ERROR;
        return UNKNOWN_ERROR;
    }
    }
    if (NO_ERROR != writeChunk(fd, END_CHUNK, 0, NULL)) {
    if (NO_ERROR != writeChunk(fd, END_CHUNK, 0, NULL)) {
        LOG(INFO) << "Failed to write end chunk to fd " << fd.get();
        LOG(ERROR) << "Failed to write end chunk to fd " << fd.get();
        return UNKNOWN_ERROR;
        return UNKNOWN_ERROR;
    }
    }
    return NO_ERROR;
    return NO_ERROR;
+78 −0
Original line number Original line Diff line number Diff line
@@ -16,6 +16,7 @@


#include <binder/BinderRecordReplay.h>
#include <binder/BinderRecordReplay.h>
#include <gtest/gtest.h>
#include <gtest/gtest.h>
#include <utils/Errors.h>


using android::Parcel;
using android::Parcel;
using android::status_t;
using android::status_t;
@@ -54,3 +55,80 @@ TEST(BinderRecordedTransaction, RoundTripEncoding) {
    EXPECT_EQ(retrievedTransaction->getDataParcel().readInt64(), 2);
    EXPECT_EQ(retrievedTransaction->getDataParcel().readInt64(), 2);
    EXPECT_EQ(retrievedTransaction->getReplyParcel().readInt32(), 99);
    EXPECT_EQ(retrievedTransaction->getReplyParcel().readInt32(), 99);
}
}

TEST(BinderRecordedTransaction, Checksum) {
    Parcel d;
    d.writeInt32(12);
    d.writeInt64(2);
    Parcel r;
    r.writeInt32(99);
    timespec ts = {1232456, 567890};
    auto transaction = RecordedTransaction::fromDetails(1, 42, ts, d, r, 0);

    auto file = std::tmpfile();
    auto fd = unique_fd(fcntl(fileno(file), F_DUPFD, 1));

    status_t status = transaction->dumpToFile(fd);
    ASSERT_EQ(android::NO_ERROR, status);

    lseek(fd.get(), 9, SEEK_SET);
    uint32_t badData = 0xffffffff;
    write(fd.get(), &badData, sizeof(uint32_t));
    std::rewind(file);

    auto retrievedTransaction = RecordedTransaction::fromFile(fd);

    EXPECT_FALSE(retrievedTransaction.has_value());
}

TEST(BinderRecordedTransaction, PayloadsExceedPageBoundaries) {
    // File contents are read with mmap.
    // This test verifies that transactions are read from portions
    // of files that cross page boundaries and don't start at a
    // page boundary offset of the fd.
    const size_t pageSize = sysconf(_SC_PAGE_SIZE);
    const size_t largeDataSize = pageSize + 100;
    std::vector<uint8_t> largePayload;
    uint8_t filler = 0xaa;
    largePayload.insert(largePayload.end(), largeDataSize, filler);
    Parcel d;
    d.writeInt32(12);
    d.writeInt64(2);
    d.writeByteVector(largePayload);
    Parcel r;
    r.writeInt32(99);
    timespec ts = {1232456, 567890};
    auto transaction = RecordedTransaction::fromDetails(1, 42, ts, d, r, 0);

    auto file = std::tmpfile();
    auto fd = unique_fd(fcntl(fileno(file), F_DUPFD, 1));

    // Write to file twice
    status_t status = transaction->dumpToFile(fd);
    ASSERT_EQ(android::NO_ERROR, status);
    status = transaction->dumpToFile(fd);
    ASSERT_EQ(android::NO_ERROR, status);

    std::rewind(file);

    for (int i = 0; i < 2; i++) {
        auto retrievedTransaction = RecordedTransaction::fromFile(fd);

        EXPECT_EQ(retrievedTransaction->getCode(), 1);
        EXPECT_EQ(retrievedTransaction->getFlags(), 42);
        EXPECT_EQ(retrievedTransaction->getTimestamp().tv_sec, ts.tv_sec);
        EXPECT_EQ(retrievedTransaction->getTimestamp().tv_nsec, ts.tv_nsec);
        EXPECT_EQ(retrievedTransaction->getDataParcel().dataSize(), d.dataSize());
        EXPECT_EQ(retrievedTransaction->getReplyParcel().dataSize(), 4);
        EXPECT_EQ(retrievedTransaction->getReturnedStatus(), 0);
        EXPECT_EQ(retrievedTransaction->getVersion(), 0);

        EXPECT_EQ(retrievedTransaction->getDataParcel().readInt32(), 12);
        EXPECT_EQ(retrievedTransaction->getDataParcel().readInt64(), 2);
        std::optional<std::vector<uint8_t>> payloadOut;
        EXPECT_EQ(retrievedTransaction->getDataParcel().readByteVector(&payloadOut), android::OK);
        EXPECT_EQ(payloadOut.value(), largePayload);

        EXPECT_EQ(retrievedTransaction->getReplyParcel().readInt32(), 99);
    }
}