// Copyright 2018 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 "snapshot/fuchsia/process_reader_fuchsia.h"

#include <link.h>
#include <zircon/syscalls.h>

#include "base/fuchsia/fuchsia_logging.h"
#include "base/fuchsia/scoped_zx_handle.h"
#include "base/logging.h"

namespace crashpad {

ProcessReaderFuchsia::Module::Module() = default;

ProcessReaderFuchsia::Module::~Module() = default;

ProcessReaderFuchsia::Thread::Thread() = default;

ProcessReaderFuchsia::Thread::~Thread() = default;

ProcessReaderFuchsia::ProcessReaderFuchsia() = default;

ProcessReaderFuchsia::~ProcessReaderFuchsia() = default;

bool ProcessReaderFuchsia::Initialize(zx_handle_t process) {
  INITIALIZATION_STATE_SET_INITIALIZING(initialized_);

  process_ = process;

  process_memory_.reset(new ProcessMemoryFuchsia());
  process_memory_->Initialize(process_);

  INITIALIZATION_STATE_SET_VALID(initialized_);
  return true;
}

const std::vector<ProcessReaderFuchsia::Module>&
ProcessReaderFuchsia::Modules() {
  INITIALIZATION_STATE_DCHECK_VALID(initialized_);

  if (!initialized_modules_) {
    InitializeModules();
  }

  return modules_;
}

const std::vector<ProcessReaderFuchsia::Thread>&
ProcessReaderFuchsia::Threads() {
  INITIALIZATION_STATE_DCHECK_VALID(initialized_);

  if (!initialized_threads_) {
    InitializeThreads();
  }

  return threads_;
}

void ProcessReaderFuchsia::InitializeModules() {
  DCHECK(!initialized_modules_);
  DCHECK(modules_.empty());

  initialized_modules_ = true;

  // TODO(scottmg): <inspector/inspector.h> does some of this, but doesn't
  // expose any of the data that's necessary to fill out a Module after it
  // retrieves (some of) the data into internal structures. It may be worth
  // trying to refactor/upstream some of this into Fuchsia.

  std::string app_name("app:");
  {
    char name[ZX_MAX_NAME_LEN];
    zx_status_t status =
        zx_object_get_property(process_, ZX_PROP_NAME, name, sizeof(name));
    if (status != ZX_OK) {
      LOG(ERROR) << "zx_object_get_property ZX_PROP_NAME";
      return;
    }

    app_name += name;
  }

  // Starting from the ld.so's _dl_debug_addr, read the link_map structure and
  // walk the list to fill out modules_.

  uintptr_t debug_address;
  zx_status_t status = zx_object_get_property(process_,
                                              ZX_PROP_PROCESS_DEBUG_ADDR,
                                              &debug_address,
                                              sizeof(debug_address));
  if (status != ZX_OK || debug_address == 0) {
    LOG(ERROR) << "zx_object_get_property ZX_PROP_PROCESS_DEBUG_ADDR";
    return;
  }

  constexpr auto k_r_debug_map_offset = offsetof(r_debug, r_map);
  uintptr_t map;
  if (!process_memory_->Read(
          debug_address + k_r_debug_map_offset, sizeof(map), &map)) {
    LOG(ERROR) << "read link_map";
    return;
  }

  int i = 0;
  constexpr int kMaxDso = 1000;  // Stop after an unreasonably large number.
  while (map != 0) {
    if (++i >= kMaxDso) {
      LOG(ERROR) << "possibly circular dso list, terminating";
      return;
    }

    constexpr auto k_link_map_addr_offset = offsetof(link_map, l_addr);
    zx_vaddr_t base;
    if (!process_memory_->Read(
            map + k_link_map_addr_offset, sizeof(base), &base)) {
      LOG(ERROR) << "Read base";
      // Could theoretically continue here, but realistically if any part of
      // link_map fails to read, things are looking bad, so just abort.
      break;
    }

    constexpr auto k_link_map_next_offset = offsetof(link_map, l_next);
    zx_vaddr_t next;
    if (!process_memory_->Read(
            map + k_link_map_next_offset, sizeof(next), &next)) {
      LOG(ERROR) << "Read next";
      break;
    }

    constexpr auto k_link_map_name_offset = offsetof(link_map, l_name);
    zx_vaddr_t name_address;
    if (!process_memory_->Read(map + k_link_map_name_offset,
                               sizeof(name_address),
                               &name_address)) {
      LOG(ERROR) << "Read name address";
      break;
    }

    std::string dsoname;
    if (!process_memory_->ReadCString(name_address, &dsoname)) {
      // In this case, it could be reasonable to continue on to the next module
      // as this data isn't strictly in the link_map.
      LOG(ERROR) << "ReadCString name";
    }

    Module module;
    if (dsoname.empty()) {
      module.name = app_name;
      module.type = ModuleSnapshot::kModuleTypeExecutable;
    } else {
      module.name = dsoname;
      // TODO(scottmg): Handle kModuleTypeDynamicLoader.
      module.type = ModuleSnapshot::kModuleTypeSharedLibrary;
    }

    std::unique_ptr<ElfImageReader> reader(new ElfImageReader());

    std::unique_ptr<ProcessMemoryRange> process_memory_range(
        new ProcessMemoryRange());
    // TODO(scottmg): Could this be limited range?
    process_memory_range->Initialize(process_memory_.get(), true);
    process_memory_ranges_.push_back(std::move(process_memory_range));

    reader->Initialize(*process_memory_ranges_.back(), base);
    module.reader = reader.get();
    module_readers_.push_back(std::move(reader));
    modules_.push_back(module);

    map = next;
  }
}

void ProcessReaderFuchsia::InitializeThreads() {
  DCHECK(!initialized_threads_);
  DCHECK(threads_.empty());

  initialized_threads_ = true;

  // Retrieve the thread koids. This is racy; better if the process is suspended
  // itself, but threads could still be externally created. As there's no
  // maximum, this needs to be retried in a loop until the actual threads
  // retrieved is equal to the available threads.

  std::vector<zx_koid_t> threads(100);
  size_t actual_num_threads, available_num_threads;
  for (;;) {
    zx_status_t status = zx_object_get_info(process_,
                                            ZX_INFO_PROCESS_THREADS,
                                            &threads[0],
                                            sizeof(threads[0]) * threads.size(),
                                            &actual_num_threads,
                                            &available_num_threads);
    // If the buffer is too small (even zero), the result is still ZX_OK, not
    // ZX_ERR_BUFFER_TOO_SMALL.
    if (status != ZX_OK) {
      ZX_LOG(ERROR, status) << "zx_object_get_info ZX_INFO_PROCESS_THREADS";
      break;
    }
    if (actual_num_threads == available_num_threads) {
      threads.resize(actual_num_threads);
      break;
    }

    // Resize to the expected number next time with a bit extra to attempt to
    // handle the race between here and the next request.
    threads.resize(available_num_threads + 10);
  }

  for (const zx_koid_t thread_koid : threads) {
    zx_handle_t raw_handle;
    zx_status_t status = zx_object_get_child(
        process_, thread_koid, ZX_RIGHT_SAME_RIGHTS, &raw_handle);
    if (status != ZX_OK) {
      ZX_LOG(ERROR, status) << "zx_object_get_child";
      // TODO(scottmg): Decide if it's worthwhile adding a mostly-empty Thread
      // here, consisting only of the koid, but no other information. The only
      // time this is expected to happen is when there's a race between getting
      // the koid above, and requesting the handle here.
      continue;
    }

    base::ScopedZxHandle thread_handle(raw_handle);

    Thread thread;
    thread.id = thread_koid;

    char name[ZX_MAX_NAME_LEN] = {0};
    status = zx_object_get_property(
        thread_handle.get(), ZX_PROP_NAME, &name, sizeof(name));
    if (status != ZX_OK) {
      ZX_LOG(WARNING, status) << "zx_object_get_property ZX_PROP_NAME";
    } else {
      thread.name.assign(name);
    }

    zx_info_thread_t thread_info;
    status = zx_object_get_info(thread_handle.get(),
                                ZX_INFO_THREAD,
                                &thread_info,
                                sizeof(thread_info),
                                nullptr,
                                nullptr);
    if (status != ZX_OK) {
      ZX_LOG(WARNING, status) << "zx_object_get_info ZX_INFO_THREAD";
    } else {
      thread.state = thread_info.state;
    }

    threads_.push_back(thread);
  }
}

}  // namespace crashpad