/* 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 "AddonManagerStartup.h"
#include "AddonManagerStartup-inlines.h"

#include "jsapi.h"
#include "jsfriendapi.h"
#include "js/Array.h"  // JS::IsArrayObject
#include "js/ArrayBuffer.h"
#include "js/Exception.h"
#include "js/JSON.h"
#include "js/PropertyAndElement.h"  // JS_GetProperty, JS_SetProperty
#include "js/TracingAPI.h"
#include "xpcpublic.h"

#include "mozilla/AppShutdown.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/EndianUtils.h"
#include "mozilla/Components.h"
#include "mozilla/Compression.h"
#include "mozilla/LinkedList.h"
#include "mozilla/Preferences.h"
#include "mozilla/ResultExtensions.h"
#include "mozilla/URLPreloader.h"
#include "mozilla/ErrorResult.h"
#include "mozilla/Services.h"
#include "mozilla/Try.h"
#include "mozilla/dom/ipc/StructuredCloneData.h"
#include "mozilla/dom/TypedArray.h"

#include "nsAppDirectoryServiceDefs.h"
#include "nsAppRunner.h"
#include "nsChromeRegistry.h"
#include "nsIDOMWindowUtils.h"  // for nsIJSRAIIHelper
#include "nsIFileURL.h"
#include "nsIIOService.h"
#include "nsIJARURI.h"
#include "nsIStringEnumerator.h"
#include "nsIZipReader.h"
#include "nsJARProtocolHandler.h"
#include "nsJSUtils.h"
#include "nsIObserverService.h"
#include "nsReadableUtils.h"
#include "nsXULAppAPI.h"

namespace mozilla {

using Compression::LZ4;
using dom::ipc::StructuredCloneData;

AddonManagerStartup& AddonManagerStartup::GetSingleton() {
  static RefPtr<AddonManagerStartup> singleton;
  if (!singleton) {
    singleton = new AddonManagerStartup();
    ClearOnShutdown(&singleton);
  }
  return *singleton;
}

AddonManagerStartup::AddonManagerStartup() = default;

nsIFile* AddonManagerStartup::ProfileDir() {
  if (!mProfileDir) {
    nsresult rv;

    rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
                                getter_AddRefs(mProfileDir));
    MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
  }

  return mProfileDir;
}

NS_IMPL_ISUPPORTS(AddonManagerStartup, amIAddonManagerStartup, nsIObserver)

/*****************************************************************************
 * URI utils
 *****************************************************************************/

static nsresult ParseJARURI(nsIJARURI* uri, nsIURI** jarFile,
                            nsCString& entry) {
  MOZ_TRY(uri->GetJARFile(jarFile));
  MOZ_TRY(uri->GetJAREntry(entry));

  // The entry portion of a jar: URI is required to begin with a '/', but for
  // nested JAR URIs, the leading / of the outer entry is currently stripped.
  // This is a bug which should be fixed in the JAR URI code, but...
  if (entry.IsEmpty() || entry[0] != '/') {
    entry.Insert('/', 0);
  }
  return NS_OK;
}

static nsresult ParseJARURI(nsIURI* uri, nsIURI** jarFile, nsCString& entry) {
  nsresult rv;
  nsCOMPtr<nsIJARURI> jarURI = do_QueryInterface(uri, &rv);
  MOZ_TRY(rv);

  return ParseJARURI(jarURI, jarFile, entry);
}

static Result<nsCOMPtr<nsIFile>, nsresult> GetFile(nsIURI* uri) {
  nsresult rv;
  nsCOMPtr<nsIFileURL> fileURL = do_QueryInterface(uri, &rv);
  MOZ_TRY(rv);

  nsCOMPtr<nsIFile> file;
  MOZ_TRY(fileURL->GetFile(getter_AddRefs(file)));
  MOZ_ASSERT(file);

  return std::move(file);
}

/*****************************************************************************
 * File utils
 *****************************************************************************/

static already_AddRefed<nsIFile> CloneAndAppend(nsIFile* aFile,
                                                const char* name) {
  nsCOMPtr<nsIFile> file;
  aFile->Clone(getter_AddRefs(file));
  file->AppendNative(nsDependentCString(name));
  return file.forget();
}

static bool IsNormalFile(nsIFile* file) {
  bool result;
  return NS_SUCCEEDED(file->IsFile(&result)) && result;
}

static const char STRUCTURED_CLONE_MAGIC[] = "mozJSSCLz40v001";

template <typename T>
static Result<nsCString, nsresult> DecodeLZ4(const nsACString& lz4,
                                             const T& magicNumber) {
  constexpr auto HEADER_SIZE = sizeof(magicNumber) + 4;

  // Note: We want to include the null terminator here.
  nsDependentCSubstring magic(magicNumber, sizeof(magicNumber));

  if (lz4.Length() < HEADER_SIZE || StringHead(lz4, magic.Length()) != magic) {
    return Err(NS_ERROR_UNEXPECTED);
  }

  auto data = lz4.BeginReading() + magic.Length();
  auto size = LittleEndian::readUint32(data);
  data += 4;

  size_t dataLen = lz4.EndReading() - data;
  size_t outputSize;

  nsCString result;
  if (!result.SetLength(size, fallible) ||
      !LZ4::decompress(data, dataLen, result.BeginWriting(), size,
                       &outputSize)) {
    return Err(NS_ERROR_UNEXPECTED);
  }

  MOZ_DIAGNOSTIC_ASSERT(size == outputSize);

  return std::move(result);
}

// Our zlib headers redefine this to MOZ_Z_compress, which breaks LZ4::compress
#undef compress

template <typename T>
static Result<nsCString, nsresult> EncodeLZ4(const nsACString& data,
                                             const T& magicNumber) {
  // Note: We want to include the null terminator here.
  nsDependentCSubstring magic(magicNumber, sizeof(magicNumber));

  nsAutoCString result;
  result.Append(magic);

  auto off = result.Length();
  if (!result.SetLength(off + 4, fallible)) {
    return Err(NS_ERROR_OUT_OF_MEMORY);
  }

  LittleEndian::writeUint32(result.BeginWriting() + off, data.Length());
  off += 4;

  auto size = LZ4::maxCompressedSize(data.Length());
  if (!result.SetLength(off + size, fallible)) {
    return Err(NS_ERROR_OUT_OF_MEMORY);
  }

  size = LZ4::compress(data.BeginReading(), data.Length(),
                       result.BeginWriting() + off);

  if (!result.SetLength(off + size, fallible)) {
    return Err(NS_ERROR_OUT_OF_MEMORY);
  }
  return std::move(result);
}

static_assert(sizeof STRUCTURED_CLONE_MAGIC % 8 == 0,
              "Magic number should be an array of uint64_t");

/**
 * Reads the contents of a LZ4-compressed file, as stored by the IOUtils
 * module, and returns the decompressed contents on success.
 */
static Result<nsCString, nsresult> ReadFileLZ4(nsIFile* file) {
  static const char MAGIC_NUMBER[] = "mozLz40";

  nsCString lz4 = MOZ_TRY(URLPreloader::ReadFile(file));

  if (lz4.IsEmpty()) {
    return lz4;
  }

  return DecodeLZ4(lz4, MAGIC_NUMBER);
}

static bool ParseJSON(JSContext* cx, nsACString& jsonData,
                      JS::MutableHandle<JS::Value> result) {
  NS_ConvertUTF8toUTF16 str(jsonData);
  jsonData.Truncate();

  return JS_ParseJSON(cx, str.Data(), str.Length(), result);
}

static Result<nsCOMPtr<nsIZipReaderCache>, nsresult> GetJarCache() {
  nsCOMPtr<nsIIOService> ios = components::IO::Service();
  NS_ENSURE_TRUE(ios, Err(NS_ERROR_FAILURE));

  nsCOMPtr<nsIProtocolHandler> jarProto;
  MOZ_TRY(ios->GetProtocolHandler("jar", getter_AddRefs(jarProto)));

  auto jar = static_cast<nsJARProtocolHandler*>(jarProto.get());
  MOZ_ASSERT(jar);

  nsCOMPtr<nsIZipReaderCache> zipCache = jar->JarCache();
  return std::move(zipCache);
}

static Result<FileLocation, nsresult> GetFileLocation(nsIURI* uri) {
  FileLocation location;

  nsCOMPtr<nsIFileURL> fileURL = do_QueryInterface(uri);
  nsCOMPtr<nsIFile> file;
  if (fileURL) {
    MOZ_TRY(fileURL->GetFile(getter_AddRefs(file)));
    location.Init(file);
  } else {
    nsCOMPtr<nsIURI> fileURI;
    nsCString entry;
    MOZ_TRY(ParseJARURI(uri, getter_AddRefs(fileURI), entry));

    file = MOZ_TRY(GetFile(fileURI));

    location.Init(file, entry);
  }

  return std::move(location);
}

/*****************************************************************************
 * JSON data handling
 *****************************************************************************/

class MOZ_STACK_CLASS WrapperBase {
 protected:
  WrapperBase(JSContext* cx, JSObject* object) : mCx(cx), mObject(cx, object) {}

  WrapperBase(JSContext* cx, const JS::Value& value) : mCx(cx), mObject(cx) {
    if (value.isObject()) {
      mObject = &value.toObject();
    } else {
      mObject = JS_NewPlainObject(cx);
    }
  }

 protected:
  JSContext* mCx;
  JS::Rooted<JSObject*> mObject;

  bool GetBool(const char* name, bool defVal = false);

  double GetNumber(const char* name, double defVal = 0);

  nsString GetString(const char* name, const char* defVal = "");

  JSObject* GetObject(const char* name);
};

bool WrapperBase::GetBool(const char* name, bool defVal) {
  JS::Rooted<JSObject*> obj(mCx, mObject);

  JS::Rooted<JS::Value> val(mCx, JS::UndefinedValue());
  if (!JS_GetProperty(mCx, obj, name, &val)) {
    JS_ClearPendingException(mCx);
  }

  if (val.isBoolean()) {
    return val.toBoolean();
  }
  return defVal;
}

double WrapperBase::GetNumber(const char* name, double defVal) {
  JS::Rooted<JSObject*> obj(mCx, mObject);

  JS::Rooted<JS::Value> val(mCx, JS::UndefinedValue());
  if (!JS_GetProperty(mCx, obj, name, &val)) {
    JS_ClearPendingException(mCx);
  }

  if (val.isNumber()) {
    return val.toNumber();
  }
  return defVal;
}

nsString WrapperBase::GetString(const char* name, const char* defVal) {
  JS::Rooted<JSObject*> obj(mCx, mObject);

  JS::Rooted<JS::Value> val(mCx, JS::UndefinedValue());
  if (!JS_GetProperty(mCx, obj, name, &val)) {
    JS_ClearPendingException(mCx);
  }

  nsString res;
  if (val.isString()) {
    AssignJSString(mCx, res, val.toString());
  } else {
    res.AppendASCII(defVal);
  }
  return res;
}

JSObject* WrapperBase::GetObject(const char* name) {
  JS::Rooted<JSObject*> obj(mCx, mObject);

  JS::Rooted<JS::Value> val(mCx, JS::UndefinedValue());
  if (!JS_GetProperty(mCx, obj, name, &val)) {
    JS_ClearPendingException(mCx);
  }

  if (val.isObject()) {
    return &val.toObject();
  }
  return nullptr;
}

class MOZ_STACK_CLASS InstallLocation : public WrapperBase {
 public:
  InstallLocation(JSContext* cx, const JS::Value& value);

  MOZ_IMPLICIT InstallLocation(PropertyIterElem& iter)
      : InstallLocation(iter.Cx(), iter.Value()) {}

  InstallLocation(const InstallLocation& other)
      : InstallLocation(other.mCx, JS::ObjectValue(*other.mObject)) {}

  void SetChanged(bool changed) {
    JS::Rooted<JSObject*> obj(mCx, mObject);

    JS::Rooted<JS::Value> val(mCx, JS::BooleanValue(changed));
    if (!JS_SetProperty(mCx, obj, "changed", val)) {
      JS_ClearPendingException(mCx);
    }
  }

  PropertyIter& Addons() { return mAddonsIter.ref(); }

  nsString Path() { return GetString("path"); }

  bool ShouldCheckStartupModifications() {
    return GetBool("checkStartupModifications");
  }

 private:
  JS::Rooted<JSObject*> mAddonsObj;
  Maybe<PropertyIter> mAddonsIter;
};

class MOZ_STACK_CLASS Addon : public WrapperBase {
 public:
  Addon(JSContext* cx, InstallLocation& location, const nsAString& id,
        JSObject* object)
      : WrapperBase(cx, object), mId(id), mLocation(location) {}

  MOZ_IMPLICIT Addon(PropertyIterElem& iter)
      : WrapperBase(iter.Cx(), iter.Value()),
        mId(iter.Name()),
        mLocation(*static_cast<InstallLocation*>(iter.Context())) {}

  Addon(const Addon& other)
      : WrapperBase(other.mCx, other.mObject),
        mId(other.mId),
        mLocation(other.mLocation) {}

  const nsString& Id() { return mId; }

  nsString Path() { return GetString("path"); }

  nsString Type() { return GetString("type", "extension"); }

  bool Enabled() { return GetBool("enabled"); }

  double LastModifiedTime() { return GetNumber("lastModifiedTime"); }

  bool ShouldCheckStartupModifications() {
    return Type().EqualsLiteral("locale");
  }

  Result<nsCOMPtr<nsIFile>, nsresult> FullPath();

  Result<bool, nsresult> UpdateLastModifiedTime();

 private:
  nsString mId;
  InstallLocation& mLocation;
};

Result<nsCOMPtr<nsIFile>, nsresult> Addon::FullPath() {
  nsString path = Path();

  // First check for an absolute path, in case we have a proxy file.
  nsCOMPtr<nsIFile> file;
  if (NS_SUCCEEDED(NS_NewLocalFile(path, getter_AddRefs(file)))) {
    return std::move(file);
  }

  // If not an absolute path, fall back to a relative path from the location.
  MOZ_TRY(NS_NewLocalFile(mLocation.Path(), getter_AddRefs(file)));

  MOZ_TRY(file->AppendRelativePath(path));
  return std::move(file);
}

Result<bool, nsresult> Addon::UpdateLastModifiedTime() {
  nsCOMPtr<nsIFile> file = MOZ_TRY(FullPath());

  JS::Rooted<JSObject*> obj(mCx, mObject);

  bool result;
  if (NS_FAILED(file->Exists(&result)) || !result) {
    JS::Rooted<JS::Value> value(mCx, JS::NullValue());
    if (!JS_SetProperty(mCx, obj, "currentModifiedTime", value)) {
      JS_ClearPendingException(mCx);
    }

    return true;
  }

  PRTime time;

  nsCOMPtr<nsIFile> manifest = file;
  if (!IsNormalFile(manifest)) {
    manifest = CloneAndAppend(file, "manifest.json");
    if (!IsNormalFile(manifest)) {
      return true;
    }
  }

  if (NS_FAILED(manifest->GetLastModifiedTime(&time))) {
    return true;
  }

  double lastModified = time;
  JS::Rooted<JS::Value> value(mCx, JS::NumberValue(lastModified));
  if (!JS_SetProperty(mCx, obj, "currentModifiedTime", value)) {
    JS_ClearPendingException(mCx);
  }

  return lastModified != LastModifiedTime();
}

InstallLocation::InstallLocation(JSContext* cx, const JS::Value& value)
    : WrapperBase(cx, value), mAddonsObj(cx) {
  mAddonsObj = GetObject("addons");
  if (!mAddonsObj) {
    mAddonsObj = JS_NewPlainObject(cx);
  }
  mAddonsIter.emplace(cx, mAddonsObj, this);
}

/*****************************************************************************
 * XPC interfacing
 *****************************************************************************/

nsresult AddonManagerStartup::ReadStartupData(
    JSContext* cx, JS::MutableHandle<JS::Value> locations) {
  locations.set(JS::UndefinedValue());

  nsCOMPtr<nsIFile> file =
      CloneAndAppend(ProfileDir(), "addonStartup.json.lz4");

  nsCString data;
  auto res = ReadFileLZ4(file);
  if (res.isOk()) {
    data = res.unwrap();
  } else if (res.inspectErr() != NS_ERROR_FILE_NOT_FOUND) {
    return res.unwrapErr();
  }

  if (data.IsEmpty() || !ParseJSON(cx, data, locations)) {
    return NS_OK;
  }

  if (!locations.isObject()) {
    return NS_ERROR_UNEXPECTED;
  }

  JS::Rooted<JSObject*> locs(cx, &locations.toObject());
  for (auto e1 : PropertyIter(cx, locs)) {
    InstallLocation loc(e1);

    bool shouldCheck = loc.ShouldCheckStartupModifications();

    for (auto e2 : loc.Addons()) {
      Addon addon(e2);

      if (addon.Enabled() &&
          (shouldCheck || addon.ShouldCheckStartupModifications())) {
        bool changed = MOZ_TRY(addon.UpdateLastModifiedTime());
        if (changed) {
          loc.SetChanged(true);
        }
      }
    }
  }

  return NS_OK;
}

nsresult AddonManagerStartup::EncodeBlob(JS::Handle<JS::Value> value,
                                         JSContext* cx,
                                         JS::MutableHandle<JS::Value> result) {
  auto holder = MakeRefPtr<StructuredCloneData>(
      JS::StructuredCloneScope::DifferentProcess,
      dom::StructuredCloneHolder::TransferringNotSupported);

  ErrorResult rv;
  holder->Write(cx, value, rv);
  rv.WouldReportJSException();
  if (rv.Failed()) {
    return rv.StealNSResult();
  }

  nsAutoCString scData;

  bool ok = holder->BufferData().ForEachDataChunk(
      [&](const char* aData, size_t aSize) {
        return scData.Append(nsDependentCSubstring(aData, aSize),
                             mozilla::fallible);
      });
  NS_ENSURE_TRUE(ok, NS_ERROR_OUT_OF_MEMORY);

  nsCString lz4 = MOZ_TRY(EncodeLZ4(scData, STRUCTURED_CLONE_MAGIC));

  JS::Rooted<JSObject*> obj(cx, dom::ArrayBuffer::Create(cx, lz4, rv));
  RETURN_NSRESULT_ON_FAILURE(rv);

  result.set(JS::ObjectValue(*obj));
  return NS_OK;
}

nsresult AddonManagerStartup::DecodeBlob(JS::Handle<JS::Value> value,
                                         JSContext* cx,
                                         JS::MutableHandle<JS::Value> result) {
  NS_ENSURE_TRUE(value.isObject() &&
                     JS::IsArrayBufferObject(&value.toObject()) &&
                     JS::ArrayBufferHasData(&value.toObject()),
                 NS_ERROR_INVALID_ARG);

  nsCString data;
  {
    JS::AutoCheckCannotGC nogc;

    auto obj = &value.toObject();
    bool isShared;

    size_t len = JS::GetArrayBufferByteLength(obj);
    NS_ENSURE_TRUE(len <= INT32_MAX, NS_ERROR_INVALID_ARG);
    nsDependentCSubstring lz4(
        reinterpret_cast<char*>(JS::GetArrayBufferData(obj, &isShared, nogc)),
        uint32_t(len));

    data = MOZ_TRY(DecodeLZ4(lz4, STRUCTURED_CLONE_MAGIC));
  }

  auto holder = MakeRefPtr<StructuredCloneData>(
      JS::StructuredCloneScope::DifferentProcess,
      dom::StructuredCloneHolder::TransferringNotSupported);
  // FIXME: This currently assumes the data on disk was serialized with the same
  // JS_STRUCTURED_CLONE_VERSION as the current binary. This should probably be
  // improved to avoid migration issues if JS_STRUCTURED_CLONE_VERSION is bumped
  // in the future.
  // See https://bugzilla.mozilla.org/show_bug.cgi?id=2014441
  bool ok = holder->CopyExternalData(data.get(), data.Length(),
                                     JS_STRUCTURED_CLONE_VERSION);
  NS_ENSURE_TRUE(ok, NS_ERROR_OUT_OF_MEMORY);

  ErrorResult rv;
  holder->Read(cx, result, rv);
  return rv.StealNSResult();
}

static nsresult EnumerateZip(nsIZipReader* zip, const nsACString& pattern,
                             nsTArray<nsString>& results) {
  nsCOMPtr<nsIUTF8StringEnumerator> entries;
  MOZ_TRY(zip->FindEntries(pattern, getter_AddRefs(entries)));

  bool hasMore;
  while (NS_SUCCEEDED(entries->HasMore(&hasMore)) && hasMore) {
    nsAutoCString name;
    MOZ_TRY(entries->GetNext(name));

    results.AppendElement(NS_ConvertUTF8toUTF16(name));
  }

  return NS_OK;
}

nsresult AddonManagerStartup::EnumerateJAR(nsIURI* uri,
                                           const nsACString& pattern,
                                           nsTArray<nsString>& results) {
  nsCOMPtr<nsIZipReaderCache> zipCache = MOZ_TRY(GetJarCache());

  nsCOMPtr<nsIZipReader> zip;
  nsCOMPtr<nsIFile> file;
  if (nsCOMPtr<nsIJARURI> jarURI = do_QueryInterface(uri)) {
    nsCOMPtr<nsIURI> fileURI;
    nsCString entry;
    MOZ_TRY(ParseJARURI(jarURI, getter_AddRefs(fileURI), entry));

    file = MOZ_TRY(GetFile(fileURI));
    MOZ_TRY(
        zipCache->GetInnerZip(file, Substring(entry, 1), getter_AddRefs(zip)));
  } else {
    file = MOZ_TRY(GetFile(uri));
    MOZ_TRY(zipCache->GetZip(file, getter_AddRefs(zip)));
  }
  MOZ_ASSERT(zip);

  return EnumerateZip(zip, pattern, results);
}

nsresult AddonManagerStartup::EnumerateJARSubtree(nsIURI* uri,
                                                  nsTArray<nsString>& results) {
  nsCOMPtr<nsIURI> fileURI;
  nsCString entry;
  MOZ_TRY(ParseJARURI(uri, getter_AddRefs(fileURI), entry));

  // Mangle the path into a pattern to match all child entries by escaping any
  // existing pattern matching metacharacters it contains and appending "/*".
  constexpr auto metaChars = "[]()?*~|$\\"_ns;

  nsCString pattern;
  pattern.SetCapacity(entry.Length());

  // The first character of the entry name is "/", which we want to skip.
  for (auto chr : Span(Substring(entry, 1))) {
    if (metaChars.FindChar(chr) >= 0) {
      pattern.Append('\\');
    }
    pattern.Append(chr);
  }
  if (!pattern.IsEmpty() && !StringEndsWith(pattern, "/"_ns)) {
    pattern.Append('/');
  }
  pattern.Append('*');

  return EnumerateJAR(fileURI, pattern, results);
}

nsresult AddonManagerStartup::InitializeURLPreloader() {
  MOZ_RELEASE_ASSERT(xpc::IsInAutomation());

  URLPreloader::ReInitialize();

  return NS_OK;
}

/******************************************************************************
 * RegisterChrome
 ******************************************************************************/

namespace {
static bool sObserverRegistered;

struct ContentEntry final {
  explicit ContentEntry(nsTArray<nsCString>&& aArgs, uint8_t aFlags = 0)
      : mArgs(std::move(aArgs)), mFlags(aFlags) {}

  AutoTArray<nsCString, 2> mArgs;
  uint8_t mFlags;
};

};  // anonymous namespace
};  // namespace mozilla

MOZ_DECLARE_RELOCATE_USING_MOVE_CONSTRUCTOR(mozilla::ContentEntry);

namespace mozilla {
namespace {

class RegistryEntries final : public nsIJSRAIIHelper,
                              public LinkedListElement<RegistryEntries> {
 public:
  NS_DECL_ISUPPORTS
  NS_DECL_NSIJSRAIIHELPER

  using Override = AutoTArray<nsCString, 2>;
  using Locale = AutoTArray<nsCString, 3>;

  RegistryEntries(FileLocation& location, nsTArray<Override>&& overrides,
                  nsTArray<ContentEntry>&& content, nsTArray<Locale>&& locales)
      : mLocation(location),
        mOverrides(std::move(overrides)),
        mContent(std::move(content)),
        mLocales(std::move(locales)) {}

  void Register();

 protected:
  virtual ~RegistryEntries() { (void)Destruct(); }

 private:
  FileLocation mLocation;
  const nsTArray<Override> mOverrides;
  const nsTArray<ContentEntry> mContent;
  const nsTArray<Locale> mLocales;
};

NS_IMPL_ISUPPORTS(RegistryEntries, nsIJSRAIIHelper)

void RegistryEntries::Register() {
  RefPtr<nsChromeRegistry> cr = nsChromeRegistry::GetSingleton();

  nsChromeRegistry::ManifestProcessingContext context(NS_EXTENSION_LOCATION,
                                                      mLocation);

  for (auto& override : mOverrides) {
    const char* args[] = {override[0].get(), override[1].get()};
    cr->ManifestOverride(context, 0, const_cast<char**>(args), 0);
  }

  for (auto& content : mContent) {
    const char* args[] = {content.mArgs[0].get(), content.mArgs[1].get()};
    cr->ManifestContent(context, 0, const_cast<char**>(args), content.mFlags);
  }

  for (auto& locale : mLocales) {
    const char* args[] = {locale[0].get(), locale[1].get(), locale[2].get()};
    cr->ManifestLocale(context, 0, const_cast<char**>(args), 0);
  }
}

NS_IMETHODIMP
RegistryEntries::Destruct() {
  if (isInList()) {
    remove();

    // No point in doing I/O to check for new chrome during shutdown, return
    // early in that case.
    if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) {
      return NS_OK;
    }

    // When we remove dynamic entries from the registry, we need to rebuild it
    // in order to ensure a consistent state. See comments in Observe().
    RefPtr<nsChromeRegistry> cr = nsChromeRegistry::GetSingleton();
    return cr->CheckForNewChrome();
  }
  return NS_OK;
}

static LinkedList<RegistryEntries>& GetRegistryEntries() {
  static LinkedList<RegistryEntries> sEntries;
  return sEntries;
}
};  // anonymous namespace

NS_IMETHODIMP
AddonManagerStartup::RegisterChrome(nsIURI* manifestURI,
                                    JS::Handle<JS::Value> locations,
                                    JSContext* cx, nsIJSRAIIHelper** result) {
  auto IsArray = [cx](JS::Handle<JS::Value> val) -> bool {
    bool isArray;
    return JS::IsArrayObject(cx, val, &isArray) && isArray;
  };

  NS_ENSURE_ARG_POINTER(manifestURI);
  NS_ENSURE_TRUE(IsArray(locations), NS_ERROR_INVALID_ARG);

  FileLocation location = MOZ_TRY(GetFileLocation(manifestURI));

  nsTArray<RegistryEntries::Locale> locales;
  nsTArray<ContentEntry> content;
  nsTArray<RegistryEntries::Override> overrides;

  JS::Rooted<JSObject*> locs(cx, &locations.toObject());
  JS::Rooted<JS::Value> arrayVal(cx);
  JS::Rooted<JSObject*> array(cx);

  for (auto elem : ArrayIter(cx, locs)) {
    arrayVal = elem.Value();
    NS_ENSURE_TRUE(IsArray(arrayVal), NS_ERROR_INVALID_ARG);

    array = &arrayVal.toObject();

    AutoTArray<nsCString, 4> vals;
    for (auto val : ArrayIter(cx, array)) {
      nsAutoJSString str;
      NS_ENSURE_TRUE(str.init(cx, val.Value()), NS_ERROR_OUT_OF_MEMORY);

      vals.AppendElement(NS_ConvertUTF16toUTF8(str));
    }
    NS_ENSURE_TRUE(vals.Length() > 0, NS_ERROR_INVALID_ARG);

    nsCString type = vals[0];
    vals.RemoveElementAt(0);

    if (type.EqualsLiteral("override")) {
      NS_ENSURE_TRUE(vals.Length() == 2, NS_ERROR_INVALID_ARG);
      overrides.AppendElement(std::move(vals));
    } else if (type.EqualsLiteral("content")) {
      if (vals.Length() == 3 &&
          vals[2].EqualsLiteral("contentaccessible=yes")) {
        vals.RemoveElementAt(2);
        content.AppendElement(ContentEntry(
            std::move(vals), nsChromeRegistry::CONTENT_ACCESSIBLE));
      } else {
        NS_ENSURE_TRUE(vals.Length() == 2, NS_ERROR_INVALID_ARG);
        content.AppendElement(ContentEntry(std::move(vals)));
      }
    } else if (type.EqualsLiteral("locale")) {
      NS_ENSURE_TRUE(vals.Length() == 3, NS_ERROR_INVALID_ARG);
      locales.AppendElement(std::move(vals));
    } else {
      return NS_ERROR_INVALID_ARG;
    }
  }

  if (!sObserverRegistered) {
    nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
    NS_ENSURE_TRUE(obs, NS_ERROR_UNEXPECTED);
    obs->AddObserver(this, "chrome-manifests-loaded", false);

    sObserverRegistered = true;
  }

  auto entry = MakeRefPtr<RegistryEntries>(
      location, std::move(overrides), std::move(content), std::move(locales));

  entry->Register();
  GetRegistryEntries().insertBack(entry);

  entry.forget(result);
  return NS_OK;
}

NS_IMETHODIMP
AddonManagerStartup::Observe(nsISupports* subject, const char* topic,
                             const char16_t* data) {
  // The chrome registry is maintained as a set of global resource mappings
  // generated mainly from manifest files, on-the-fly, as they're parsed.
  // Entries added later override entries added earlier, and no record is kept
  // of the former state.
  //
  // As a result, if we remove a dynamically-added manifest file, or a set of
  // dynamic entries, the registry needs to be rebuilt from scratch, from the
  // manifests and dynamic entries that remain. The chrome registry itself
  // takes care of re-parsing manifes files. This observer notification lets
  // us know when we need to re-register our dynamic entries.
  if (!strcmp(topic, "chrome-manifests-loaded")) {
    for (auto entry : GetRegistryEntries()) {
      entry->Register();
    }
  }

  return NS_OK;
}

}  // namespace mozilla
