/* -*- 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 "js/friend/DumpFunctions.h"

#include "mozilla/Maybe.h"  // mozilla::Maybe

#include <inttypes.h>  // PRIu64
#include <stddef.h>    // size_t
#include <stdio.h>     // fprintf, fflush

#include "jsfriendapi.h"  // js::WeakMapTracer
#include "jstypes.h"      // JS_PUBLIC_API

#include "gc/Cell.h"              // js::gc::Cell, js::gc::TenuredCell
#include "gc/GC.h"                // js::TraceRuntimeWithoutEviction
#include "gc/GCEnum.h"            // js::CanGC
#include "gc/Heap.h"              // js::gc::Arena
#include "gc/Tracer.h"            // js::TraceChildren
#include "gc/WeakMap.h"           // js::IterateHeapUnbarriered, js::WeakMapBase
#include "js/CallAndConstruct.h"  // JS::IsCallable
#include "js/ColumnNumber.h"      // JS::LimitedColumnNumberOneOrigin
#include "js/GCAPI.h"             // JS::GCReason
#include "js/GCVector.h"          // JS::RootedVector
#include "js/HeapAPI.h"           // JS::GCCellPtr, js::gc::IsInsideNursery
#include "js/Id.h"                // JS::PropertyKey
#include "js/Printer.h"     // js::GenericPrinter, js::QuoteString, js::Sprinter
#include "js/RootingAPI.h"  // JS::Handle, JS::Rooted
#include "js/TracingAPI.h"  // JS::CallbackTracer, JS_GetTraceThingInfo
#include "js/UbiNode.h"     // JS::ubi::Node
#include "js/Value.h"       // JS::Value
#include "js/Wrapper.h"     // js::UncheckedUnwrapWithoutExpose
#include "vm/BigIntType.h"  // JS::BigInt::dump
#include "vm/FrameIter.h"   // js::AllFramesIter, js::FrameIter
#include "vm/Interpreter.h"  // GetFunctionThis
#include "vm/JSContext.h"    // JSContext
#include "vm/JSFunction.h"   // JSFunction
#include "vm/JSObject.h"     // JSObject
#include "vm/JSScript.h"     // JSScript
#include "vm/Realm.h"        // JS::Realm
#include "vm/Runtime.h"      // JSRuntime
#include "vm/Scope.h"        // js::PositionalFormalParameterIter
#include "vm/Stack.h"        // js::DONT_CHECK_ALIASING
#include "vm/StringType.h"   // JSAtom, JSString, js::ToString

#include "vm/JSObject-inl.h"  // js::IsCallable
#include "vm/Realm-inl.h"     // js::AutoRealm

namespace JS {

class JS_PUBLIC_API AutoRequireNoGC;
class JS_PUBLIC_API Zone;

}  // namespace JS

using JS::Handle;
using JS::MagicValue;
using JS::PropertyKey;
using JS::Realm;
using JS::Rooted;
using JS::RootedVector;
using JS::UniqueChars;
using JS::Value;
using JS::Zone;

using js::AllFramesIter;
using js::AutoRealm;
using js::CanGC;
using js::DONT_CHECK_ALIASING;
using js::FrameIter;
using js::PositionalFormalParameterIter;
using js::Sprinter;
using js::ToString;
using js::UncheckedUnwrapWithoutExpose;
using js::WeakMapTracer;

// We don't want jsfriendapi.h to depend on GenericPrinter,
// so these functions are declared directly in the cpp.

namespace js {

class InterpreterFrame;

extern JS_PUBLIC_API void DumpString(JSString* str, GenericPrinter& out);

extern JS_PUBLIC_API void DumpAtom(JSAtom* atom, GenericPrinter& out);

extern JS_PUBLIC_API void DumpObject(JSObject* obj, GenericPrinter& out);

extern JS_PUBLIC_API void DumpChars(const char16_t* s, size_t n,
                                    GenericPrinter& out);

extern JS_PUBLIC_API void DumpValue(const JS::Value& val, GenericPrinter& out);

extern JS_PUBLIC_API void DumpId(PropertyKey id, GenericPrinter& out);

extern JS_PUBLIC_API void DumpInterpreterFrame(
    JSContext* cx, GenericPrinter& out, InterpreterFrame* start = nullptr);

extern JS_PUBLIC_API void DumpBigInt(JS::BigInt* bi, GenericPrinter& out);

}  // namespace js

void js::DumpString(JSString* str, GenericPrinter& out) {
#if defined(DEBUG) || defined(JS_JITSPEW)
  str->dump(out);
#endif
}

void js::DumpAtom(JSAtom* atom, GenericPrinter& out) {
#if defined(DEBUG) || defined(JS_JITSPEW)
  atom->dump(out);
#endif
}

void js::DumpChars(const char16_t* s, size_t n, GenericPrinter& out) {
#if defined(DEBUG) || defined(JS_JITSPEW)
  if (n == SIZE_MAX) {
    n = 0;
    while (s[n]) {
      n++;
    }
  }

  out.printf("char16_t * (%p) = \"", (void*)s);
  JSString::dumpCharsNoQuote(s, n, out);
  out.put("\"\n");
#endif
}

void js::DumpObject(JSObject* obj, GenericPrinter& out) {
#if defined(DEBUG) || defined(JS_JITSPEW)
  if (!obj) {
    out.printf("NULL\n");
    return;
  }
  obj->dump(out);
#endif
}

void js::DumpBigInt(JS::BigInt* bi, GenericPrinter& out) {
#if defined(DEBUG) || defined(JS_JITSPEW)
  bi->dump(out);
#endif
}

void js::DumpString(JSString* str, FILE* fp) {
#if defined(DEBUG) || defined(JS_JITSPEW)
  Fprinter out(fp);
  js::DumpString(str, out);
#endif
}

void js::DumpAtom(JSAtom* atom, FILE* fp) {
#if defined(DEBUG) || defined(JS_JITSPEW)
  Fprinter out(fp);
  js::DumpAtom(atom, out);
#endif
}

void js::DumpChars(const char16_t* s, size_t n, FILE* fp) {
#if defined(DEBUG) || defined(JS_JITSPEW)
  Fprinter out(fp);
  js::DumpChars(s, n, out);
#endif
}

void js::DumpObject(JSObject* obj, FILE* fp) {
#if defined(DEBUG) || defined(JS_JITSPEW)
  Fprinter out(fp);
  js::DumpObject(obj, out);
#endif
}

void js::DumpBigInt(JS::BigInt* bi, FILE* fp) {
#if defined(DEBUG) || defined(JS_JITSPEW)
  Fprinter out(fp);
  js::DumpBigInt(bi, out);
#endif
}

void js::DumpId(PropertyKey id, FILE* fp) {
#if defined(DEBUG) || defined(JS_JITSPEW)
  Fprinter out(fp);
  js::DumpId(id, out);
#endif
}

void js::DumpValue(const JS::Value& val, FILE* fp) {
#if defined(DEBUG) || defined(JS_JITSPEW)
  Fprinter out(fp);
  js::DumpValue(val, out);
#endif
}

void js::DumpString(JSString* str) { DumpString(str, stderr); }
void js::DumpAtom(JSAtom* atom) { DumpAtom(atom, stderr); }
void js::DumpObject(JSObject* obj) { DumpObject(obj, stderr); }
void js::DumpChars(const char16_t* s, size_t n) { DumpChars(s, n, stderr); }
void js::DumpBigInt(JS::BigInt* bi) { DumpBigInt(bi, stderr); }
void js::DumpValue(const JS::Value& val) { DumpValue(val, stderr); }
void js::DumpId(PropertyKey id) { DumpId(id, stderr); }
void js::DumpInterpreterFrame(JSContext* cx, InterpreterFrame* start) {
#if defined(DEBUG) || defined(JS_JITSPEW)
  Fprinter out(stderr);
  DumpInterpreterFrame(cx, out, start);
#endif
}

bool js::DumpPC(JSContext* cx) {
#if defined(DEBUG) || defined(JS_JITSPEW)
  return DumpPC(cx, stdout);
#else
  return true;
#endif
}

bool js::DumpScript(JSContext* cx, JSScript* scriptArg) {
#if defined(DEBUG) || defined(JS_JITSPEW)
  return DumpScript(cx, scriptArg, stdout);
#else
  return true;
#endif
}

static const char* FormatValue(JSContext* cx, Handle<Value> v,
                               UniqueChars& bytes) {
  if (v.isMagic()) {
    MOZ_ASSERT(v.whyMagic() == JS_OPTIMIZED_OUT ||
               v.whyMagic() == JS_UNINITIALIZED_LEXICAL);
    return "[unavailable]";
  }

  if (js::IsCallable(v)) {
    return "[function]";
  }

  if (v.isObject() && js::IsCrossCompartmentWrapper(&v.toObject())) {
    return "[cross-compartment wrapper]";
  }

  JSString* str;
  {
    mozilla::Maybe<AutoRealm> ar;
    if (v.isObject()) {
      ar.emplace(cx, &v.toObject());
    }

    str = ToString<CanGC>(cx, v);
    if (!str) {
      return nullptr;
    }
  }

  bytes = js::QuoteString(cx, str, '"');
  return bytes.get();
}

static bool FormatFrame(JSContext* cx, const FrameIter& iter, Sprinter& sp,
                        int num, bool showArgs, bool showLocals,
                        bool showThisProps) {
  MOZ_ASSERT(!cx->isExceptionPending());
  Rooted<JSScript*> script(cx, iter.script());
  jsbytecode* pc = iter.pc();

  Rooted<JSObject*> envChain(cx, iter.environmentChain(cx));
  JSAutoRealm ar(cx, envChain);

  const char* filename = script->filename();
  JS::LimitedColumnNumberOneOrigin column;
  unsigned lineno = PCToLineNumber(script, pc, &column);
  Rooted<JSFunction*> fun(cx, iter.maybeCallee(cx));
  Rooted<JSString*> funname(cx);
  if (fun) {
    funname = fun->fullDisplayAtom();
  }

  Rooted<Value> thisVal(cx);
  if (iter.hasUsableAbstractFramePtr() && iter.isFunctionFrame() && fun &&
      !fun->isArrow() && !fun->isDerivedClassConstructor()) {
    if (!GetFunctionThis(cx, iter.abstractFramePtr(), &thisVal)) {
      return false;
    }
  }

  // print the frame number and function name
  if (funname) {
    UniqueChars funbytes = js::QuoteString(cx, funname);
    if (!funbytes) {
      return false;
    }
    sp.printf("%d %s(", num, funbytes.get());
  } else if (fun) {
    sp.printf("%d anonymous(", num);
  } else {
    sp.printf("%d <TOP LEVEL>", num);
  }

  if (showArgs && iter.hasArgs()) {
    PositionalFormalParameterIter fi(script);
    bool first = true;
    for (unsigned i = 0; i < iter.numActualArgs(); i++) {
      Rooted<Value> arg(cx);
      if (i < iter.numFormalArgs() && fi.closedOver()) {
        if (iter.hasInitialEnvironment(cx)) {
          arg = iter.callObj(cx).aliasedBinding(fi);
        } else {
          arg = MagicValue(JS_OPTIMIZED_OUT);
        }
      } else if (iter.hasUsableAbstractFramePtr()) {
        if (script->argsObjAliasesFormals() && iter.hasArgsObj()) {
          arg = iter.argsObj().arg(i);
        } else {
          arg = iter.unaliasedActual(i, DONT_CHECK_ALIASING);
        }
      } else {
        arg = MagicValue(JS_OPTIMIZED_OUT);
      }

      UniqueChars valueBytes;
      const char* value = FormatValue(cx, arg, valueBytes);
      if (!value) {
        if (cx->isThrowingOutOfMemory()) {
          return false;
        }
        cx->clearPendingException();
      }

      UniqueChars nameBytes;
      const char* name = nullptr;

      if (i < iter.numFormalArgs()) {
        MOZ_ASSERT(fi.argumentSlot() == i);
        if (!fi.isDestructured()) {
          nameBytes = StringToNewUTF8CharsZ(cx, *fi.name());
          name = nameBytes.get();
          if (!name) {
            return false;
          }
        } else {
          name = "(destructured parameter)";
        }
        fi++;
      }

      if (value) {
        sp.printf("%s%s%s%s%s%s", !first ? ", " : "", name ? name : "",
                  name ? " = " : "", arg.isString() ? "\"" : "", value,
                  arg.isString() ? "\"" : "");

        first = false;
      } else {
        sp.put(
            "    <Failed to get argument while inspecting stack "
            "frame>\n");
      }
    }
  }

  // print filename, line number and column
  sp.printf("%s [\"%s\":%u:%u]\n", fun ? ")" : "",
            filename ? filename : "<unknown>", lineno, column.oneOriginValue());

  // Note: Right now we don't dump the local variables anymore, because
  // that is hard to support across all the JITs etc.

  // print the value of 'this'
  if (showLocals) {
    if (!thisVal.isUndefined()) {
      Rooted<JSString*> thisValStr(cx, ToString<CanGC>(cx, thisVal));
      if (!thisValStr) {
        if (cx->isThrowingOutOfMemory()) {
          return false;
        }
        cx->clearPendingException();
      }
      if (thisValStr) {
        UniqueChars thisValBytes = QuoteString(cx, thisValStr);
        if (!thisValBytes) {
          return false;
        }
        sp.printf("    this = %s\n", thisValBytes.get());
      } else {
        sp.put("    <failed to get 'this' value>\n");
      }
    }
  }

  if (showThisProps && thisVal.isObject()) {
    Rooted<JSObject*> obj(cx, &thisVal.toObject());

    RootedVector<PropertyKey> keys(cx);
    if (!GetPropertyKeys(cx, obj, JSITER_OWNONLY, &keys)) {
      if (cx->isThrowingOutOfMemory()) {
        return false;
      }
      cx->clearPendingException();
    }

    for (size_t i = 0; i < keys.length(); i++) {
      Rooted<jsid> id(cx, keys[i]);
      Rooted<Value> key(cx, IdToValue(id));
      Rooted<Value> v(cx);

      if (!GetProperty(cx, obj, obj, id, &v)) {
        if (cx->isThrowingOutOfMemory()) {
          return false;
        }
        cx->clearPendingException();
        sp.put(
            "    <Failed to fetch property while inspecting stack "
            "frame>\n");
        continue;
      }

      UniqueChars nameBytes;
      const char* name = FormatValue(cx, key, nameBytes);
      if (!name) {
        if (cx->isThrowingOutOfMemory()) {
          return false;
        }
        cx->clearPendingException();
      }

      UniqueChars valueBytes;
      const char* value = FormatValue(cx, v, valueBytes);
      if (!value) {
        if (cx->isThrowingOutOfMemory()) {
          return false;
        }
        cx->clearPendingException();
      }

      if (name && value) {
        sp.printf("    this.%s = %s%s%s\n", name, v.isString() ? "\"" : "",
                  value, v.isString() ? "\"" : "");
      } else {
        sp.put(
            "    <Failed to format values while inspecting stack "
            "frame>\n");
      }
    }
  }

  MOZ_ASSERT(!cx->isExceptionPending());
  return true;
}

static bool FormatWasmFrame(JSContext* cx, const FrameIter& iter, Sprinter& sp,
                            int num) {
  UniqueChars nameStr;
  if (JSAtom* functionDisplayAtom = iter.maybeFunctionDisplayAtom()) {
    nameStr = StringToNewUTF8CharsZ(cx, *functionDisplayAtom);
    if (!nameStr) {
      return false;
    }
  }

  sp.printf("%d %s()", num, nameStr ? nameStr.get() : "<wasm-function>");
  sp.printf(" [\"%s\":wasm-function[%u]:0x%x]\n",
            iter.filename() ? iter.filename() : "<unknown>",
            iter.wasmFuncIndex(), iter.wasmBytecodeOffset());

  MOZ_ASSERT(!cx->isExceptionPending());
  return true;
}

JS::UniqueChars JS::FormatStackDump(JSContext* cx, bool showArgs,
                                    bool showLocals, bool showThisProps) {
  int num = 0;

  Sprinter sp(cx);
  if (!sp.init()) {
    return nullptr;
  }

  for (AllFramesIter i(cx); !i.done(); ++i) {
    bool ok = i.hasScript() ? FormatFrame(cx, i, sp, num, showArgs, showLocals,
                                          showThisProps)
                            : FormatWasmFrame(cx, i, sp, num);
    if (!ok) {
      return nullptr;
    }
    num++;
  }

  if (num == 0) {
    sp.put("JavaScript stack is empty\n");
  }

  return sp.release();
}

struct DumpHeapTracer final : public JS::CallbackTracer, public WeakMapTracer {
  const char* prefix;
  FILE* output;
  mozilla::MallocSizeOf mallocSizeOf;

  DumpHeapTracer(FILE* fp, JSContext* cx, mozilla::MallocSizeOf mallocSizeOf)
      : JS::CallbackTracer(cx, JS::TracerKind::Callback,
                           JS::WeakMapTraceAction::Skip),
        WeakMapTracer(cx->runtime()),
        prefix(""),
        output(fp),
        mallocSizeOf(mallocSizeOf) {}

 private:
  void trace(JSObject* map, JS::GCCellPtr key, JS::GCCellPtr value) override {
    JSObject* kdelegate = nullptr;
    if (key.is<JSObject>()) {
      kdelegate = UncheckedUnwrapWithoutExpose(&key.as<JSObject>());
    }

    fprintf(output, "WeakMapEntry map=%p key=%p keyDelegate=%p value=%p\n", map,
            key.asCell(), kdelegate, value.asCell());
  }

  void onChild(JS::GCCellPtr thing, const char* name) override;
};

static char MarkDescriptor(js::gc::Cell* thing) {
  js::gc::TenuredCell* cell = &thing->asTenured();
  if (cell->isMarkedBlack()) {
    return 'B';
  }
  if (cell->isMarkedGray()) {
    return 'G';
  }
  if (cell->isMarkedAny()) {
    return 'X';
  }
  return 'W';
}

static void DumpHeapVisitZone(JSRuntime* rt, void* data, Zone* zone,
                              const JS::AutoRequireNoGC& nogc) {
  DumpHeapTracer* dtrc = static_cast<DumpHeapTracer*>(data);
  fprintf(dtrc->output, "# zone %p\n", static_cast<void*>(zone));
}

static void DumpHeapVisitRealm(JSContext* cx, void* data, Realm* realm,
                               const JS::AutoRequireNoGC& nogc) {
  char name[1024];
  if (auto nameCallback = cx->runtime()->realmNameCallback) {
    nameCallback(cx, realm, name, sizeof(name), nogc);
  } else {
    strcpy(name, "<unknown>");
  }

  DumpHeapTracer* dtrc = static_cast<DumpHeapTracer*>(data);
  fprintf(dtrc->output, "# realm %s [in compartment %p, zone %p]\n", name,
          static_cast<void*>(realm->compartment()),
          static_cast<void*>(realm->zone()));
}

static void DumpHeapVisitArena(JSRuntime* rt, void* data, js::gc::Arena* arena,
                               JS::TraceKind traceKind, size_t thingSize,
                               const JS::AutoRequireNoGC& nogc) {
  DumpHeapTracer* dtrc = static_cast<DumpHeapTracer*>(data);
  fprintf(dtrc->output, "# arena allockind=%u size=%u\n",
          unsigned(arena->getAllocKind()), unsigned(thingSize));
}

static void DumpHeapVisitCell(JSRuntime* rt, void* data, JS::GCCellPtr cellptr,
                              size_t thingSize,
                              const JS::AutoRequireNoGC& nogc) {
  DumpHeapTracer* dtrc = static_cast<DumpHeapTracer*>(data);
  char cellDesc[1024 * 32];
  js::gc::GetTraceThingInfo(cellDesc, sizeof(cellDesc), cellptr.asCell(),
                            cellptr.kind(), true);

  fprintf(dtrc->output, "%p %c %s", cellptr.asCell(),
          MarkDescriptor(cellptr.asCell()), cellDesc);
  if (dtrc->mallocSizeOf) {
    auto size = JS::ubi::Node(cellptr).size(dtrc->mallocSizeOf);
    fprintf(dtrc->output, " SIZE:: %" PRIu64 "\n", size);
  } else {
    fprintf(dtrc->output, "\n");
  }

  JS::TraceChildren(dtrc, cellptr);
}

void DumpHeapTracer::onChild(JS::GCCellPtr thing, const char* name) {
  if (js::gc::IsInsideNursery(thing.asCell())) {
    return;
  }

  char buffer[1024];
  context().getEdgeName(name, buffer, sizeof(buffer));
  fprintf(output, "%s%p %c %s\n", prefix, thing.asCell(),
          MarkDescriptor(thing.asCell()), buffer);
}

void js::DumpHeap(JSContext* cx, FILE* fp,
                  DumpHeapNurseryBehaviour nurseryBehaviour,
                  mozilla::MallocSizeOf mallocSizeOf) {
  if (nurseryBehaviour == CollectNurseryBeforeDump) {
    cx->runtime()->gc.evictNursery(JS::GCReason::API);
  }

  DumpHeapTracer dtrc(fp, cx, mallocSizeOf);

  fprintf(dtrc.output, "# Roots.\n");
  TraceRuntimeWithoutEviction(&dtrc);

  fprintf(dtrc.output, "# Weak maps.\n");
  WeakMapBase::traceAllMappings(&dtrc);

  fprintf(dtrc.output, "==========\n");

  dtrc.prefix = "> ";
  IterateHeapUnbarriered(cx, &dtrc, DumpHeapVisitZone, DumpHeapVisitRealm,
                         DumpHeapVisitArena, DumpHeapVisitCell);

  fflush(dtrc.output);
}

void DumpFmtV(FILE* fp, const char* fmt, va_list args) {
  js::Fprinter out(fp);
  out.vprintf(fmt, args);
}

void js::DumpFmt(FILE* fp, const char* fmt, ...) {
  va_list args;
  va_start(args, fmt);
  DumpFmtV(fp, fmt, args);
  va_end(args);
}

void js::DumpFmt(const char* fmt, ...) {
  va_list args;
  va_start(args, fmt);
  DumpFmtV(stderr, fmt, args);
  va_end(args);
}
