// Copyright 2014 The Crashpad Authors. All rights reserved.
//
// 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/exc_client_variants.h"

#include <mach/mach.h>
#include <pthread.h>
#include <string.h>
#include <sys/types.h>

#include "base/cxx17_backports.h"
#include "base/strings/stringprintf.h"
#include "gtest/gtest.h"
#include "test/mac/mach_errors.h"
#include "test/mac/mach_multiprocess.h"
#include "util/mach/exc_server_variants.h"
#include "util/mach/exception_behaviors.h"
#include "util/mach/mach_extensions.h"
#include "util/mach/mach_message.h"
#include "util/mach/mach_message_server.h"
#include "util/misc/implicit_cast.h"

namespace crashpad {
namespace test {
namespace {

class TestExcClientVariants : public MachMultiprocess,
                              public UniversalMachExcServer::Interface {
 public:
  TestExcClientVariants(exception_behavior_t behavior, bool all_fields)
      : MachMultiprocess(),
        UniversalMachExcServer::Interface(),
        behavior_(behavior),
        all_fields_(all_fields),
        handled_(false) {
    ++exception_;
    ++exception_code_;
    ++exception_subcode_;
  }

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

  // UniversalMachExcServer::Interface:

  virtual kern_return_t CatchMachException(
      exception_behavior_t behavior,
      exception_handler_t exception_port,
      thread_t thread,
      task_t task,
      exception_type_t exception,
      const mach_exception_data_type_t* code,
      mach_msg_type_number_t code_count,
      thread_state_flavor_t* flavor,
      ConstThreadState old_state,
      mach_msg_type_number_t old_state_count,
      thread_state_t new_state,
      mach_msg_type_number_t* new_state_count,
      const mach_msg_trailer_t* trailer,
      bool* destroy_complex_request) override {
    *destroy_complex_request = true;

    EXPECT_FALSE(handled_);
    handled_ = true;

    EXPECT_EQ(behavior, behavior_);
    EXPECT_EQ(exception_port, LocalPort());

    if (HasIdentity()) {
      EXPECT_NE(thread, THREAD_NULL);
      EXPECT_EQ(task, ChildTask());
    } else {
      EXPECT_EQ(thread, THREAD_NULL);
      EXPECT_EQ(task, TASK_NULL);
    }

    mach_exception_code_t expect_code = exception_code_;
    mach_exception_subcode_t expect_subcode = exception_subcode_;
    if ((behavior & MACH_EXCEPTION_CODES) == 0) {
      expect_code = implicit_cast<exception_data_type_t>(expect_code);
      expect_subcode = implicit_cast<exception_data_type_t>(expect_subcode);
    }

    EXPECT_EQ(exception, exception_);
    EXPECT_EQ(code_count, 2u);

    // The code_count check above would ideally use ASSERT_EQ so that the next
    // conditionals would not be necessary, but ASSERT_* requires a function
    // returning type void, and the interface dictates otherwise here.
    if (code_count >= 1) {
      EXPECT_EQ(code[0], expect_code);
    }
    if (code_count >= 2) {
      EXPECT_EQ(code[1], expect_subcode);
    }

    if (HasState()) {
      EXPECT_EQ(*flavor, exception_ + 10);
      EXPECT_EQ(old_state_count, MACHINE_THREAD_STATE_COUNT);
      EXPECT_NE(old_state, nullptr);
      EXPECT_EQ(*new_state_count,
                implicit_cast<mach_msg_type_number_t>(THREAD_STATE_MAX));
      EXPECT_NE(new_state, nullptr);

      for (size_t index = 0; index < old_state_count; ++index) {
        EXPECT_EQ(old_state[index], index);
      }

      // Use a flavor known to be different from the incoming flavor, for a test
      // of the “out” side of the in-out flavor parameter.
      *flavor = exception_ + 20;
      *new_state_count = MACHINE_THREAD_STATE_COUNT;

      // Send a new state back to the client.
      for (size_t index = 0; index < *new_state_count; ++index) {
        new_state[index] = MACHINE_THREAD_STATE_COUNT - index;
      }
    } else {
      EXPECT_EQ(*flavor, THREAD_STATE_NONE);
      EXPECT_EQ(old_state_count, 0u);
      EXPECT_EQ(old_state, nullptr);
      EXPECT_EQ(*new_state_count, 0u);
      EXPECT_EQ(new_state, nullptr);
    }

    return KERN_SUCCESS;
  }

 private:
  // MachMultiprocess:

  void MachMultiprocessParent() override {
    UniversalMachExcServer universal_mach_exc_server(this);

    kern_return_t kr =
        MachMessageServer::Run(&universal_mach_exc_server,
                               LocalPort(),
                               MACH_MSG_OPTION_NONE,
                               MachMessageServer::kOneShot,
                               MachMessageServer::kReceiveLargeError,
                               kMachMessageTimeoutWaitIndefinitely);
    EXPECT_EQ(kr, KERN_SUCCESS)
        << MachErrorMessage(kr, "MachMessageServer::Run");

    EXPECT_TRUE(handled_);
  }

  void MachMultiprocessChild() override {
    const exception_type_t exception = exception_;
    const mach_exception_data_type_t code[] = {
        exception_code_,
        exception_subcode_
    };

    thread_t thread = THREAD_NULL;
    task_t task = TASK_NULL;
    if (all_fields_ || HasIdentity()) {
      thread = MachThreadSelf();
      task = mach_task_self();
    }

    thread_state_flavor_t flavor;
    thread_state_flavor_t* flavor_p = nullptr;
    natural_t old_state[MACHINE_THREAD_STATE_COUNT];
    thread_state_t old_state_p = nullptr;
    mach_msg_type_number_t old_state_count = 0;
    natural_t new_state[THREAD_STATE_MAX];
    thread_state_t new_state_p = nullptr;
    mach_msg_type_number_t new_state_count;
    mach_msg_type_number_t* new_state_count_p = nullptr;
    if (all_fields_ || HasState()) {
      // Pick a different flavor each time based on the value of exception_.
      // These aren’t real flavors, it’s just for testing.
      flavor = exception_ + 10;
      flavor_p = &flavor;
      for (size_t index = 0; index < base::size(old_state); ++index) {
        old_state[index] = index;
      }
      old_state_p = reinterpret_cast<thread_state_t>(&old_state);
      old_state_count = base::size(old_state);

      // new_state and new_state_count are out parameters that the server should
      // never see or use, so set them to bogus values. The call to the server
      // should overwrite these.
      memset(new_state, 0xa5, sizeof(new_state));
      new_state_p = reinterpret_cast<thread_state_t>(&new_state);
      new_state_count = 0x5a;
      new_state_count_p = &new_state_count;
    }

    EXPECT_EQ(UniversalExceptionRaise(behavior_,
                                      RemotePort(),
                                      thread,
                                      task,
                                      exception,
                                      code,
                                      base::size(code),
                                      flavor_p,
                                      old_state_p,
                                      old_state_count,
                                      new_state_p,
                                      new_state_count_p),
              KERN_SUCCESS);

    if (HasState()) {
      // Verify the out parameters.

      EXPECT_EQ(flavor, exception_ + 20);
      EXPECT_EQ(new_state_count, MACHINE_THREAD_STATE_COUNT);

      for (size_t index = 0; index < new_state_count; ++index) {
        EXPECT_EQ(new_state[index], MACHINE_THREAD_STATE_COUNT - index);
      }
    }
  }

  bool HasIdentity() const {
    return ExceptionBehaviorHasIdentity(behavior_);
  }

  bool HasState() const {
    return ExceptionBehaviorHasState(behavior_);
  }

  // The behavior to test.
  exception_behavior_t behavior_;

  // If false, only fields required for the current value of behavior_ are set
  // in a call to UniversalExceptionRaise(). The thread and task fields are only
  // set for identity-carrying behaviors, and the flavor and state fields are
  // only set for state-carrying behaviors. If true, all fields are set
  // regardless of the behavior. Testing in both ways verifies that
  // UniversalExceptionRaise() can tolerate the null arguments documented as
  // usable when the behavior allows it, and that it ignores these arguments
  // even when set when the behavior does not make use of them.
  bool all_fields_;

  // true if an exception message was handled.
  bool handled_;

  // These fields will increment for each instantiation of the test class.
  static exception_type_t exception_;
  static mach_exception_code_t exception_code_;
  static mach_exception_subcode_t exception_subcode_;
};

exception_type_t TestExcClientVariants::exception_ = 0;

// exception_code_ and exception_subcode_ are always large enough to require
// 64 bits, so that when the 32-bit-only variants not using MACH_EXCEPITON_CODES
// are tested, the code and subcode fields can be checked for proper truncation.
mach_exception_code_t TestExcClientVariants::exception_code_ = 0x100000000;
mach_exception_subcode_t TestExcClientVariants::exception_subcode_ =
        0xffffffff00000000;

TEST(ExcClientVariants, UniversalExceptionRaise) {
  static constexpr exception_behavior_t kBehaviors[] = {
      EXCEPTION_DEFAULT,
      EXCEPTION_STATE,
      EXCEPTION_STATE_IDENTITY,
      kMachExceptionCodes | EXCEPTION_DEFAULT,
      kMachExceptionCodes | EXCEPTION_STATE,
      kMachExceptionCodes | EXCEPTION_STATE_IDENTITY,
  };

  for (size_t index = 0; index < base::size(kBehaviors); ++index) {
    exception_behavior_t behavior = kBehaviors[index];
    SCOPED_TRACE(base::StringPrintf("index %zu, behavior %d", index, behavior));

    {
      SCOPED_TRACE("all_fields = false");

      TestExcClientVariants test_exc_client_variants(behavior, false);
      test_exc_client_variants.Run();
    }

    {
      SCOPED_TRACE("all_fields = true");

      TestExcClientVariants test_exc_client_variants(behavior, true);
      test_exc_client_variants.Run();
    }
  }
}

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