From 2ec418a4c98f6e8f95395456e1ad4c2956cac007 Mon Sep 17 00:00:00 2001 From: David Pursell Date: Wed, 20 Jan 2016 08:32:08 -0800 Subject: [PATCH] fastboot: add TCP protocol. This CL implements a TCP protocol for use with fastboot. Protocol description is given in fastboot_protocol.txt, some examples of expected behavior can also be found in tcp_test.cpp. Usage is: fastboot -s tcp:[:port] Bug: http://b/26558551 Change-Id: If53a514a534489c617db32c4fea8819949121282 --- fastboot/Android.mk | 3 + fastboot/fastboot.cpp | 68 ++++++++--- fastboot/fastboot_protocol.txt | 61 +++++++-- fastboot/tcp.cpp | 196 +++++++++++++++++++++++++++++ fastboot/tcp.h | 59 +++++++++ fastboot/tcp_test.cpp | 217 +++++++++++++++++++++++++++++++++ 6 files changed, 583 insertions(+), 21 deletions(-) create mode 100644 fastboot/tcp.cpp create mode 100644 fastboot/tcp.h create mode 100644 fastboot/tcp_test.cpp diff --git a/fastboot/Android.mk b/fastboot/Android.mk index 11d769bad..e0f7c73f3 100644 --- a/fastboot/Android.mk +++ b/fastboot/Android.mk @@ -31,6 +31,7 @@ LOCAL_SRC_FILES := \ fs.cpp\ protocol.cpp \ socket.cpp \ + tcp.cpp \ util.cpp \ LOCAL_MODULE := fastboot @@ -111,6 +112,8 @@ LOCAL_SRC_FILES := \ socket.cpp \ socket_mock.cpp \ socket_test.cpp \ + tcp.cpp \ + tcp_test.cpp \ LOCAL_STATIC_LIBRARIES := libbase libcutils diff --git a/fastboot/fastboot.cpp b/fastboot/fastboot.cpp index 4573da052..b21956446 100644 --- a/fastboot/fastboot.cpp +++ b/fastboot/fastboot.cpp @@ -42,22 +42,22 @@ #include #include #include + #include #include #include #include +#include #include #include #include -#include -#include - #include "bootimg_utils.h" #include "diagnose_usb.h" #include "fastboot.h" #include "fs.h" +#include "tcp.h" #include "transport.h" #include "usb.h" @@ -69,9 +69,9 @@ char cur_product[FB_RESPONSE_SZ + 1]; -static const char *serial = 0; -static const char *product = 0; -static const char *cmdline = 0; +static const char* serial = nullptr; +static const char* product = nullptr; +static const char* cmdline = nullptr; static unsigned short vendor_id = 0; static int long_listing = 0; static int64_t sparse_limit = -1; @@ -227,17 +227,51 @@ static int list_devices_callback(usb_ifc_info* info) { return -1; } +// Opens a new Transport connected to a device. If |serial| is non-null it will be used to identify +// a specific device, otherwise the first USB device found will be used. +// +// If |serial| is non-null but invalid, this prints an error message to stderr and returns nullptr. +// Otherwise it blocks until the target is available. +// +// The returned Transport is a singleton, so multiple calls to this function will return the same +// object, and the caller should not attempt to delete the returned Transport. static Transport* open_device() { static Transport* transport = nullptr; - int announce = 1; + bool announce = true; - if (transport) return transport; + if (transport != nullptr) { + return transport; + } + + std::string host; + int port = tcp::kDefaultPort; + if (serial != nullptr && android::base::StartsWith(serial, "tcp:")) { + std::string error; + const char* address = serial + strlen("tcp:"); + + if (!android::base::ParseNetAddress(address, &host, &port, nullptr, &error)) { + fprintf(stderr, "error: Invalid network address '%s': %s\n", address, error.c_str()); + return nullptr; + } + } + + while (true) { + if (!host.empty()) { + std::string error; + transport = tcp::Connect(host, port, &error).release(); + if (transport == nullptr && announce) { + fprintf(stderr, "error: %s\n", error.c_str()); + } + } else { + transport = usb_open(match_fastboot); + } + + if (transport != nullptr) { + return transport; + } - for (;;) { - transport = usb_open(match_fastboot); - if (transport) return transport; if (announce) { - announce = 0; + announce = false; fprintf(stderr, "< waiting for %s >\n", serial ? serial : "any device"); } usleep(1000); @@ -299,8 +333,10 @@ static void usage() { " if supported by partition type).\n" " -u Do not erase partition before\n" " formatting.\n" - " -s Specify device serial number\n" - " or path to device port.\n" + " -s Specify a device. For USB, provide either\n" + " a serial number or path to device port.\n" + " For TCP, provide an address in the form\n" + " tcp:[:port].\n" " -p Specify product name.\n" " -c Override kernel commandline.\n" " -i Specify a custom USB vendor id.\n" @@ -1263,6 +1299,10 @@ int main(int argc, char **argv) } Transport* transport = open_device(); + if (transport == nullptr) { + return 1; + } + if (slot_override != "") slot_override = verify_slot(transport, slot_override.c_str()); if (next_active != "") diff --git a/fastboot/fastboot_protocol.txt b/fastboot/fastboot_protocol.txt index bb73d8a84..358a44882 100644 --- a/fastboot/fastboot_protocol.txt +++ b/fastboot/fastboot_protocol.txt @@ -3,19 +3,25 @@ FastBoot Version 0.4 ---------------------- The fastboot protocol is a mechanism for communicating with bootloaders -over USB. It is designed to be very straightforward to implement, to -allow it to be used across a wide range of devices and from hosts running +over USB or ethernet. It is designed to be very straightforward to implement, +to allow it to be used across a wide range of devices and from hosts running Linux, Windows, or OSX. Basic Requirements ------------------ -* Two bulk endpoints (in, out) are required -* Max packet size must be 64 bytes for full-speed, 512 bytes for - high-speed and 1024 bytes for Super Speed USB. -* The protocol is entirely host-driven and synchronous (unlike the - multi-channel, bi-directional, asynchronous ADB protocol) +* USB + * Two bulk endpoints (in, out) are required + * Max packet size must be 64 bytes for full-speed, 512 bytes for + high-speed and 1024 bytes for Super Speed USB. + * The protocol is entirely host-driven and synchronous (unlike the + multi-channel, bi-directional, asynchronous ADB protocol) + +* TCP + * Device must be reachable via IP. + * Device will act as the TCP server, fastboot will be the client. + * Fastboot data is wrapped in a simple protocol; see below for details. Transport and Framing @@ -171,3 +177,44 @@ specification. OEM-specific names should not start with lowercase characters. +TCP Protocol v1 +--------------- + +The TCP protocol is designed to be a simple way to use the fastboot protocol +over ethernet if USB is not available. + +The device will open a TCP server on port 5554 and wait for a fastboot client +to connect. + +-- Handshake -- +Upon connecting, both sides will send a 4-byte handshake message to ensure they +are speaking the same protocol. This consists of the ASCII characters "FB" +followed by a 2-digit base-10 ASCII version number. For example, the version 1 +handshake message will be [FB01]. + +If either side detects a malformed handshake, it should disconnect. + +The protocol version to use must be the minimum of the versions sent by each +side; if either side cannot speak this protocol version, it should disconnect. + +-- Fastboot Data -- +Once the handshake is complete, fastboot data will be sent as follows: + + [data_size][data] + +Where data_size is an unsigned 8-byte big-endian binary value, and data is the +fastboot packet. The 8-byte length is intended to provide future-proofing even +though currently fastboot packets have a 4-byte maximum length. + +-- Example -- +In this example the fastboot host queries the device for two variables, +"version" and "none". + +Host +Host FB01 +Device FB01 +Host [0x00][0x00][0x00][0x00][0x00][0x00][0x00][0x0E]getvar:version +Device [0x00][0x00][0x00][0x00][0x00][0x00][0x00][0x07]OKAY0.4 +Host [0x00][0x00][0x00][0x00][0x00][0x00][0x00][0x0B]getvar:none +Device [0x00][0x00][0x00][0x00][0x00][0x00][0x00][0x04]OKAY +Host diff --git a/fastboot/tcp.cpp b/fastboot/tcp.cpp new file mode 100644 index 000000000..da2880a5e --- /dev/null +++ b/fastboot/tcp.cpp @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#include "tcp.h" + +#include + +namespace tcp { + +static constexpr int kProtocolVersion = 1; +static constexpr size_t kHandshakeLength = 4; +static constexpr int kHandshakeTimeoutMs = 2000; + +// Extract the big-endian 8-byte message length into a 64-bit number. +static uint64_t ExtractMessageLength(const void* buffer) { + uint64_t ret = 0; + for (int i = 0; i < 8; ++i) { + ret |= uint64_t{reinterpret_cast(buffer)[i]} << (56 - i * 8); + } + return ret; +} + +// Encode the 64-bit number into a big-endian 8-byte message length. +static void EncodeMessageLength(uint64_t length, void* buffer) { + for (int i = 0; i < 8; ++i) { + reinterpret_cast(buffer)[i] = length >> (56 - i * 8); + } +} + +class TcpTransport : public Transport { + public: + // Factory function so we can return nullptr if initialization fails. + static std::unique_ptr NewTransport(std::unique_ptr socket, + std::string* error); + + ~TcpTransport() override = default; + + ssize_t Read(void* data, size_t length) override; + ssize_t Write(const void* data, size_t length) override; + int Close() override; + + private: + TcpTransport(std::unique_ptr sock) : socket_(std::move(sock)) {} + + // Connects to the device and performs the initial handshake. Returns false and fills |error| + // on failure. + bool InitializeProtocol(std::string* error); + + std::unique_ptr socket_; + uint64_t message_bytes_left_ = 0; + + DISALLOW_COPY_AND_ASSIGN(TcpTransport); +}; + +std::unique_ptr TcpTransport::NewTransport(std::unique_ptr socket, + std::string* error) { + std::unique_ptr transport(new TcpTransport(std::move(socket))); + + if (!transport->InitializeProtocol(error)) { + return nullptr; + } + + return transport; +} + +// These error strings are checked in tcp_test.cpp and should be kept in sync. +bool TcpTransport::InitializeProtocol(std::string* error) { + std::string handshake_message(android::base::StringPrintf("FB%02d", kProtocolVersion)); + + if (!socket_->Send(handshake_message.c_str(), kHandshakeLength)) { + *error = android::base::StringPrintf("Failed to send initialization message (%s)", + Socket::GetErrorMessage().c_str()); + return false; + } + + char buffer[kHandshakeLength]; + if (socket_->ReceiveAll(buffer, kHandshakeLength, kHandshakeTimeoutMs) != kHandshakeLength) { + *error = android::base::StringPrintf( + "No initialization message received (%s). Target may not support TCP fastboot", + Socket::GetErrorMessage().c_str()); + return false; + } + + if (memcmp(buffer, "FB", 2) != 0) { + *error = "Unrecognized initialization message. Target may not support TCP fastboot"; + return false; + } + + if (memcmp(buffer + 2, "01", 2) != 0) { + *error = android::base::StringPrintf("Unknown TCP protocol version %s (host version %02d)", + std::string(buffer + 2, 2).c_str(), kProtocolVersion); + return false; + } + + error->clear(); + return true; +} + +ssize_t TcpTransport::Read(void* data, size_t length) { + if (socket_ == nullptr) { + return -1; + } + + // Unless we're mid-message, read the next 8-byte message length. + if (message_bytes_left_ == 0) { + char buffer[8]; + if (socket_->ReceiveAll(buffer, 8, 0) != 8) { + Close(); + return -1; + } + message_bytes_left_ = ExtractMessageLength(buffer); + } + + // Now read the message (up to |length| bytes). + if (length > message_bytes_left_) { + length = message_bytes_left_; + } + ssize_t bytes_read = socket_->ReceiveAll(data, length, 0); + if (bytes_read == -1) { + Close(); + } else { + message_bytes_left_ -= bytes_read; + } + return bytes_read; +} + +ssize_t TcpTransport::Write(const void* data, size_t length) { + if (socket_ == nullptr) { + return -1; + } + + // Use multi-buffer writes for better performance. + char header[8]; + EncodeMessageLength(length, header); + if (!socket_->Send(std::vector{{header, 8}, {data, length}})) { + Close(); + return -1; + } + + return length; +} + +int TcpTransport::Close() { + if (socket_ == nullptr) { + return 0; + } + + int result = socket_->Close(); + socket_.reset(); + return result; +} + +std::unique_ptr Connect(const std::string& hostname, int port, std::string* error) { + return internal::Connect(Socket::NewClient(Socket::Protocol::kTcp, hostname, port, error), + error); +} + +namespace internal { + +std::unique_ptr Connect(std::unique_ptr sock, std::string* error) { + if (sock == nullptr) { + // If Socket creation failed |error| is already set. + return nullptr; + } + + return TcpTransport::NewTransport(std::move(sock), error); +} + +} // namespace internal + +} // namespace tcp diff --git a/fastboot/tcp.h b/fastboot/tcp.h new file mode 100644 index 000000000..aa3ef13d9 --- /dev/null +++ b/fastboot/tcp.h @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#ifndef TCP_H_ +#define TCP_H_ + +#include +#include + +#include + +#include "socket.h" +#include "transport.h" + +namespace tcp { + +constexpr int kDefaultPort = 5554; + +// Returns a newly allocated Transport object connected to |hostname|:|port|. On failure, |error| is +// filled and nullptr is returned. +std::unique_ptr Connect(const std::string& hostname, int port, std::string* error); + +// Internal namespace for test use only. +namespace internal { + +// Creates a TCP Transport object but using a given Socket instead of connecting to a hostname. +// Used for unit tests to create a Transport object that uses a SocketMock. +std::unique_ptr Connect(std::unique_ptr sock, std::string* error); + +} // namespace internal + +} // namespace tcp + +#endif // TCP_H_ diff --git a/fastboot/tcp_test.cpp b/fastboot/tcp_test.cpp new file mode 100644 index 000000000..7d80d76f5 --- /dev/null +++ b/fastboot/tcp_test.cpp @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#include "tcp.h" + +#include + +#include "socket_mock.h" + +TEST(TcpConnectTest, TestSuccess) { + std::unique_ptr mock(new SocketMock); + mock->ExpectSend("FB01"); + mock->AddReceive("FB01"); + + std::string error; + EXPECT_NE(nullptr, tcp::internal::Connect(std::move(mock), &error)); + EXPECT_EQ("", error); +} + +TEST(TcpConnectTest, TestSendFailure) { + std::unique_ptr mock(new SocketMock); + mock->ExpectSendFailure("FB01"); + + std::string error; + EXPECT_EQ(nullptr, tcp::internal::Connect(std::move(mock), &error)); + EXPECT_NE(std::string::npos, error.find("Failed to send initialization message")); +} + +TEST(TcpConnectTest, TestNoResponseFailure) { + std::unique_ptr mock(new SocketMock); + mock->ExpectSend("FB01"); + mock->AddReceiveFailure(); + + std::string error; + EXPECT_EQ(nullptr, tcp::internal::Connect(std::move(mock), &error)); + EXPECT_NE(std::string::npos, error.find("No initialization message received")); +} + +TEST(TcpConnectTest, TestBadResponseFailure) { + std::unique_ptr mock(new SocketMock); + mock->ExpectSend("FB01"); + mock->AddReceive("XX01"); + + std::string error; + EXPECT_EQ(nullptr, tcp::internal::Connect(std::move(mock), &error)); + EXPECT_NE(std::string::npos, error.find("Unrecognized initialization message")); +} + +TEST(TcpConnectTest, TestUnknownVersionFailure) { + std::unique_ptr mock(new SocketMock); + mock->ExpectSend("FB01"); + mock->AddReceive("FB02"); + + std::string error; + EXPECT_EQ(nullptr, tcp::internal::Connect(std::move(mock), &error)); + EXPECT_EQ("Unknown TCP protocol version 02 (host version 01)", error); +} + +// Fixture to configure a SocketMock for a successful TCP connection. +class TcpTest : public ::testing::Test { + protected: + void SetUp() override { + mock_ = new SocketMock; + mock_->ExpectSend("FB01"); + mock_->AddReceive("FB01"); + + std::string error; + transport_ = tcp::internal::Connect(std::unique_ptr(mock_), &error); + ASSERT_NE(nullptr, transport_); + ASSERT_EQ("", error); + }; + + // Writes |message| to |transport_|, returns true on success. + bool Write(const std::string& message) { + return transport_->Write(message.data(), message.length()) == + static_cast(message.length()); + } + + // Reads from |transport_|, returns true if it matches |message|. + bool Read(const std::string& message) { + std::string buffer(message.length(), '\0'); + return transport_->Read(&buffer[0], buffer.length()) == + static_cast(message.length()) && + buffer == message; + } + + // Use a raw SocketMock* here because we pass ownership to the Transport object, but we still + // need access to configure mock expectations. + SocketMock* mock_ = nullptr; + std::unique_ptr transport_; +}; + +TEST_F(TcpTest, TestWriteSuccess) { + mock_->ExpectSend(std::string{0, 0, 0, 0, 0, 0, 0, 3} + "foo"); + + EXPECT_TRUE(Write("foo")); +} + +TEST_F(TcpTest, TestReadSuccess) { + mock_->AddReceive(std::string{0, 0, 0, 0, 0, 0, 0, 3}); + mock_->AddReceive("foo"); + + EXPECT_TRUE(Read("foo")); +} + +// Tests that fragmented TCP reads are handled properly. +TEST_F(TcpTest, TestReadFragmentSuccess) { + mock_->AddReceive(std::string{0, 0, 0, 0}); + mock_->AddReceive(std::string{0, 0, 0, 3}); + mock_->AddReceive("f"); + mock_->AddReceive("o"); + mock_->AddReceive("o"); + + EXPECT_TRUE(Read("foo")); +} + +TEST_F(TcpTest, TestLargeWriteSuccess) { + // 0x100000 = 1MiB. + std::string data(0x100000, '\0'); + for (size_t i = 0; i < data.length(); ++i) { + data[i] = i; + } + mock_->ExpectSend(std::string{0, 0, 0, 0, 0, 0x10, 0, 0} + data); + + EXPECT_TRUE(Write(data)); +} + +TEST_F(TcpTest, TestLargeReadSuccess) { + // 0x100000 = 1MiB. + std::string data(0x100000, '\0'); + for (size_t i = 0; i < data.length(); ++i) { + data[i] = i; + } + mock_->AddReceive(std::string{0, 0, 0, 0, 0, 0x10, 0, 0}); + mock_->AddReceive(data); + + EXPECT_TRUE(Read(data)); +} + +// Tests a few sample fastboot protocol commands. +TEST_F(TcpTest, TestFastbootProtocolSuccess) { + mock_->ExpectSend(std::string{0, 0, 0, 0, 0, 0, 0, 14} + "getvar:version"); + mock_->AddReceive(std::string{0, 0, 0, 0, 0, 0, 0, 7}); + mock_->AddReceive("OKAY0.4"); + + mock_->ExpectSend(std::string{0, 0, 0, 0, 0, 0, 0, 10} + "getvar:all"); + mock_->AddReceive(std::string{0, 0, 0, 0, 0, 0, 0, 16}); + mock_->AddReceive("INFOversion: 0.4"); + mock_->AddReceive(std::string{0, 0, 0, 0, 0, 0, 0, 12}); + mock_->AddReceive("INFOfoo: bar"); + mock_->AddReceive(std::string{0, 0, 0, 0, 0, 0, 0, 4}); + mock_->AddReceive("OKAY"); + + EXPECT_TRUE(Write("getvar:version")); + EXPECT_TRUE(Read("OKAY0.4")); + + EXPECT_TRUE(Write("getvar:all")); + EXPECT_TRUE(Read("INFOversion: 0.4")); + EXPECT_TRUE(Read("INFOfoo: bar")); + EXPECT_TRUE(Read("OKAY")); +} + +TEST_F(TcpTest, TestReadLengthFailure) { + mock_->AddReceiveFailure(); + + char buffer[16]; + EXPECT_EQ(-1, transport_->Read(buffer, sizeof(buffer))); +} + +TEST_F(TcpTest, TestReadDataFailure) { + mock_->AddReceive(std::string{0, 0, 0, 0, 0, 0, 0, 3}); + mock_->AddReceiveFailure(); + + char buffer[16]; + EXPECT_EQ(-1, transport_->Read(buffer, sizeof(buffer))); +} + +TEST_F(TcpTest, TestWriteFailure) { + mock_->ExpectSendFailure(std::string{0, 0, 0, 0, 0, 0, 0, 3} + "foo"); + + EXPECT_EQ(-1, transport_->Write("foo", 3)); +} + +TEST_F(TcpTest, TestTransportClose) { + EXPECT_EQ(0, transport_->Close()); + + // After closing, Transport Read()/Write() should return -1 without actually attempting any + // network operations. + char buffer[16]; + EXPECT_EQ(-1, transport_->Read(buffer, sizeof(buffer))); + EXPECT_EQ(-1, transport_->Write("foo", 3)); +}