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

#include "nsIIncrementalStreamLoader.h"
#include "nsIURI.h"
#include "nsIChannel.h"
#include "nsNetUtil.h"
#include "nsThreadUtils.h"

#include "jsapi.h"
#include "jsfriendapi.h"
#include "js/CompileOptions.h"  // JS::CompileOptions, JS::OwningCompileOptions
#include "js/CompilationAndEvaluation.h"
#include "js/experimental/CompileScript.h"  // JS::CompileGlobalScriptToStencil, JS::NewFrontendContext, JS::DestroyFrontendContext, JS::SetNativeStackQuota, JS::ThreadStackQuotaForSize, JS::HadFrontendErrors, JS::ConvertFrontendErrorsToRuntimeErrors
#include "js/experimental/JSStencil.h"  // JS::Stencil, JS::CompileGlobalScriptToStencil, JS::InstantiateGlobalStencil
#include "js/SourceText.h"  // JS::SourceText
#include "js/Utility.h"

#include "mozilla/AlreadyAddRefed.h"  // already_AddRefed
#include "mozilla/Assertions.h"       // MOZ_ASSERT
#include "mozilla/Attributes.h"
#include "mozilla/ClearOnShutdown.h"  // RunOnShutdown
#include "mozilla/EventQueue.h"       // EventQueuePriority
#include "mozilla/Mutex.h"
#include "mozilla/SchedulerGroup.h"
#include "mozilla/StaticMutex.h"
#include "mozilla/StaticPrefs_javascript.h"
#include "mozilla/dom/ChromeUtils.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/dom/ScriptLoader.h"
#include "mozilla/HoldDropJSObjects.h"
#include "mozilla/RefPtr.h"          // RefPtr
#include "mozilla/TaskController.h"  // TaskController, Task
#include "mozilla/ThreadSafety.h"    // MOZ_GUARDED_BY
#include "mozilla/Utf8.h"            // Utf8Unit
#include "mozilla/Vector.h"
#include "nsCCUncollectableMarker.h"
#include "nsCycleCollectionParticipant.h"

using namespace JS;
using namespace mozilla;
using namespace mozilla::dom;

class AsyncScriptCompileTask final : public Task {
  static mozilla::StaticMutex sOngoingTasksMutex;
  static Vector<AsyncScriptCompileTask*> sOngoingTasks
      MOZ_GUARDED_BY(sOngoingTasksMutex);
  static bool sIsShutdownRegistered;

  // Compilation tasks should be cancelled before calling JS_ShutDown, in order
  // to avoid keeping JS::Stencil and SharedImmutableString pointers alive
  // beyond it.
  //
  // Cancel all ongoing tasks at ShutdownPhase::XPCOMShutdownFinal, which
  // happens before calling JS_ShutDown.
  static bool RegisterTask(AsyncScriptCompileTask* aTask) {
    MOZ_ASSERT(NS_IsMainThread());

    if (!sIsShutdownRegistered) {
      sIsShutdownRegistered = true;

      RunOnShutdown([] {
        StaticMutexAutoLock lock(sOngoingTasksMutex);
        for (auto* task : sOngoingTasks) {
          task->Cancel();
        }
      });
    }

    StaticMutexAutoLock lock(sOngoingTasksMutex);
    return sOngoingTasks.append(aTask);
  }

  static void UnregisterTask(const AsyncScriptCompileTask* aTask) {
    StaticMutexAutoLock lock(sOngoingTasksMutex);
    sOngoingTasks.eraseIfEqual(aTask);
  }

 public:
  explicit AsyncScriptCompileTask(JS::SourceText<Utf8Unit>&& aSrcBuf)
      : Task(Kind::OffMainThreadOnly, EventQueuePriority::Normal),
        mOptions(JS::OwningCompileOptions::ForFrontendContext()),
        mSrcBuf(std::move(aSrcBuf)),
        mMutex("AsyncScriptCompileTask") {}

  ~AsyncScriptCompileTask() {
    if (mFrontendContext) {
      JS::DestroyFrontendContext(mFrontendContext);
    }
    UnregisterTask(this);
  }

  bool Init(const JS::OwningCompileOptions& aOptions) {
    if (!RegisterTask(this)) {
      return false;
    }

    mFrontendContext = JS::NewFrontendContext();
    if (!mFrontendContext) {
      return false;
    }

    if (!mOptions.copy(mFrontendContext, aOptions)) {
      return false;
    }

    return true;
  }

 private:
  void Compile() {
    // NOTE: The stack limit must be set from the same thread that compiles.
    size_t stackSize = TaskController::GetThreadStackSize();
    JS::SetNativeStackQuota(mFrontendContext,
                            JS::ThreadStackQuotaForSize(stackSize));

    mStencil =
        JS::CompileGlobalScriptToStencil(mFrontendContext, mOptions, mSrcBuf);
  }

  // Cancel the task.
  // If the task is already running, this waits for the task to finish.
  void Cancel() {
    MOZ_ASSERT(NS_IsMainThread());

    MutexAutoLock lock(mMutex);

    mIsCancelled = true;

    mStencil = nullptr;
  }

 public:
  TaskResult Run() override {
    MutexAutoLock lock(mMutex);

    if (mIsCancelled) {
      return TaskResult::Complete;
    }

    Compile();
    return TaskResult::Complete;
  }

  already_AddRefed<JS::Stencil> StealStencil(JSContext* aCx) {
    JS::FrontendContext* fc = mFrontendContext;
    mFrontendContext = nullptr;

    MOZ_ASSERT(fc);

    if (JS::HadFrontendErrors(fc)) {
      (void)JS::ConvertFrontendErrorsToRuntimeErrors(aCx, fc, mOptions);
      JS::DestroyFrontendContext(fc);
      return nullptr;
    }

    // Report warnings.
    if (!JS::ConvertFrontendErrorsToRuntimeErrors(aCx, fc, mOptions)) {
      JS::DestroyFrontendContext(fc);
      return nullptr;
    }

    JS::DestroyFrontendContext(fc);

    return mStencil.forget();
  }

#ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY
  bool GetName(nsACString& aName) override {
    aName.AssignLiteral("AsyncScriptCompileTask");
    return true;
  }
#endif

 private:
  // Owning-pointer for the context associated with the script compilation.
  //
  // The context is allocated on main thread in Init method, and is freed on
  // any thread in the destructor.
  JS::FrontendContext* mFrontendContext = nullptr;

  JS::OwningCompileOptions mOptions;

  RefPtr<JS::Stencil> mStencil;

  JS::SourceText<Utf8Unit> mSrcBuf;

  // This mutex is locked during running the task or cancelling task.
  mozilla::Mutex mMutex;

  bool mIsCancelled MOZ_GUARDED_BY(mMutex) = false;
};

/* static */ mozilla::StaticMutex AsyncScriptCompileTask::sOngoingTasksMutex;
MOZ_RUNINIT /* static */ Vector<AsyncScriptCompileTask*>
    AsyncScriptCompileTask::sOngoingTasks;
/* static */ bool AsyncScriptCompileTask::sIsShutdownRegistered = false;

class AsyncScriptCompiler;

class AsyncScriptCompilationCompleteTask : public Task {
  static Vector<AsyncScriptCompilationCompleteTask*> sPendingTasks;

  static void RegisterTask(AsyncScriptCompilationCompleteTask* aTask) {
    MOZ_ASSERT(NS_IsMainThread());

    static bool sIsShutdownRegistered = false;
    if (!sIsShutdownRegistered) {
      sIsShutdownRegistered = true;

      RunOnShutdown([] {
        for (auto* task : sPendingTasks) {
          task->CancelAtShutdown();
        }
      });
    }
    DebugOnly<bool> ok = sPendingTasks.append(aTask);
    MOZ_ASSERT(ok, "AsyncScriptCompilationCompleteTask should register");
  }

  static void UnregisterTask(const AsyncScriptCompilationCompleteTask* aTask) {
    MOZ_ASSERT(NS_IsMainThread());
    sPendingTasks.eraseIfEqual(aTask);
  }

 public:
  AsyncScriptCompilationCompleteTask(AsyncScriptCompiler* aCompiler,
                                     AsyncScriptCompileTask* aCompileTask)
      : Task(Kind::MainThreadOnly, EventQueuePriority::Normal),
        mCompiler(aCompiler),
        mCompileTask(aCompileTask) {
    MOZ_ASSERT(NS_IsMainThread());
    RegisterTask(this);
  }

  ~AsyncScriptCompilationCompleteTask() {
    MOZ_ASSERT(NS_IsMainThread());  // mCompiler should be freed on main thread.
    UnregisterTask(this);
  }

#ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY
  bool GetName(nsACString& aName) override {
    aName.AssignLiteral("AsyncScriptCompilationCompleteTask");
    return true;
  }
#endif

  TaskResult Run() override;

 private:
  void CancelAtShutdown() {
    MOZ_ASSERT(NS_IsMainThread());

    // Release AsyncScriptCompiler so that it can be destructed earlier while
    // JS is still alive, instead of at TaskController shutdown (where JS is
    // gone). This avoids use of values such as AsyncScriptCompiler::mPromise,
    // see https://bugzilla.mozilla.org/show_bug.cgi?id=1940852#c40
    //
    // We don't bother with settling mCompiler::mPromise because we are
    // shutting down anyway.
    mCompiler = nullptr;
    mCompileTask = nullptr;
  }

  // NOTE:
  // This field is main-thread only, and this task shouldn't be freed off
  // main thread.
  //
  // This is guaranteed by not having off-thread tasks which depends on this
  // task, because otherwise the off-thread task's mDependencies can be the
  // last reference, which results in freeing this task off main thread.
  //
  // If such task is added, this field must be moved to separate storage.
  RefPtr<AsyncScriptCompiler> mCompiler;

  RefPtr<AsyncScriptCompileTask> mCompileTask;
};

MOZ_RUNINIT /* static */ Vector<AsyncScriptCompilationCompleteTask*>
    AsyncScriptCompilationCompleteTask::sPendingTasks;

class AsyncScriptCompiler final : public nsIIncrementalStreamLoaderObserver {
 public:
  // Note: References to this class are never held by cycle-collected objects.
  // If at any point a reference is returned to a caller, please update this
  // class to implement cycle collection.
  NS_DECL_ISUPPORTS
  NS_DECL_NSIINCREMENTALSTREAMLOADEROBSERVER

  AsyncScriptCompiler(JSContext* aCx, nsIGlobalObject* aGlobal,
                      const nsACString& aURL, Promise* aPromise)
      : mOptions(aCx),
        mURL(aURL),
        mGlobalObject(aGlobal),
        mPromise(aPromise),
        mScriptLength(0) {}

  [[nodiscard]] nsresult Start(JSContext* aCx,
                               const CompileScriptOptionsDictionary& aOptions,
                               nsIPrincipal* aPrincipal);

  void OnCompilationComplete(AsyncScriptCompileTask* aCompileTask);

 protected:
  virtual ~AsyncScriptCompiler() {
    if (mPromise->State() == Promise::PromiseState::Pending) {
      mPromise->MaybeReject(NS_ERROR_FAILURE);
    }
  }

 private:
  void Reject(JSContext* aCx);
  void Reject(JSContext* aCx, const char* aMxg);

  bool StartCompile(JSContext* aCx);
  bool StartOffThreadCompile(JS::SourceText<Utf8Unit>&& aSrcBuf);
  void FinishCompile(JSContext* aCx);
  void Finish(JSContext* aCx, RefPtr<JS::Stencil>&& aStencil);

  OwningCompileOptions mOptions;
  nsCString mURL;
  nsCOMPtr<nsIGlobalObject> mGlobalObject;
  RefPtr<Promise> mPromise;
  nsString mCharset;
  UniquePtr<Utf8Unit[], JS::FreePolicy> mScriptText;
  size_t mScriptLength;
};

NS_IMPL_ISUPPORTS(AsyncScriptCompiler, nsIIncrementalStreamLoaderObserver)

nsresult AsyncScriptCompiler::Start(
    JSContext* aCx, const CompileScriptOptionsDictionary& aOptions,
    nsIPrincipal* aPrincipal) {
  mCharset = aOptions.mCharset;

  CompileOptions options(aCx);
  nsAutoCString filename;
  if (aOptions.mFilename.WasPassed()) {
    filename = NS_ConvertUTF16toUTF8(aOptions.mFilename.Value());
    options.setFile(filename.get());
  } else {
    options.setFile(mURL.get());
  }
  options.setNoScriptRval(!aOptions.mHasReturnValue);

  if (!aOptions.mLazilyParse) {
    options.setForceFullParse();
  }

  if (NS_WARN_IF(!mOptions.copy(aCx, options))) {
    return NS_ERROR_OUT_OF_MEMORY;
  }

  nsCOMPtr<nsIURI> uri;
  nsresult rv = NS_NewURI(getter_AddRefs(uri), mURL);
  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr<nsIChannel> channel;
  rv = NS_NewChannel(
      getter_AddRefs(channel), uri, aPrincipal,
      nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
      nsIContentPolicy::TYPE_INTERNAL_CHROMEUTILS_COMPILED_SCRIPT);
  NS_ENSURE_SUCCESS(rv, rv);

  // allow deprecated HTTP request from SystemPrincipal
  nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo();
  loadInfo->SetAllowDeprecatedSystemRequests(true);
  nsCOMPtr<nsIIncrementalStreamLoader> loader;
  rv = NS_NewIncrementalStreamLoader(getter_AddRefs(loader), this);
  NS_ENSURE_SUCCESS(rv, rv);

  return channel->AsyncOpen(loader);
}

bool AsyncScriptCompiler::StartCompile(JSContext* aCx) {
  JS::SourceText<Utf8Unit> srcBuf;
  if (!srcBuf.init(aCx, std::move(mScriptText), mScriptLength)) {
    return false;
  }

  // TODO: This uses the same heuristics and the same threshold as the
  //       JS::CanCompileOffThread, but the heuristics needs to be updated to
  //       reflect the change regarding the Stencil API, and also the thread
  //       management on the consumer side (bug 1846388).
  static constexpr size_t OffThreadMinimumTextLength = 5 * 1000;

  if (StaticPrefs::javascript_options_parallel_parsing() &&
      mScriptLength >= OffThreadMinimumTextLength) {
    if (!StartOffThreadCompile(std::move(srcBuf))) {
      return false;
    }
    return true;
  }

  RefPtr<Stencil> stencil =
      JS::CompileGlobalScriptToStencil(aCx, mOptions, srcBuf);
  if (!stencil) {
    return false;
  }

  Finish(aCx, std::move(stencil));
  return true;
}

bool AsyncScriptCompiler::StartOffThreadCompile(
    JS::SourceText<Utf8Unit>&& aSrcBuf) {
  RefPtr<AsyncScriptCompileTask> compileTask =
      new AsyncScriptCompileTask(std::move(aSrcBuf));

  RefPtr<AsyncScriptCompilationCompleteTask> complationCompleteTask =
      new AsyncScriptCompilationCompleteTask(this, compileTask.get());

  if (!compileTask->Init(mOptions)) {
    return false;
  }

  complationCompleteTask->AddDependency(compileTask.get());

  TaskController::Get()->AddTask(compileTask.forget());
  TaskController::Get()->AddTask(complationCompleteTask.forget());
  return true;
}

Task::TaskResult AsyncScriptCompilationCompleteTask::Run() {
  // mCompiler could have been cleared earlier, by CancelAtShutdown.
  if (mCompiler) {
    mCompiler->OnCompilationComplete(mCompileTask.get());
  }
  mCompiler = nullptr;
  mCompileTask = nullptr;
  return TaskResult::Complete;
}

void AsyncScriptCompiler::OnCompilationComplete(
    AsyncScriptCompileTask* aCompileTask) {
  AutoJSAPI jsapi;
  if (!jsapi.Init(mGlobalObject)) {
    mPromise->MaybeReject(NS_ERROR_FAILURE);
    return;
  }

  JSContext* cx = jsapi.cx();
  RefPtr<JS::Stencil> stencil = aCompileTask->StealStencil(cx);
  if (!stencil) {
    Reject(cx);
    return;
  }

  Finish(cx, std::move(stencil));
  return;
}

void AsyncScriptCompiler::Finish(JSContext* aCx,
                                 RefPtr<JS::Stencil>&& aStencil) {
  RefPtr<PrecompiledScript> result =
      new PrecompiledScript(mGlobalObject, aStencil, mOptions);

  mPromise->MaybeResolve(result);
}

void AsyncScriptCompiler::Reject(JSContext* aCx) {
  RootedValue value(aCx, JS::UndefinedValue());
  if (JS_GetPendingException(aCx, &value)) {
    JS_ClearPendingException(aCx);
  }
  mPromise->MaybeReject(value);
}

void AsyncScriptCompiler::Reject(JSContext* aCx, const char* aMsg) {
  nsAutoString msg;
  msg.AppendASCII(aMsg);
  msg.AppendLiteral(": ");
  nsDependentCString filename(mOptions.filename().c_str());
  AppendUTF8toUTF16(filename, msg);

  RootedValue exn(aCx);
  if (xpc::NonVoidStringToJsval(aCx, msg, &exn)) {
    JS_SetPendingException(aCx, exn);
  }

  Reject(aCx);
}

NS_IMETHODIMP
AsyncScriptCompiler::OnStartRequest(nsIRequest* aRequest) { return NS_OK; }

NS_IMETHODIMP
AsyncScriptCompiler::OnIncrementalData(nsIIncrementalStreamLoader* aLoader,
                                       nsISupports* aContext,
                                       uint32_t aDataLength,
                                       const uint8_t* aData,
                                       uint32_t* aConsumedData) {
  return NS_OK;
}

NS_IMETHODIMP
AsyncScriptCompiler::OnStreamComplete(nsIIncrementalStreamLoader* aLoader,
                                      nsISupports* aContext, nsresult aStatus,
                                      uint32_t aLength, const uint8_t* aBuf) {
  AutoJSAPI jsapi;
  if (!jsapi.Init(mGlobalObject)) {
    mPromise->MaybeReject(NS_ERROR_FAILURE);
    return NS_OK;
  }

  JSContext* cx = jsapi.cx();

  if (NS_FAILED(aStatus)) {
    Reject(cx, "Unable to load script");
    return NS_OK;
  }

  nsresult rv = ScriptLoader::ConvertToUTF8(
      nullptr, aBuf, aLength, mCharset, nullptr, mScriptText, mScriptLength);
  if (NS_FAILED(rv)) {
    Reject(cx, "Unable to decode script");
    return NS_OK;
  }

  if (!StartCompile(cx)) {
    Reject(cx);
  }

  return NS_OK;
}

namespace mozilla {
namespace dom {

/* static */
already_AddRefed<Promise> ChromeUtils::CompileScript(
    GlobalObject& aGlobal, const nsAString& aURL,
    const CompileScriptOptionsDictionary& aOptions, ErrorResult& aRv) {
  nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
  MOZ_ASSERT(global);

  RefPtr<Promise> promise = Promise::Create(global, aRv);
  if (aRv.Failed()) {
    return nullptr;
  }

  NS_ConvertUTF16toUTF8 url(aURL);
  RefPtr<AsyncScriptCompiler> compiler =
      new AsyncScriptCompiler(aGlobal.Context(), global, url, promise);

  nsresult rv = compiler->Start(aGlobal.Context(), aOptions,
                                aGlobal.GetSubjectPrincipal());
  if (NS_FAILED(rv)) {
    promise->MaybeReject(rv);
  }

  return promise.forget();
}

PrecompiledScript::PrecompiledScript(nsISupports* aParent,
                                     RefPtr<JS::Stencil> aStencil,
                                     JS::ReadOnlyCompileOptions& aOptions)
    : mParent(aParent),
      mStencil(aStencil),
      mPublicURL(aOptions.filename().c_str()),
      mHasReturnValue(!aOptions.noScriptRval) {
  MOZ_ASSERT(aParent);
  MOZ_ASSERT(aStencil);
#ifdef DEBUG
  // AsyncScriptCompiler::Start can call JS::CompileOptions::setForceFullParse,
  // but it should be compatible with the default JS::InstantiateOptions.
  JS::InstantiateOptions options(aOptions);
  options.assertCompatibleWithDefault();
#endif
};

void PrecompiledScript::ExecuteInGlobal(JSContext* aCx, HandleObject aGlobal,
                                        const ExecuteInGlobalOptions& aOptions,
                                        MutableHandleValue aRval,
                                        ErrorResult& aRv) {
  {
    RootedObject targetObj(aCx, JS_FindCompilationScope(aCx, aGlobal));
    // Use AutoEntryScript for its ReportException method call.
    // This will ensure notified any exception happening in the content script
    // directly to the console, so that exceptions are flagged with the right
    // innerWindowID. It helps these exceptions to appear in the page's web
    // console.
    AutoEntryScript aes(targetObj, "pre-compiled-script execution");
    JSContext* cx = aes.cx();

    // See assertion in constructor.
    JS::InstantiateOptions options;
    Rooted<JSScript*> script(
        cx, JS::InstantiateGlobalStencil(cx, options, mStencil));
    if (!script) {
      aRv.NoteJSContextException(aCx);
      return;
    }

    if (!JS_ExecuteScript(cx, script, aRval)) {
      JS::RootedValue exn(cx);
      if (aOptions.mReportExceptions) {
        // Note that ReportException will consume the exception.
        aes.ReportException();
      } else {
        // Set the exception on our caller's cx.
        aRv.MightThrowJSException();
        aRv.StealExceptionFromJSContext(cx);
      }
      return;
    }
  }

  JS_WrapValue(aCx, aRval);
}

void PrecompiledScript::GetUrl(nsAString& aUrl) {
  CopyUTF8toUTF16(mPublicURL, aUrl);
}

bool PrecompiledScript::HasReturnValue() { return mHasReturnValue; }

JSObject* PrecompiledScript::WrapObject(JSContext* aCx,
                                        HandleObject aGivenProto) {
  return PrecompiledScript_Binding::Wrap(aCx, this, aGivenProto);
}

bool PrecompiledScript::IsBlackForCC(bool aTracingNeeded) {
  return (nsCCUncollectableMarker::sGeneration && HasKnownLiveWrapper() &&
          (!aTracingNeeded || HasNothingToTrace(this)));
}

NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(PrecompiledScript, mParent)

NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PrecompiledScript)
  NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
  NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END

NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_BEGIN(PrecompiledScript)
  return tmp->IsBlackForCC(false);
NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_END

NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_IN_CC_BEGIN(PrecompiledScript)
  return tmp->IsBlackForCC(true);
NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_IN_CC_END

NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_BEGIN(PrecompiledScript)
  return tmp->IsBlackForCC(false);
NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_END

NS_IMPL_CYCLE_COLLECTING_ADDREF(PrecompiledScript)
NS_IMPL_CYCLE_COLLECTING_RELEASE(PrecompiledScript)

}  // namespace dom
}  // namespace mozilla
