/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "mozilla/moz_zucchini.h"

#include "absl/types/optional.h"
#include "base/logging.h"
#include "base/files/file.h"
#include "base/files/file_util.h"
#include "build/build_config.h"
#include "components/zucchini/crc32.h"
#include "components/zucchini/mapped_file.h"
#include "components/zucchini/patch_reader.h"
#include "components/zucchini/zucchini.h"
#include "components/zucchini/zucchini_apply.h"

#include <cstdio>
#include <limits>
#include <new>
#include <string>

#if BUILDFLAG(IS_WIN)
// Get EXCEPTION_* constants defined with DWORD type
#  include <ntstatus.h>

#  include "components/zucchini/exception_filter_helper_win.h"

#  include <io.h>
#endif  // BUILDFLAG(IS_WIN)

namespace zucchini::mozilla {

#ifdef ENABLE_TESTS
// Helpers for crash recovery tests.

static TestOptions gTestOptions;

void SetTestOptions(const TestOptions& aTestOptions) {
  gTestOptions = aTestOptions;
}

// Test-only log to prove stack unwinding reaches local destructors when we
// recover from a fault originating from inside zucchini.
class ScopedDestructorMarker {
 public:
  ScopedDestructorMarker() = default;

  ~ScopedDestructorMarker() {
    if (!gTestOptions.logDestructorMarker) {
      return;
    }

    LOG(ERROR) << "MOZ_TEST_ZUCCHINI_DTOR_MARKER";
  }
};

// Test-only OOM injection used by updater tests to verify that a thrown
// bad_alloc is translated into kStatusOutOfMemory.
static void MaybeTriggerTestBadAlloc() {
  if (!gTestOptions.triggerBadAlloc) {
    return;
  }

  // This allocation is expected to throw std::bad_alloc. volatile prevents
  // the compiler from optimizing it away. If it somehow succeeded, CHECK(false)
  // ensures the test fails visibly rather than silently passing.
  void* volatile allocation = ::operator new(std::numeric_limits<size_t>::max());
  (void)allocation;
  CHECK(false);
}

// Test-only CHECK() injection used by updater tests to verify that deliberate
// crash paths are converted into kStatusFatal.
static void MaybeTriggerTestCheckFailure() {
  if (!gTestOptions.triggerCheckFailure) {
    return;
  }

  CHECK(false);
}
#endif  // ENABLE_TESTS

static LogFunctionPtr gLogFunction = nullptr;

bool LogMessageHandler(int aSeverity, const char* aFile, int aLine,
                       size_t aMessageStart, const std::string& aStr) {
  if (!gLogFunction) {
    return false;
  }
  gLogFunction(aStr.c_str());
  return true;
}

void SetLogFunction(LogFunctionPtr aLogFunction) {
  gLogFunction = aLogFunction;
  logging::SetLogMessageHandler(LogMessageHandler);
}

uint32_t ComputeCrc32(const uint8_t* aBuf, size_t aBufSize) {
  return CalculateCrc32(aBuf, aBuf + aBufSize);
}

// This is zucchini::ApplyBuffer, except that we *assume* that aCheckedOldImage
// has the correct size and crc32 instead of checking it.
status::Code ApplyBufferUnsafe(ConstBufferView aCheckedOldImage,
                               const EnsemblePatchReader& aPatchReader,
                               MutableBufferView aNewImage) {
  for (const auto& elementPatch : aPatchReader.elements()) {
    ElementMatch match = elementPatch.element_match();
    if (!ApplyElement(match.exe_type(),
                      aCheckedOldImage[match.old_element.region()],
                      elementPatch, aNewImage[match.new_element.region()]))
      return status::kStatusFatal;
  }

  if (!aPatchReader.CheckNewFile(ConstBufferView(aNewImage))) {
    LOG(ERROR) << "Invalid aNewImage.";
    return status::kStatusInvalidNewImage;
  }
  return status::kStatusSuccess;
}

class MappedPatchImpl {
 public:
  MappedPatchImpl() = default;
  ~MappedPatchImpl() = default;
  std::optional<MappedFileReader> mFileReader;
  EnsemblePatchReader mPatchReader;
#if BUILDFLAG(IS_WIN)
  ExceptionFilterHelper mExceptionFilterHelper;
  DWORD mLastExceptionCode = 0;
#endif  // BUILDFLAG(IS_WIN)
};

MappedPatch::MappedPatch() : mImpl(new MappedPatchImpl()) {}

MappedPatch::~MappedPatch() { delete mImpl; }

#if BUILDFLAG(IS_WIN)
#  if !defined(HAVE_SEH_EXCEPTIONS) || !HAVE_SEH_EXCEPTIONS
#    error Compiler support for SEH is required to build zucchini on Windows.
#  endif

static constexpr DWORD kMsvcCppExceptionCode = 0xE06D7363;

// SEH filter for exceptions raised within zucchini code. Without this,
// recoverable exceptions would crash the updater before it can write
// update.status, causing SERVICE_STILL_APPLYING_ON_FAILURE errors.
//
// We only catch exceptions where the process state is known to be sound:
// - EXCEPTION_IN_PAGE_ERROR on mapped ranges (I/O failure, process healthy)
// - EXCEPTION_BREAKPOINT/EXCEPTION_ILLEGAL_INSTRUCTION (deliberate crash from
//   Chromium CHECK via ImmediateCrash, data validation failure)
// - 0xE06D7363 (MSVC C++ exception: std::bad_alloc from non-fallible
//   allocations in zucchini's disassembler. Neither zucchini nor its base shim
//   contain any explicit throw, so bad_alloc is the only possible C++ exception
//   observed here. On Windows, operator new allocation failure still surfaces
//   through SEH with this exception code.)
//
// Exceptions indicating corrupt process state (EXCEPTION_ACCESS_VIOLATION,
// EXCEPTION_STACK_OVERFLOW, etc.) are left unhandled so the process crashes
// as expected.
static int FilterZucchiniException(
    EXCEPTION_RECORD* aExceptionRecord,
    ExceptionFilterHelper& aPageErrorHelper,
    DWORD& aOutExceptionCode) {
  aOutExceptionCode = aExceptionRecord->ExceptionCode;

  // Check if this is a page error on our mapped ranges. This populates
  // nt_status/is_write for the handler to use in its diagnostic message.
  int pageResult = aPageErrorHelper.FilterPageError(aExceptionRecord);
  if (pageResult == EXCEPTION_EXECUTE_HANDLER) {
    return EXCEPTION_EXECUTE_HANDLER;
  }

  if (aOutExceptionCode == EXCEPTION_BREAKPOINT ||
      aOutExceptionCode == EXCEPTION_ILLEGAL_INSTRUCTION ||
      aOutExceptionCode ==
          kMsvcCppExceptionCode /* MSVC C++ exception (std::bad_alloc) */) {
    return EXCEPTION_EXECUTE_HANDLER;
  }

  return EXCEPTION_CONTINUE_SEARCH;
}

#  define BEGIN_TRY_EXCEPT() __try {
#  define END_TRY_EXCEPT()                                                 \
    }                                                                      \
    __except (FilterZucchiniException(                                     \
        GetExceptionInformation()->ExceptionRecord,                        \
        mImpl->mExceptionFilterHelper, mImpl->mLastExceptionCode)) {       \
      if (mImpl->mLastExceptionCode == EXCEPTION_IN_PAGE_ERROR) {          \
        LOG(ERROR) << "EXCEPTION_IN_PAGE_ERROR while "                     \
                   << (mImpl->mExceptionFilterHelper.is_write()            \
                           ? "writing to"                                  \
                           : "reading from")                               \
                   << " mapped files; NTSTATUS = "                         \
                   << mImpl->mExceptionFilterHelper.nt_status();           \
        return mImpl->mExceptionFilterHelper.nt_status() ==                \
                       STATUS_DISK_FULL                                    \
                   ? status::kStatusDiskFull                               \
                   : status::kStatusIoError;                               \
      }                                                                    \
      if (mImpl->mLastExceptionCode == kMsvcCppExceptionCode) {            \
        return status::kStatusOutOfMemory;                                 \
      }                                                                    \
      LOG(ERROR) << "CHECK failure (exception 0x" << std::hex              \
                 << mImpl->mLastExceptionCode                              \
                 << ") caught in zucchini; this is a bug.";                \
      return status::kStatusFatal;                                         \
    }
#else
#  define BEGIN_TRY_EXCEPT()
#  define END_TRY_EXCEPT()
#endif  // BUILDFLAG(IS_WIN)

// This corresponds to the first half of zucchini::ApplyCommon.
status::Code MappedPatch::Load(FILE* aPatchFile, uint32_t* aSourceSize,
                               uint32_t* aDestinationSize,
                               uint32_t* aSourceCrc32) {
  base::File patchFile = base::FILEToFile(aPatchFile);
  if (!patchFile.IsValid()) {
    LOG(ERROR) << "Invalid patch file.";
    return status::kStatusFileReadError;
  }
  mImpl->mFileReader.emplace(std::move(patchFile));
  auto& fileReader = *mImpl->mFileReader;
  if (fileReader.HasError()) {
    LOG(ERROR) << "Error with patch file: " << fileReader.error();
    if (fileReader.error_is_oom()) {
      return status::kStatusOutOfMemory;
    }
    return status::kStatusFileReadError;
  }
#if BUILDFLAG(IS_WIN)
  mImpl->mExceptionFilterHelper.AddRange(
      {fileReader.data(), fileReader.length()});
#endif
  BEGIN_TRY_EXCEPT()
#ifdef ENABLE_TESTS
  ScopedDestructorMarker destructorTester;
  MaybeTriggerTestBadAlloc();
  MaybeTriggerTestCheckFailure();
#endif  // ENABLE_TESTS

  BufferSource source(fileReader.region());
  auto& patchReader = mImpl->mPatchReader;
  if (!patchReader.Initialize(&source)) {
    LOG(ERROR) << "Error reading patch header.";
    return status::kStatusPatchReadError;
  }
  auto& header = patchReader.header();
  *aSourceSize = header.old_size;
  *aDestinationSize = header.new_size;
  *aSourceCrc32 = header.old_crc;
  return status::kStatusSuccess;
  END_TRY_EXCEPT()
}

// This corresponds to the second half of zucchini::ApplyCommon.
status::Code MappedPatch::ApplyUnsafe(const uint8_t* aCheckedOldImage,
                                      size_t aCheckedOldImageSize,
                                      FILE* aNewFile) {
  ConstBufferView oldImageView(aCheckedOldImage, aCheckedOldImageSize);

  base::File newFile = base::FILEToFile(aNewFile);
  if (!newFile.IsValid()) {
    LOG(ERROR) << "Invalid new file.";
    return status::kStatusFileWriteError;
  }

  BEGIN_TRY_EXCEPT()
  PatchHeader header = mImpl->mPatchReader.header();
  base::FilePath name;
  name = name.AppendASCII("old_name");
  MappedFileWriter mappedNew(name, std::move(newFile), header.new_size,
                             /*keep*/ true);
  if (mappedNew.HasError()) {
    LOG(ERROR) << "Error with new file: " << mappedNew.error();
    if (mappedNew.error_is_oom()) {
      return status::kStatusOutOfMemory;
    }
    return status::kStatusFileWriteError;
  }

#if BUILDFLAG(IS_WIN)
  mImpl->mExceptionFilterHelper.AddRange(
      {mappedNew.data(), mappedNew.length()});
#endif

  status::Code result =
      ApplyBufferUnsafe(oldImageView, mImpl->mPatchReader, mappedNew.region());
  if (result != status::kStatusSuccess) {
    LOG(ERROR) << "Fatal error encountered while applying patch.";
    return result;
  }

  if (!mappedNew.Flush()) {
    LOG(ERROR) << "Error flushing changes to disk: " << mappedNew.error();
    return status::kStatusFileWriteError;
  }

  return status::kStatusSuccess;
  END_TRY_EXCEPT()
}

}  // namespace zucchini::mozilla
