/* -*- 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 <climits>
#include <cmath>
#include "FuzzingTraits.h"
#include "jsapi.h"
#include "jsfriendapi.h"
#include "prenv.h"
#include "MessageManagerFuzzer.h"
#include "mozilla/ErrorResult.h"
#include "nsDebug.h"
#include "nsError.h"
#include "nsFrameMessageManager.h"
#include "nsJSUtils.h"
#include "nsXULAppAPI.h"
#include "nsNetCID.h"
#include "nsString.h"
#include "nsUnicharUtils.h"
#include "nsIFile.h"
#include "nsIFileStreams.h"
#include "nsILineInputStream.h"
#include "nsLocalFile.h"
#include "nsTArray.h"


#define MESSAGEMANAGER_FUZZER_DEFAULT_MUTATION_PROBABILITY 2
#define MSGMGR_FUZZER_LOG(fmt, args...)                         \
  if (MessageManagerFuzzer::IsLoggingEnabled()) {               \
    printf_stderr("[MessageManagerFuzzer] " fmt "\n", ## args); \
  }

namespace mozilla {
namespace dom {

using namespace fuzzing;
using namespace ipc;

/* static */
void
MessageManagerFuzzer::ReadFile(const char* path, nsTArray<nsCString> &aArray)
{
  nsCOMPtr<nsIFile> file;
  nsresult rv = NS_NewLocalFile(NS_ConvertUTF8toUTF16(path),
                                true,
                                getter_AddRefs(file));
  NS_ENSURE_SUCCESS_VOID(rv);

  bool exists = false;
  rv = file->Exists(&exists);
  if (NS_FAILED(rv) || !exists) {
    return;
  }

  nsCOMPtr<nsIFileInputStream> fileStream(
    do_CreateInstance(NS_LOCALFILEINPUTSTREAM_CONTRACTID, &rv));
  NS_ENSURE_SUCCESS_VOID(rv);

  rv = fileStream->Init(file, -1, -1, false);
  NS_ENSURE_SUCCESS_VOID(rv);

  nsCOMPtr<nsILineInputStream> lineStream(do_QueryInterface(fileStream, &rv));
  NS_ENSURE_SUCCESS_VOID(rv);

  nsAutoCString line;
  bool more = true;
  do {
    rv = lineStream->ReadLine(line, &more);
    NS_ENSURE_SUCCESS_VOID(rv);
    aArray.AppendElement(line);
  } while (more);
}

/* static */
bool
MessageManagerFuzzer::IsMessageNameBlacklisted(const nsAString& aMessageName) {
  static bool sFileLoaded = false;
  static nsTArray<nsCString> valuesInFile;

  if (!sFileLoaded) {
    ReadFile(PR_GetEnv("MESSAGEMANAGER_FUZZER_BLACKLIST"), valuesInFile);
    sFileLoaded = true;
  }

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

  return valuesInFile.Contains(NS_ConvertUTF16toUTF8(aMessageName).get());
}

/* static */
nsCString
MessageManagerFuzzer::GetFuzzValueFromFile()
{
  static bool sFileLoaded = false;
  static nsTArray<nsCString> valuesInFile;

  if (!sFileLoaded) {
    ReadFile(PR_GetEnv("MESSAGEMANAGER_FUZZER_STRINGSFILE"), valuesInFile);
    sFileLoaded = true;
  }

  // If something goes wrong with importing the file we return an empty string.
  if (valuesInFile.Length() == 0) {
    return nsCString();
  }

  unsigned randIdx = RandomIntegerRange<unsigned>(0, valuesInFile.Length());
  return valuesInFile.ElementAt(randIdx);
}


/* static */
void
MessageManagerFuzzer::MutateObject(JSContext* aCx,
                                   JS::HandleValue aValue,
                                   unsigned short int aRecursionCounter)
{
  JS::Rooted<JSObject*> object(aCx, &aValue.toObject());
  JS::Rooted<JS::IdVector> ids(aCx, JS::IdVector(aCx));

  if (!JS_Enumerate(aCx, object, &ids)) {
    return;
  }

  for (size_t i = 0, n = ids.length(); i < n; i++) {
    // Retrieve Property name.
    nsAutoJSString propName;
    if (!propName.init(aCx, ids[i])) {
      continue;
    }
    MSGMGR_FUZZER_LOG("%*s- Property: %s",
                      aRecursionCounter*4, "",
                      NS_ConvertUTF16toUTF8(propName).get());

    // The likelihood when a value gets fuzzed of this object.
    if (!FuzzingTraits::Sometimes(DefaultMutationProbability())) {
      continue;
    }

    // Retrieve Property value.
    JS::RootedValue propertyValue(aCx);
    JS_GetPropertyById(aCx, object, ids[i], &propertyValue);

    JS::RootedValue newPropValue(aCx);
    MutateValue(aCx, propertyValue, &newPropValue, aRecursionCounter);

    JS_SetPropertyById(aCx, object, ids[i], newPropValue);
  }
}

/* static */
bool
MessageManagerFuzzer::MutateValue(JSContext* aCx,
                                  JS::HandleValue aValue,
                                  JS::MutableHandleValue aOutMutationValue,
                                  unsigned short int aRecursionCounter)
{
  if (aValue.isInt32()) {
    if (FuzzingTraits::Sometimes(DefaultMutationProbability() * 2)) {
      aOutMutationValue.set(JS::Int32Value(RandomNumericLimit<int>()));
    } else {
      aOutMutationValue.set(JS::Int32Value(RandomInteger<int>()));
    }
    MSGMGR_FUZZER_LOG("%*s! Mutated value of type |int32|: '%d' to '%d'",
                      aRecursionCounter * 4, "",
                      aValue.toInt32(), aOutMutationValue.toInt32());
    return true;
  }

  if (aValue.isDouble()) {
    aOutMutationValue.set(JS::DoubleValue(RandomFloatingPoint<double>()));
    MSGMGR_FUZZER_LOG("%*s! Mutated value of type |double|: '%f' to '%f'",
                      aRecursionCounter * 4, "",
                      aValue.toDouble(), aOutMutationValue.toDouble());
    return true;
  }

  if (aValue.isBoolean()) {
    aOutMutationValue.set(JS::BooleanValue(bool(RandomIntegerRange(0, 2))));
    MSGMGR_FUZZER_LOG("%*s! Mutated value of type |boolean|: '%d' to '%d'",
                      aRecursionCounter * 4, "",
                      aValue.toBoolean(), aOutMutationValue.toBoolean());
    return true;
  }

  if (aValue.isString()) {
    nsCString x = GetFuzzValueFromFile();
    if (x.IsEmpty()) {
      return false;
    }
    JSString* str = JS_NewStringCopyZ(aCx, x.get());
    aOutMutationValue.set(JS::StringValue(str));
    MSGMGR_FUZZER_LOG("%*s! Mutated value of type |string|: '%s' to '%s'",
                      aRecursionCounter * 4, "",
                      JS_EncodeString(aCx, aValue.toString()), x.get());
    return true;
  }

  if (aValue.isObject()) {
    aRecursionCounter++;
    MSGMGR_FUZZER_LOG("%*s<Enumerating found object>",
                      aRecursionCounter * 4, "");
    MutateObject(aCx, aValue, aRecursionCounter);
    aOutMutationValue.set(aValue);
    return true;
  }

  return false;
}

/* static */
bool
MessageManagerFuzzer::Mutate(JSContext* aCx,
                             const nsAString& aMessageName,
                             ipc::StructuredCloneData* aData,
                             const JS::Value& aTransfer)
{
  MSGMGR_FUZZER_LOG("Message: %s in process: %d",
                   NS_ConvertUTF16toUTF8(aMessageName).get(),
                   XRE_GetProcessType());

  unsigned short int aRecursionCounter = 0;
  ErrorResult rv;
  JS::RootedValue t(aCx, aTransfer);

  /* Read original StructuredCloneData. */
  JS::RootedValue scdContent(aCx);
  aData->Read(aCx, &scdContent, rv);
  if (NS_WARN_IF(rv.Failed())) {
    rv.SuppressException();
    JS_ClearPendingException(aCx);
    return false;
  }

  JS::RootedValue scdMutationContent(aCx);
  bool isMutated = MutateValue(aCx,
                               scdContent,
                               &scdMutationContent,
                               aRecursionCounter);

  /* Write mutated StructuredCloneData. */
  ipc::StructuredCloneData mutatedStructuredCloneData;
  mutatedStructuredCloneData.Write(aCx, scdMutationContent, t, rv);
  if (NS_WARN_IF(rv.Failed())) {
    rv.SuppressException();
    JS_ClearPendingException(aCx);
    return false;
  }

  // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1346040
  aData->Copy(mutatedStructuredCloneData);

  /* Mutated and successfully written to StructuredCloneData object. */
  if (isMutated) {
    JS::RootedString str(aCx, JS_ValueToSource(aCx, scdMutationContent));
    MSGMGR_FUZZER_LOG("Mutated '%s' Message: %s",
                      NS_ConvertUTF16toUTF8(aMessageName).get(),
                      JS_EncodeStringToUTF8(aCx, str));
  }

  return true;
}

/* static */
unsigned int
MessageManagerFuzzer::DefaultMutationProbability()
{
  static unsigned long sPropValue = MESSAGEMANAGER_FUZZER_DEFAULT_MUTATION_PROBABILITY;
  static bool sInitialized = false;

  if (sInitialized) {
    return sPropValue;
  }
  sInitialized = true;

  // Defines the likelihood of fuzzing a message.
  const char* probability = PR_GetEnv("MESSAGEMANAGER_FUZZER_MUTATION_PROBABILITY");
  if (probability) {
    long n = std::strtol(probability, nullptr, 10);
    if (n != 0) {
      sPropValue = n;
      return sPropValue;
    }
  }

  return sPropValue;
}

/* static */
bool
MessageManagerFuzzer::IsLoggingEnabled()
{
  static bool sInitialized = false;
  static bool sIsLoggingEnabled = false;

  if (!sInitialized) {
    sIsLoggingEnabled = !!PR_GetEnv("MESSAGEMANAGER_FUZZER_ENABLE_LOGGING");
    sInitialized = true;
  }

  return sIsLoggingEnabled;
}

/* static */
bool
MessageManagerFuzzer::IsEnabled()
{
  return !!PR_GetEnv("MESSAGEMANAGER_FUZZER_ENABLE") && XRE_IsContentProcess();
}

/* static */
void
MessageManagerFuzzer::TryMutate(JSContext* aCx,
                                const nsAString& aMessageName,
                                ipc::StructuredCloneData* aData,
                                const JS::Value& aTransfer)
{
  if (!IsEnabled()) {
    return;
  }

  if (IsMessageNameBlacklisted(aMessageName)) {
    MSGMGR_FUZZER_LOG("Blacklisted message: %s",
                      NS_ConvertUTF16toUTF8(aMessageName).get());
    return;
  }

  Mutate(aCx, aMessageName, aData, aTransfer);
}

} // namespace dom
} // namespace mozilla
