/* 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 "xpcrtfuzzing/xpcrtfuzzing.h"

#include "mozilla/Assertions.h"  // MOZ_CRASH
#include "mozilla/Utf8.h"        // mozilla::Utf8Unit

#include <stdio.h>  // fflush, fprintf, fputs

#include "FuzzingInterface.h"
#include "jsapi.h"

#include "js/CompilationAndEvaluation.h"  // JS::Evaluate
#include "js/CompileOptions.h"            // JS::CompileOptions
#include "js/Conversions.h"               // JS::Conversions
#include "js/ErrorReport.h"               // JS::PrintError
#include "js/Exception.h"                 // JS::StealPendingExceptionStack
#include "js/experimental/TypedData.h"  // JS_GetUint8ClampedArrayData, JS_NewUint8ClampedArray
#include "js/PropertyAndElement.h"  // JS_SetProperty, JS_HasOwnProperty
#include "js/RootingAPI.h"          // JS::Rooted
#include "js/SourceText.h"          // JS::Source{Ownership,Text}
#include "js/Value.h"               // JS::Value

using mozilla::dom::AutoJSAPI;

static AutoJSAPI* gJsapi = nullptr;
MOZ_GLIBCXX_CONSTINIT static std::string gFuzzModuleName;

static void CrashOnPendingException() {
  if (gJsapi->HasException()) {
    gJsapi->ReportException();

    MOZ_CRASH("Unhandled exception from JS runtime!");
  }
}

int FuzzXPCRuntimeStart(AutoJSAPI* jsapi, int* argc, char*** argv,
                        const XREShellData* aShellData) {
  gFuzzModuleName = getenv("FUZZER");
  gJsapi = jsapi;

  int ret = FuzzXPCRuntimeInit();
  if (ret) {
    fprintf(stderr, "Fuzzing Interface: Error: Initialize callback failed\n");
    return ret;
  }

#ifdef AFLFUZZ
  ret = aShellData->fuzzerDriver(FuzzXPCRuntimeFuzz);
#else
  ret = aShellData->fuzzerDriver(argc, argv, FuzzXPCRuntimeFuzz);
#endif
  if (!ret) {
    fprintf(stdout, "Trying to shutdown!\n");
    int shutdown = FuzzXPCRuntimeShutdown();
    if (shutdown) {
      fprintf(stderr, "Fuzzing Interface: Error: Shutdown callback failed\n");
      return shutdown;
    }
  }

  return ret;
}

int FuzzXPCRuntimeInit() {
  JSContext* cx = gJsapi->cx();
  JS::Rooted<JS::Value> v(cx);
  JS::CompileOptions opts(cx);

  // Load the fuzzing module specified in the FUZZER environment variable
  JS::EvaluateUtf8Path(cx, opts, gFuzzModuleName.c_str(), &v);

  // Any errors while loading the fuzzing module should be fatal
  CrashOnPendingException();

  return 0;
}

int FuzzXPCRuntimeFuzz(const uint8_t* buf, size_t size) {
  if (!size) {
    return 0;
  }

  JSContext* cx = gJsapi->cx();

  JS::Rooted<JSObject*> arr(cx, JS_NewUint8ClampedArray(cx, size));
  if (!arr) {
    MOZ_CRASH("OOM");
  }

  do {
    JS::AutoCheckCannotGC nogc;
    bool isShared;
    uint8_t* data = JS_GetUint8ClampedArrayData(arr, &isShared, nogc);
    MOZ_RELEASE_ASSERT(!isShared);
    memcpy(data, buf, size);
  } while (false);

  JS::Rooted<JSObject*> global(cx, JS::CurrentGlobalOrNull(cx));
  JS::RootedValue arrVal(cx, JS::ObjectValue(*arr));
  if (!JS_SetProperty(cx, global, "fuzzBuf", arrVal)) {
    MOZ_CRASH("JS_SetProperty failed");
  }

  JS::Rooted<JS::Value> v(cx);
  JS::CompileOptions opts(cx);

  static const char data[] = "JSFuzzIterate();";

  JS::SourceText<mozilla::Utf8Unit> srcBuf;
  if (!srcBuf.init(cx, data, strlen(data), JS::SourceOwnership::Borrowed)) {
    return 1;
  }

  if (!JS::Evaluate(cx, opts.setFileAndLine(__FILE__, __LINE__), srcBuf, &v) &&
      !JS_IsExceptionPending(cx)) {
    // A return value of `false` without a pending exception indicates
    // a timeout as triggered by the `timeout` shell function.
    return 1;
  }

  // The fuzzing module is required to handle any exceptions
  CrashOnPendingException();

  int32_t ret = 0;
  if (!ToInt32(cx, v, &ret)) {
    MOZ_CRASH("Must return an int32 compatible return value!");
  }

  return ret;
}

int FuzzXPCRuntimeShutdown() {
  JSContext* cx = gJsapi->cx();
  JS::Rooted<JS::Value> v(cx);
  JS::CompileOptions opts(cx);
  JS::Rooted<JSObject*> global(cx, JS::CurrentGlobalOrNull(cx));

  bool found = false;
  if (JS_HasOwnProperty(cx, global, "JSFuzzShutdown", &found)) {
    if (found) {
      static const char data[] = "JSFuzzShutdown();";
      JS::SourceText<mozilla::Utf8Unit> srcBuf;
      if (!srcBuf.init(cx, data, strlen(data), JS::SourceOwnership::Borrowed)) {
        return 1;
      }

      if (!JS::Evaluate(cx, opts.setFileAndLine(__FILE__, __LINE__), srcBuf,
                        &v) &&
          !JS_IsExceptionPending(cx)) {
        // A return value of `false` without a pending exception indicates
        // a timeout as triggered by the `timeout` shell function.
        return 1;
      }
    }
  }

  // The fuzzing module is required to handle any exceptions
  CrashOnPendingException();

  return 0;
}
