// Copyright 2014 The Crashpad Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "util/mach/child_port_handshake.h"

#include "base/apple/scoped_mach_port.h"
#include "gtest/gtest.h"
#include "test/multiprocess.h"
#include "util/mach/child_port_types.h"
#include "util/mach/mach_extensions.h"

namespace crashpad {
namespace test {
namespace {

class ChildPortHandshakeTest : public Multiprocess {
 public:
  enum class ClientProcess {
    // The child runs the client and the parent runs the server.
    kChildClient = 0,

    // The parent runs the client and the child runs the server.
    kParentClient,
  };

  enum class TestType {
    // The client checks in with the server, transferring a receive right.
    kClientChecksIn_ReceiveRight = 0,

    // In this test, the client checks in with the server normally. It sends a
    // copy of its bootstrap port to the server, because both parent and child
    // should have the same bootstrap port, allowing for verification.
    kClientChecksIn_SendRight,

    // The client checks in with the server, transferring a send-once right.
    kClientChecksIn_SendOnceRight,

    // In this test, the client reads from its pipe, and subsequently exits
    // without checking in. This tests that the server properly detects that it
    // has lost its client after sending instructions to it via the pipe, while
    // waiting for a check-in message.
    kClientDoesNotCheckIn,

    // In this test, the client exits without checking in. This tests that the
    // server properly detects that it has lost a client. Whether or not the
    // client closes the pipe before the server writes to it is a race, and the
    // server needs to be able to detect client loss in both cases, so the
    // ClientDoesNotCheckIn_ReadsPipe and NoClient tests also exist to test
    // these individual cases more deterministically.
    kClientDoesNotCheckIn_ReadsPipe,

    // In this test, the client checks in with the server with an incorrect
    // token value and a copy of its own task port. The server should reject the
    // message because of the invalid token, and return MACH_PORT_NULL to its
    // caller.
    kTokenIncorrect,

    // In this test, the client checks in with the server with an incorrect
    // token value and a copy of its own task port, and subsequently, the
    // correct token value and a copy of its bootstrap port. The server should
    // reject the first because of the invalid token, but it should continue
    // waiting for a message with a valid token as long as the pipe remains
    // open. It should wind wind up returning the bootstrap port, allowing for
    // verification.
    kTokenIncorrectThenCorrect,

    // The server dies. The failure should be reported in the client. This test
    // type is only compatible with ClientProcess::kParentClient.
    kServerDies,
  };

  ChildPortHandshakeTest(ClientProcess client_process, TestType test_type)
      : Multiprocess(),
        child_port_handshake_(),
        client_process_(client_process),
        test_type_(test_type) {
  }

  ChildPortHandshakeTest(const ChildPortHandshakeTest&) = delete;
  ChildPortHandshakeTest& operator=(const ChildPortHandshakeTest&) = delete;

  ~ChildPortHandshakeTest() {
  }

 private:
  void RunServer() {
    if (test_type_ == TestType::kServerDies) {
      return;
    }

    base::apple::ScopedMachReceiveRight receive_right;
    base::apple::ScopedMachSendRight send_right;
    if (test_type_ == TestType::kClientChecksIn_ReceiveRight) {
      receive_right.reset(child_port_handshake_.RunServer(
          ChildPortHandshake::PortRightType::kReceiveRight));
    } else {
      send_right.reset(child_port_handshake_.RunServer(
          ChildPortHandshake::PortRightType::kSendRight));
    }

    switch (test_type_) {
      case TestType::kClientChecksIn_ReceiveRight:
        EXPECT_TRUE(receive_right.is_valid());
        break;

      case TestType::kClientChecksIn_SendRight:
      case TestType::kTokenIncorrectThenCorrect:
        EXPECT_EQ(send_right, bootstrap_port);
        break;

      case TestType::kClientChecksIn_SendOnceRight:
        EXPECT_TRUE(send_right.is_valid());
        EXPECT_NE(send_right, bootstrap_port);
        break;

      case TestType::kClientDoesNotCheckIn:
      case TestType::kClientDoesNotCheckIn_ReadsPipe:
      case TestType::kTokenIncorrect:
        EXPECT_FALSE(send_right.is_valid());
        break;

      case TestType::kServerDies:
        // This was special-cased as an early return above.
        FAIL();
    }
  }

  void RunClient() {
    switch (test_type_) {
      case TestType::kClientChecksIn_SendRight: {
        ASSERT_TRUE(child_port_handshake_.RunClient(bootstrap_port,
                                                    MACH_MSG_TYPE_COPY_SEND));
        break;
      }

      case TestType::kClientChecksIn_ReceiveRight: {
        mach_port_t receive_right = NewMachPort(MACH_PORT_RIGHT_RECEIVE);
        ASSERT_TRUE(child_port_handshake_.RunClient(
              receive_right, MACH_MSG_TYPE_MOVE_RECEIVE));
        break;
      }

      case TestType::kClientChecksIn_SendOnceRight: {
        base::apple::ScopedMachReceiveRight receive_right(
            NewMachPort(MACH_PORT_RIGHT_RECEIVE));
        ASSERT_TRUE(child_port_handshake_.RunClient(
            receive_right.get(), MACH_MSG_TYPE_MAKE_SEND_ONCE));
        break;
      }

      case TestType::kClientDoesNotCheckIn: {
        child_port_handshake_.ServerWriteFD().reset();
        child_port_handshake_.ClientReadFD().reset();
        break;
      }

      case TestType::kClientDoesNotCheckIn_ReadsPipe: {
        // Don’t run the standard client routine. Instead, drain the pipe, which
        // will get the parent to the point that it begins waiting for a
        // check-in message. Then, exit. The pipe is drained using the same
        // implementation that the real client would use.
        child_port_handshake_.ServerWriteFD().reset();
        base::ScopedFD client_read_fd = child_port_handshake_.ClientReadFD();
        child_port_token_t token;
        std::string service_name;
        ASSERT_TRUE(ChildPortHandshake::RunClientInternal_ReadPipe(
            client_read_fd.get(), &token, &service_name));
        break;
      }

      case TestType::kTokenIncorrect: {
        // Don’t run the standard client routine. Instead, read the token and
        // service name, mutate the token, and then check in with the bad token.
        // The parent should reject the message.
        child_port_handshake_.ServerWriteFD().reset();
        base::ScopedFD client_read_fd = child_port_handshake_.ClientReadFD();
        child_port_token_t token;
        std::string service_name;
        ASSERT_TRUE(ChildPortHandshake::RunClientInternal_ReadPipe(
            client_read_fd.get(), &token, &service_name));
        child_port_token_t bad_token = ~token;
        ASSERT_TRUE(ChildPortHandshake::RunClientInternal_SendCheckIn(
            service_name,
            bad_token,
            mach_task_self(),
            MACH_MSG_TYPE_COPY_SEND));
        break;
      }

      case TestType::kTokenIncorrectThenCorrect: {
        // Don’t run the standard client routine. Instead, read the token and
        // service name. Mutate the token, and check in with the bad token,
        // expecting the parent to reject the message. Then, check in with the
        // correct token, expecting the parent to accept it.
        child_port_handshake_.ServerWriteFD().reset();
        base::ScopedFD client_read_fd = child_port_handshake_.ClientReadFD();
        child_port_token_t token;
        std::string service_name;
        ASSERT_TRUE(ChildPortHandshake::RunClientInternal_ReadPipe(
            client_read_fd.release(), &token, &service_name));
        child_port_token_t bad_token = ~token;
        ASSERT_TRUE(ChildPortHandshake::RunClientInternal_SendCheckIn(
            service_name,
            bad_token,
            mach_task_self(),
            MACH_MSG_TYPE_COPY_SEND));
        ASSERT_TRUE(ChildPortHandshake::RunClientInternal_SendCheckIn(
            service_name, token, bootstrap_port, MACH_MSG_TYPE_COPY_SEND));
        break;
      }

      case TestType::kServerDies: {
        ASSERT_EQ(client_process_, ClientProcess::kParentClient);
        ASSERT_FALSE(child_port_handshake_.RunClient(bootstrap_port,
                                                     MACH_MSG_TYPE_COPY_SEND));
        break;
      }
    }
  }

  // Multiprocess:

  void MultiprocessParent() override {
    switch (client_process_) {
      case ClientProcess::kChildClient:
        RunServer();
        break;
      case ClientProcess::kParentClient:
        RunClient();
        break;
    }
  }

  void MultiprocessChild() override {
    switch (client_process_) {
      case ClientProcess::kChildClient:
        RunClient();
        break;
      case ClientProcess::kParentClient:
        RunServer();
        break;
    }
  }

 private:
  ChildPortHandshake child_port_handshake_;
  ClientProcess client_process_;
  TestType test_type_;
};

TEST(ChildPortHandshake, ChildClientChecksIn_ReceiveRight) {
  ChildPortHandshakeTest test(
      ChildPortHandshakeTest::ClientProcess::kChildClient,
      ChildPortHandshakeTest::TestType::kClientChecksIn_ReceiveRight);
  test.Run();
}

TEST(ChildPortHandshake, ChildClientChecksIn_SendRight) {
  ChildPortHandshakeTest test(
      ChildPortHandshakeTest::ClientProcess::kChildClient,
      ChildPortHandshakeTest::TestType::kClientChecksIn_SendRight);
  test.Run();
}

TEST(ChildPortHandshake, ChildClientChecksIn_SendOnceRight) {
  ChildPortHandshakeTest test(
      ChildPortHandshakeTest::ClientProcess::kChildClient,
      ChildPortHandshakeTest::TestType::kClientChecksIn_SendOnceRight);
  test.Run();
}

TEST(ChildPortHandshake, ChildClientDoesNotCheckIn) {
  ChildPortHandshakeTest test(
      ChildPortHandshakeTest::ClientProcess::kChildClient,
      ChildPortHandshakeTest::TestType::kClientDoesNotCheckIn);
  test.Run();
}

TEST(ChildPortHandshake, ChildClientDoesNotCheckIn_ReadsPipe) {
  ChildPortHandshakeTest test(
      ChildPortHandshakeTest::ClientProcess::kChildClient,
      ChildPortHandshakeTest::TestType::kClientDoesNotCheckIn_ReadsPipe);
  test.Run();
}

TEST(ChildPortHandshake, ChildClientTokenIncorrect) {
  ChildPortHandshakeTest test(
      ChildPortHandshakeTest::ClientProcess::kChildClient,
      ChildPortHandshakeTest::TestType::kTokenIncorrect);
  test.Run();
}

TEST(ChildPortHandshake, ChildClientTokenIncorrectThenCorrect) {
  ChildPortHandshakeTest test(
      ChildPortHandshakeTest::ClientProcess::kChildClient,
      ChildPortHandshakeTest::TestType::kTokenIncorrectThenCorrect);
  test.Run();
}

TEST(ChildPortHandshake, ParentClientChecksIn_ReceiveRight) {
  ChildPortHandshakeTest test(
      ChildPortHandshakeTest::ClientProcess::kParentClient,
      ChildPortHandshakeTest::TestType::kClientChecksIn_ReceiveRight);
  test.Run();
}

TEST(ChildPortHandshake, ParentClientChecksIn_SendRight) {
  ChildPortHandshakeTest test(
      ChildPortHandshakeTest::ClientProcess::kParentClient,
      ChildPortHandshakeTest::TestType::kClientChecksIn_SendRight);
  test.Run();
}

TEST(ChildPortHandshake, ParentClientChecksIn_SendOnceRight) {
  ChildPortHandshakeTest test(
      ChildPortHandshakeTest::ClientProcess::kParentClient,
      ChildPortHandshakeTest::TestType::kClientChecksIn_SendOnceRight);
  test.Run();
}

TEST(ChildPortHandshake, ParentClientDoesNotCheckIn) {
  ChildPortHandshakeTest test(
      ChildPortHandshakeTest::ClientProcess::kParentClient,
      ChildPortHandshakeTest::TestType::kClientDoesNotCheckIn);
  test.Run();
}

TEST(ChildPortHandshake, ParentClientDoesNotCheckIn_ReadsPipe) {
  ChildPortHandshakeTest test(
      ChildPortHandshakeTest::ClientProcess::kParentClient,
      ChildPortHandshakeTest::TestType::kClientDoesNotCheckIn_ReadsPipe);
  test.Run();
}

TEST(ChildPortHandshake, ParentClientTokenIncorrect) {
  ChildPortHandshakeTest test(
      ChildPortHandshakeTest::ClientProcess::kParentClient,
      ChildPortHandshakeTest::TestType::kTokenIncorrect);
  test.Run();
}

TEST(ChildPortHandshake, ParentClientTokenIncorrectThenCorrect) {
  ChildPortHandshakeTest test(
      ChildPortHandshakeTest::ClientProcess::kParentClient,
      ChildPortHandshakeTest::TestType::kTokenIncorrectThenCorrect);
  test.Run();
}

TEST(ChildPortHandshake, ParentClientServerDies) {
  ChildPortHandshakeTest test(
      ChildPortHandshakeTest::ClientProcess::kParentClient,
      ChildPortHandshakeTest::TestType::kServerDies);
  test.Run();
}

TEST(ChildPortHandshake, NoClient) {
  // In this test, the client never checks in with the server because it never
  // even runs. This tests that the server properly detects that it has no
  // client at all, and does not terminate execution with an error such as
  // “broken pipe” when attempting to send instructions to the client. This test
  // is similar to kClientDoesNotCheckIn, but because there’s no client at all,
  // the server is guaranteed to see that its pipe partner is gone.
  ChildPortHandshake child_port_handshake;
  base::apple::ScopedMachSendRight child_port(child_port_handshake.RunServer(
      ChildPortHandshake::PortRightType::kSendRight));
  EXPECT_FALSE(child_port.is_valid());
}

}  // namespace
}  // namespace test
}  // namespace crashpad
