/* -*- 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/ArrayUtils.h"
#include "mozilla/Printf.h"
#include "mozilla/UniquePtr.h"

#include "ManifestParser.h"

#include <string.h>

#include "prio.h"
#if defined(XP_WIN)
#  include <windows.h>
#elif defined(MOZ_WIDGET_COCOA)
#  include <CoreServices/CoreServices.h>
#  include "nsCocoaFeatures.h"
#elif defined(MOZ_WIDGET_GTK)
#  include <gtk/gtk.h>
#endif

#ifdef MOZ_WIDGET_ANDROID
#  include "AndroidBuild.h"
#  include "mozilla/java/GeckoAppShellWrappers.h"
#endif

#ifdef MOZ_BACKGROUNDTASKS
#  include "mozilla/BackgroundTasks.h"
#endif

#include "mozilla/Services.h"

#include "nsCRT.h"
#include "nsConsoleMessage.h"
#include "nsTextFormatter.h"
#include "nsVersionComparator.h"
#include "nsXPCOMCIDInternal.h"

#include "nsIConsoleService.h"
#include "nsIScriptError.h"
#include "nsIXULAppInfo.h"
#include "nsIXULRuntime.h"

using namespace mozilla;

struct ManifestDirective {
  const char* directive;
  int argc;

  bool ischrome;

  // The contentaccessible flags only apply to content/resource directives.
  bool contentflags;

  // Function to handle this directive. This isn't a union because C++ still
  // hasn't learned how to initialize unions in a sane way.
  void (nsComponentManagerImpl::*mgrfunc)(
      nsComponentManagerImpl::ManifestProcessingContext& aCx, int aLineNo,
      char* const* aArgv);
  void (nsChromeRegistry::*regfunc)(
      nsChromeRegistry::ManifestProcessingContext& aCx, int aLineNo,
      char* const* aArgv, int aFlags);
};
static const ManifestDirective kParsingTable[] = {
    // clang-format off
  {
    "manifest",         1, true, false,
    &nsComponentManagerImpl::ManifestManifest, nullptr,
  },
  {
    "category",         3, false, false,
    &nsComponentManagerImpl::ManifestCategory, nullptr,
  },
  {
    "content",          2, true,  true,
    nullptr, &nsChromeRegistry::ManifestContent,
  },
  {
    "locale",           3, true, false,
    nullptr, &nsChromeRegistry::ManifestLocale,
  },
  {
    "skin",             3, true, false,
    nullptr, &nsChromeRegistry::ManifestSkin,
  },
  {
    // NB: note that while skin manifests can use this, they are only allowed
    // to use it for chrome://../skin/ URLs
    "override",         2, true, false,
    nullptr, &nsChromeRegistry::ManifestOverride,
  },
  {
    "resource",         2, false, true,
    nullptr, &nsChromeRegistry::ManifestResource,
  }
    // clang-format on
};

static const char kWhitespace[] = "\t ";

static bool IsNewline(char aChar) { return aChar == '\n' || aChar == '\r'; }

void LogMessage(const char* aMsg, ...) {
  MOZ_ASSERT(nsComponentManagerImpl::gComponentManager);

  nsCOMPtr<nsIConsoleService> console =
      do_GetService(NS_CONSOLESERVICE_CONTRACTID);
  if (!console) {
    return;
  }

  va_list args;
  va_start(args, aMsg);
  SmprintfPointer formatted(mozilla::Vsmprintf(aMsg, args));
  va_end(args);

  nsCOMPtr<nsIConsoleMessage> error =
      new nsConsoleMessage(NS_ConvertUTF8toUTF16(formatted.get()));
  console->LogMessage(error);
}

void LogMessageWithContext(FileLocation& aFile, uint32_t aLineNumber,
                           const char* aMsg, ...) {
  va_list args;
  va_start(args, aMsg);
  SmprintfPointer formatted(mozilla::Vsmprintf(aMsg, args));
  va_end(args);
  if (!formatted) {
    return;
  }

  MOZ_ASSERT(nsComponentManagerImpl::gComponentManager);

  nsCString file;
  aFile.GetURIString(file);

  nsCOMPtr<nsIScriptError> error = do_CreateInstance(NS_SCRIPTERROR_CONTRACTID);
  if (!error) {
    // This can happen early in component registration. Fall back to a
    // generic console message.
    LogMessage("Warning: in '%s', line %i: %s", file.get(), aLineNumber,
               formatted.get());
    return;
  }

  nsCOMPtr<nsIConsoleService> console =
      do_GetService(NS_CONSOLESERVICE_CONTRACTID);
  if (!console) {
    return;
  }

  nsresult rv = error->Init(
      NS_ConvertUTF8toUTF16(formatted.get()), NS_ConvertUTF8toUTF16(file),
      u""_ns, aLineNumber, 0, nsIScriptError::warningFlag,
      "chrome registration"_ns, false /* from private window */,
      true /* from chrome context */);
  if (NS_FAILED(rv)) {
    return;
  }

  console->LogMessage(error);
}

/**
 * Check for a modifier flag of the following forms:
 *   "flag"   (same as "true")
 *   "flag=yes|true|1"
 *   "flag="no|false|0"
 * @param aFlag The flag to compare.
 * @param aData The tokenized data to check; this is lowercased
 *              before being passed in.
 * @param aResult If the flag is found, the value is assigned here.
 * @return Whether the flag was handled.
 */
static bool CheckFlag(const nsAString& aFlag, const nsAString& aData,
                      bool& aResult) {
  if (!StringBeginsWith(aData, aFlag)) {
    return false;
  }

  if (aFlag.Length() == aData.Length()) {
    // the data is simply "flag", which is the same as "flag=yes"
    aResult = true;
    return true;
  }

  if (aData.CharAt(aFlag.Length()) != '=') {
    // the data is "flag2=", which is not anything we care about
    return false;
  }

  if (aData.Length() == aFlag.Length() + 1) {
    aResult = false;
    return true;
  }

  switch (aData.CharAt(aFlag.Length() + 1)) {
    case '1':
    case 't':  // true
    case 'y':  // yes
      aResult = true;
      return true;

    case '0':
    case 'f':  // false
    case 'n':  // no
      aResult = false;
      return true;
  }

  return false;
}

enum TriState { eUnspecified, eBad, eOK };

/**
 * Check for a modifier flag of the following form:
 *   "flag=string"
 *   "flag!=string"
 * @param aFlag The flag to compare.
 * @param aData The tokenized data to check; this is lowercased
 *              before being passed in.
 * @param aValue The value that is expected.
 * @param aResult If this is "ok" when passed in, this is left alone.
 *                Otherwise if the flag is found it is set to eBad or eOK.
 * @return Whether the flag was handled.
 */
static bool CheckStringFlag(const nsAString& aFlag, const nsAString& aData,
                            const nsAString& aValue, TriState& aResult) {
  if (aData.Length() < aFlag.Length() + 1) {
    return false;
  }

  if (!StringBeginsWith(aData, aFlag)) {
    return false;
  }

  bool comparison = true;
  if (aData[aFlag.Length()] != '=') {
    if (aData[aFlag.Length()] == '!' && aData.Length() >= aFlag.Length() + 2 &&
        aData[aFlag.Length() + 1] == '=') {
      comparison = false;
    } else {
      return false;
    }
  }

  if (aResult != eOK) {
    nsDependentSubstring testdata =
        Substring(aData, aFlag.Length() + (comparison ? 1 : 2));
    if (testdata.Equals(aValue)) {
      aResult = comparison ? eOK : eBad;
    } else {
      aResult = comparison ? eBad : eOK;
    }
  }

  return true;
}

static bool CheckOsFlag(const nsAString& aFlag, const nsAString& aData,
                        const nsAString& aValue, TriState& aResult) {
  bool result = CheckStringFlag(aFlag, aData, aValue, aResult);
#if defined(XP_UNIX) && !defined(XP_DARWIN) && !defined(ANDROID)
  if (result && aResult == eBad) {
    result = CheckStringFlag(aFlag, aData, u"likeunix"_ns, aResult);
  }
#endif
  return result;
}

/**
 * Check for a modifier flag of the following form:
 *   "flag=version"
 *   "flag<=version"
 *   "flag<version"
 *   "flag>=version"
 *   "flag>version"
 * @param aFlag The flag to compare.
 * @param aData The tokenized data to check; this is lowercased
 *              before being passed in.
 * @param aValue The value that is expected. If this is empty then no
 *               comparison will match.
 * @param aResult If this is eOK when passed in, this is left alone.
 *                Otherwise if the flag is found it is set to eBad or eOK.
 * @return Whether the flag was handled.
 */

#define COMPARE_EQ 1 << 0
#define COMPARE_LT 1 << 1
#define COMPARE_GT 1 << 2

static bool CheckVersionFlag(const nsString& aFlag, const nsString& aData,
                             const nsString& aValue, TriState& aResult) {
  if (aData.Length() < aFlag.Length() + 2) {
    return false;
  }

  if (!StringBeginsWith(aData, aFlag)) {
    return false;
  }

  if (aValue.Length() == 0) {
    if (aResult != eOK) {
      aResult = eBad;
    }
    return true;
  }

  uint32_t comparison;
  nsAutoString testdata;

  switch (aData[aFlag.Length()]) {
    case '=':
      comparison = COMPARE_EQ;
      testdata = Substring(aData, aFlag.Length() + 1);
      break;

    case '<':
      if (aData[aFlag.Length() + 1] == '=') {
        comparison = COMPARE_EQ | COMPARE_LT;
        testdata = Substring(aData, aFlag.Length() + 2);
      } else {
        comparison = COMPARE_LT;
        testdata = Substring(aData, aFlag.Length() + 1);
      }
      break;

    case '>':
      if (aData[aFlag.Length() + 1] == '=') {
        comparison = COMPARE_EQ | COMPARE_GT;
        testdata = Substring(aData, aFlag.Length() + 2);
      } else {
        comparison = COMPARE_GT;
        testdata = Substring(aData, aFlag.Length() + 1);
      }
      break;

    default:
      return false;
  }

  if (testdata.Length() == 0) {
    return false;
  }

  if (aResult != eOK) {
    int32_t c = mozilla::CompareVersions(NS_ConvertUTF16toUTF8(aValue).get(),
                                         NS_ConvertUTF16toUTF8(testdata).get());
    if ((c == 0 && comparison & COMPARE_EQ) ||
        (c < 0 && comparison & COMPARE_LT) ||
        (c > 0 && comparison & COMPARE_GT)) {
      aResult = eOK;
    } else {
      aResult = eBad;
    }
  }

  return true;
}

// In-place conversion of ascii characters to lower case
static void ToLowerCase(char* aToken) {
  for (; *aToken; ++aToken) {
    *aToken = NS_ToLower(*aToken);
  }
}

namespace {

struct CachedDirective {
  int lineno;
  char* argv[4];
};

}  // namespace

void ParseManifest(NSLocationType aType, FileLocation& aFile, char* aBuf,
                   bool aChromeOnly) {
  nsComponentManagerImpl::ManifestProcessingContext mgrcx(aType, aFile,
                                                          aChromeOnly);
  nsChromeRegistry::ManifestProcessingContext chromecx(aType, aFile);
  nsresult rv;

  constexpr auto kContentAccessible = u"contentaccessible"_ns;
  constexpr auto kRemoteEnabled = u"remoteenabled"_ns;
  constexpr auto kRemoteRequired = u"remoterequired"_ns;
  constexpr auto kApplication = u"application"_ns;
  constexpr auto kAppVersion = u"appversion"_ns;
  constexpr auto kGeckoVersion = u"platformversion"_ns;
  constexpr auto kOs = u"os"_ns;
  constexpr auto kOsVersion = u"osversion"_ns;
  constexpr auto kABI = u"abi"_ns;
  constexpr auto kProcess = u"process"_ns;
#if defined(MOZ_WIDGET_ANDROID)
  constexpr auto kTablet = u"tablet"_ns;
#endif
  // You might expect this to be guarded by MOZ_BACKGROUNDTASKS, but it's not
  // possible to have conditional manifest contents, so we need to recognize and
  // discard these tokens even when MOZ_BACKGROUNDTASKS is not set.
  constexpr auto kBackgroundTask = u"backgroundtask"_ns;

  constexpr auto kMain = u"main"_ns;
  constexpr auto kContent = u"content"_ns;

  // Obsolete
  constexpr auto kXPCNativeWrappers = u"xpcnativewrappers"_ns;

  nsAutoString appID;
  nsAutoString appVersion;
  nsAutoString geckoVersion;
  nsAutoString osTarget;
  nsAutoString abi;
  nsAutoString process;

  nsCOMPtr<nsIXULAppInfo> xapp(do_GetService(XULAPPINFO_SERVICE_CONTRACTID));
  if (xapp) {
    nsAutoCString s;
    rv = xapp->GetID(s);
    if (NS_SUCCEEDED(rv)) {
      CopyUTF8toUTF16(s, appID);
    }

    rv = xapp->GetVersion(s);
    if (NS_SUCCEEDED(rv)) {
      CopyUTF8toUTF16(s, appVersion);
    }

    rv = xapp->GetPlatformVersion(s);
    if (NS_SUCCEEDED(rv)) {
      CopyUTF8toUTF16(s, geckoVersion);
    }

    nsCOMPtr<nsIXULRuntime> xruntime(do_QueryInterface(xapp));
    if (xruntime) {
      rv = xruntime->GetOS(s);
      if (NS_SUCCEEDED(rv)) {
        ToLowerCase(s);
        CopyUTF8toUTF16(s, osTarget);
      }

      rv = xruntime->GetXPCOMABI(s);
      if (NS_SUCCEEDED(rv) && osTarget.Length()) {
        ToLowerCase(s);
        CopyUTF8toUTF16(s, abi);
        abi.Insert(char16_t('_'), 0);
        abi.Insert(osTarget, 0);
      }
    }
  }

  nsAutoString osVersion;
#if defined(XP_WIN)
#  pragma warning(push)
#  pragma warning(disable : 4996)  // VC12+ deprecates GetVersionEx
  OSVERSIONINFO info = {sizeof(OSVERSIONINFO)};
  if (GetVersionEx(&info)) {
    nsTextFormatter::ssprintf(osVersion, u"%ld.%ld", info.dwMajorVersion,
                              info.dwMinorVersion);
  }
#  pragma warning(pop)
#elif defined(MOZ_WIDGET_COCOA)
  SInt32 majorVersion = nsCocoaFeatures::macOSVersionMajor();
  SInt32 minorVersion = nsCocoaFeatures::macOSVersionMinor();
  nsTextFormatter::ssprintf(osVersion, u"%ld.%ld", majorVersion, minorVersion);
#elif defined(MOZ_WIDGET_GTK)
  nsTextFormatter::ssprintf(osVersion, u"%ld.%ld", gtk_major_version,
                            gtk_minor_version);
#elif defined(MOZ_WIDGET_ANDROID)
  bool isTablet = false;
  if (jni::IsAvailable()) {
    jni::String::LocalRef release = java::sdk::Build::VERSION::RELEASE();
    osVersion.Assign(release->ToString());
    isTablet = java::GeckoAppShell::IsTablet();
  }
#endif

  if (XRE_IsContentProcess()) {
    process = kContent;
  } else {
    process = kMain;
  }

  char* token;
  char* newline = aBuf;
  uint32_t line = 0;

  // outer loop tokenizes by newline
  while (*newline) {
    while (*newline && IsNewline(*newline)) {
      ++newline;
      ++line;
    }
    if (!*newline) {
      break;
    }

    token = newline;
    while (*newline && !IsNewline(*newline)) {
      ++newline;
    }

    if (*newline) {
      *newline = '\0';
      ++newline;
    }
    ++line;

    if (*token == '#') {  // ignore lines that begin with # as comments
      continue;
    }

    char* whitespace = token;
    token = nsCRT::strtok(whitespace, kWhitespace, &whitespace);
    if (!token) {
      continue;
    }

    const ManifestDirective* directive = nullptr;
    for (const ManifestDirective* d = kParsingTable;
         d < ArrayEnd(kParsingTable); ++d) {
      if (!strcmp(d->directive, token)) {
        directive = d;
        break;
      }
    }

    if (!directive) {
      LogMessageWithContext(
          aFile, line, "Ignoring unrecognized chrome manifest directive '%s'.",
          token);
      continue;
    }

    if (!directive->ischrome && NS_BOOTSTRAPPED_LOCATION == aType) {
      LogMessageWithContext(
          aFile, line,
          "Bootstrapped manifest not allowed to use '%s' directive.", token);
      continue;
    }

    NS_ASSERTION(directive->argc < 4, "Need to reset argv array length");
    char* argv[4];
    for (int i = 0; i < directive->argc; ++i) {
      argv[i] = nsCRT::strtok(whitespace, kWhitespace, &whitespace);
    }

    if (!argv[directive->argc - 1]) {
      LogMessageWithContext(aFile, line,
                            "Not enough arguments for chrome manifest "
                            "directive '%s', expected %i.",
                            token, directive->argc);
      continue;
    }

    bool ok = true;
    TriState stAppVersion = eUnspecified;
    TriState stGeckoVersion = eUnspecified;
    TriState stApp = eUnspecified;
    TriState stOsVersion = eUnspecified;
    TriState stOs = eUnspecified;
    TriState stABI = eUnspecified;
    TriState stProcess = eUnspecified;
#if defined(MOZ_WIDGET_ANDROID)
    TriState stTablet = eUnspecified;
#endif
#ifdef MOZ_BACKGROUNDTASKS
    // When in background task mode, default to not registering
    // category directivies unless backgroundtask=1 is specified.
    TriState stBackgroundTask = (BackgroundTasks::IsBackgroundTaskMode() &&
                                 strcmp("category", directive->directive) == 0)
                                    ? eBad
                                    : eUnspecified;
#endif
    int flags = 0;

    while ((token = nsCRT::strtok(whitespace, kWhitespace, &whitespace)) &&
           ok) {
      ToLowerCase(token);
      NS_ConvertASCIItoUTF16 wtoken(token);

      if (CheckStringFlag(kApplication, wtoken, appID, stApp) ||
          CheckOsFlag(kOs, wtoken, osTarget, stOs) ||
          CheckStringFlag(kABI, wtoken, abi, stABI) ||
          CheckStringFlag(kProcess, wtoken, process, stProcess) ||
          CheckVersionFlag(kOsVersion, wtoken, osVersion, stOsVersion) ||
          CheckVersionFlag(kAppVersion, wtoken, appVersion, stAppVersion) ||
          CheckVersionFlag(kGeckoVersion, wtoken, geckoVersion,
                           stGeckoVersion)) {
        continue;
      }

#if defined(MOZ_WIDGET_ANDROID)
      bool tablet = false;
      if (CheckFlag(kTablet, wtoken, tablet)) {
        stTablet = (tablet == isTablet) ? eOK : eBad;
        continue;
      }
#endif

      // You might expect this to be guarded by MOZ_BACKGROUNDTASKS, it's not
      // possible to have conditional manifest contents.
      bool flag;
      if (CheckFlag(kBackgroundTask, wtoken, flag)) {
#if defined(MOZ_BACKGROUNDTASKS)
        // Background task mode is active: filter.
        stBackgroundTask =
            (flag == BackgroundTasks::IsBackgroundTaskMode()) ? eOK : eBad;
#endif /* defined(MOZ_BACKGROUNDTASKS) */
        continue;
      }

      if (directive->contentflags) {
        bool flag;
        if (CheckFlag(kContentAccessible, wtoken, flag)) {
          if (flag) flags |= nsChromeRegistry::CONTENT_ACCESSIBLE;
          continue;
        }
        if (CheckFlag(kRemoteEnabled, wtoken, flag)) {
          if (flag) flags |= nsChromeRegistry::REMOTE_ALLOWED;
          continue;
        }
        if (CheckFlag(kRemoteRequired, wtoken, flag)) {
          if (flag) flags |= nsChromeRegistry::REMOTE_REQUIRED;
          continue;
        }
      }

      bool xpcNativeWrappers = true;  // Dummy for CheckFlag.
      if (CheckFlag(kXPCNativeWrappers, wtoken, xpcNativeWrappers)) {
        LogMessageWithContext(
            aFile, line, "Ignoring obsolete chrome registration modifier '%s'.",
            token);
        continue;
      }

      LogMessageWithContext(
          aFile, line, "Unrecognized chrome manifest modifier '%s'.", token);
      ok = false;
    }

    if (!ok || stApp == eBad || stAppVersion == eBad ||
        stGeckoVersion == eBad || stOs == eBad || stOsVersion == eBad ||
#ifdef MOZ_WIDGET_ANDROID
        stTablet == eBad ||
#endif
#ifdef MOZ_BACKGROUNDTASKS
        stBackgroundTask == eBad ||
#endif
        stABI == eBad || stProcess == eBad) {
      continue;
    }

    if (directive->regfunc) {
      if (GeckoProcessType_Default != XRE_GetProcessType()) {
        continue;
      }

      if (!nsChromeRegistry::gChromeRegistry) {
        nsCOMPtr<nsIChromeRegistry> cr = mozilla::services::GetChromeRegistry();
        if (!nsChromeRegistry::gChromeRegistry) {
          LogMessageWithContext(aFile, line,
                                "Chrome registry isn't available yet.");
          continue;
        }
      }

      (nsChromeRegistry::gChromeRegistry->*(directive->regfunc))(chromecx, line,
                                                                 argv, flags);
    } else if (directive->ischrome || !aChromeOnly) {
      (nsComponentManagerImpl::gComponentManager->*(directive->mgrfunc))(
          mgrcx, line, argv);
    }
  }
}
