/* -*- 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/Utf8.h"  // mozilla::Utf8Unit

#include "builtin/TestingFunctions.h"
#include "js/ColumnNumber.h"  // JS::LimitedColumnNumberOneOrigin, JS::TaggedColumnNumberOneOrigin
#include "js/CompilationAndEvaluation.h"  // JS::Evaluate
#include "js/Exception.h"
#include "js/SavedFrameAPI.h"
#include "js/SourceText.h"  // JS::Source{Ownership,Text}
#include "js/Stack.h"
#include "jsapi-tests/tests.h"
#include "util/Text.h"
#include "vm/ArrayObject.h"
#include "vm/Realm.h"
#include "vm/SavedStacks.h"

BEGIN_TEST(testSavedStacks_withNoStack) {
  JS::Realm* realm = cx->realm();
  realm->setAllocationMetadataBuilder(&js::SavedStacks::metadataBuilder);
  JS::RootedObject obj(cx, js::NewDenseEmptyArray(cx));
  realm->setAllocationMetadataBuilder(nullptr);
  return true;
}
END_TEST(testSavedStacks_withNoStack)

BEGIN_TEST(testSavedStacks_ApiDefaultValues) {
  JS::Rooted<js::SavedFrame*> savedFrame(cx, nullptr);

  JSPrincipals* principals = cx->realm()->principals();

  // Source
  JS::RootedString str(cx);
  JS::SavedFrameResult result =
      JS::GetSavedFrameSource(cx, principals, savedFrame, &str);
  CHECK(result == JS::SavedFrameResult::AccessDenied);
  CHECK(str.get() == cx->runtime()->emptyString);

  // Line
  uint32_t line = 123;
  result = JS::GetSavedFrameLine(cx, principals, savedFrame, &line);
  CHECK(result == JS::SavedFrameResult::AccessDenied);
  CHECK(line == 0);

  // Column
  JS::TaggedColumnNumberOneOrigin column(JS::LimitedColumnNumberOneOrigin(123));
  result = JS::GetSavedFrameColumn(cx, principals, savedFrame, &column);
  CHECK(result == JS::SavedFrameResult::AccessDenied);
  CHECK(column == JS::TaggedColumnNumberOneOrigin());

  // Function display name
  result =
      JS::GetSavedFrameFunctionDisplayName(cx, principals, savedFrame, &str);
  CHECK(result == JS::SavedFrameResult::AccessDenied);
  CHECK(str.get() == nullptr);

  // Parent
  JS::RootedObject parent(cx);
  result = JS::GetSavedFrameParent(cx, principals, savedFrame, &parent);
  CHECK(result == JS::SavedFrameResult::AccessDenied);
  CHECK(parent.get() == nullptr);

  // Stack string
  CHECK(JS::BuildStackString(cx, principals, savedFrame, &str));
  CHECK(str.get() == cx->runtime()->emptyString);

  return true;
}
END_TEST(testSavedStacks_ApiDefaultValues)

BEGIN_TEST(testSavedStacks_RangeBasedForLoops) {
  CHECK(js::DefineTestingFunctions(cx, global, false, false));

  JS::RootedValue val(cx);
  CHECK(
      evaluate("(function one() {                      \n"   // 1
               "  return (function two() {             \n"   // 2
               "    return (function three() {         \n"   // 3
               "      return saveStack();              \n"   // 4
               "    }());                              \n"   // 5
               "  }());                                \n"   // 6
               "}());                                  \n",  // 7
               "filename.js", 1, &val));

  CHECK(val.isObject());
  JS::RootedObject obj(cx, &val.toObject());

  CHECK(obj->is<js::SavedFrame>());
  JS::Rooted<js::SavedFrame*> savedFrame(cx, &obj->as<js::SavedFrame>());

  JS::Rooted<js::SavedFrame*> rf(cx, savedFrame);
  for (JS::Handle<js::SavedFrame*> frame :
       js::SavedFrame::RootedRange(cx, rf)) {
    JS_GC(cx);
    CHECK(frame == rf);
    rf = rf->getParent();
  }
  CHECK(rf == nullptr);

  // Stack string
  static const char SpiderMonkeyStack[] =
      "three@filename.js:4:14\n"
      "two@filename.js:5:6\n"
      "one@filename.js:6:4\n"
      "@filename.js:7:2\n";
  static const char V8Stack[] =
      "    at three (filename.js:4:14)\n"
      "    at two (filename.js:5:6)\n"
      "    at one (filename.js:6:4)\n"
      "    at filename.js:7:2";
  struct {
    js::StackFormat format;
    const char* expected;
  } expectations[] = {{js::StackFormat::Default, SpiderMonkeyStack},
                      {js::StackFormat::SpiderMonkey, SpiderMonkeyStack},
                      {js::StackFormat::V8, V8Stack}};
  auto CheckStacks = [&]() {
    for (auto& expectation : expectations) {
      JS::RootedString str(cx);
      JSPrincipals* principals = cx->realm()->principals();
      CHECK(JS::BuildStackString(cx, principals, savedFrame, &str, 0,
                                 expectation.format));
      JSLinearString* lin = str->ensureLinear(cx);
      CHECK(lin);
      CHECK(js::StringEqualsAscii(lin, expectation.expected));
    }
    return true;
  };

  CHECK(CheckStacks());

  js::SetStackFormat(cx, js::StackFormat::V8);
  expectations[0].expected = V8Stack;

  CHECK(CheckStacks());

  return true;
}
END_TEST(testSavedStacks_RangeBasedForLoops)

BEGIN_TEST(testSavedStacks_ErrorStackSpiderMonkey) {
  JS::RootedValue val(cx);
  CHECK(
      evaluate("(function one() {                      \n"   // 1
               "  return (function two() {             \n"   // 2
               "    return (function three() {         \n"   // 3
               "      return new Error('foo');         \n"   // 4
               "    }());                              \n"   // 5
               "  }());                                \n"   // 6
               "}()).stack                             \n",  // 7
               "filename.js", 1, &val));

  CHECK(val.isString());
  JS::RootedString stack(cx, val.toString());

  // Stack string
  static const char SpiderMonkeyStack[] =
      "three@filename.js:4:14\n"
      "two@filename.js:5:6\n"
      "one@filename.js:6:4\n"
      "@filename.js:7:2\n";
  JSLinearString* lin = stack->ensureLinear(cx);
  CHECK(lin);
  CHECK(js::StringEqualsLiteral(lin, SpiderMonkeyStack));

  return true;
}
END_TEST(testSavedStacks_ErrorStackSpiderMonkey)

BEGIN_TEST(testSavedStacks_ErrorStackV8) {
  js::SetStackFormat(cx, js::StackFormat::V8);

  JS::RootedValue val(cx);
  CHECK(
      evaluate("(function one() {                      \n"   // 1
               "  return (function two() {             \n"   // 2
               "    return (function three() {         \n"   // 3
               "      return new Error('foo');         \n"   // 4
               "    }());                              \n"   // 5
               "  }());                                \n"   // 6
               "}()).stack                             \n",  // 7
               "filename.js", 1, &val));

  CHECK(val.isString());
  JS::RootedString stack(cx, val.toString());

  // Stack string
  static const char V8Stack[] =
      "Error: foo\n"
      "    at three (filename.js:4:14)\n"
      "    at two (filename.js:5:6)\n"
      "    at one (filename.js:6:4)\n"
      "    at filename.js:7:2";
  JSLinearString* lin = stack->ensureLinear(cx);
  CHECK(lin);
  CHECK(js::StringEqualsLiteral(lin, V8Stack));

  return true;
}
END_TEST(testSavedStacks_ErrorStackV8)

BEGIN_TEST(testSavedStacks_selfHostedFrames) {
  CHECK(js::DefineTestingFunctions(cx, global, false, false));

  JS::RootedValue val(cx);
  //             0         1         2         3
  //             0123456789012345678901234567890123456789
  CHECK(
      evaluate("(function one() {                      \n"   // 1
               "  try {                                \n"   // 2
               "    [1].map(function two() {           \n"   // 3
               "      throw saveStack();               \n"   // 4
               "    });                                \n"   // 5
               "  } catch (stack) {                    \n"   // 6
               "    return stack;                      \n"   // 7
               "  }                                    \n"   // 8
               "}())                                   \n",  // 9
               "filename.js", 1, &val));

  CHECK(val.isObject());
  JS::RootedObject obj(cx, &val.toObject());

  CHECK(obj->is<js::SavedFrame>());
  JS::Rooted<js::SavedFrame*> savedFrame(cx, &obj->as<js::SavedFrame>());

  JS::Rooted<js::SavedFrame*> selfHostedFrame(cx, savedFrame->getParent());
  CHECK(selfHostedFrame->isSelfHosted(cx));

  JSPrincipals* principals = cx->realm()->principals();

  // Source
  JS::RootedString str(cx);
  JS::SavedFrameResult result = JS::GetSavedFrameSource(
      cx, principals, selfHostedFrame, &str, JS::SavedFrameSelfHosted::Exclude);
  CHECK(result == JS::SavedFrameResult::Ok);
  JSLinearString* lin = str->ensureLinear(cx);
  CHECK(lin);
  CHECK(js::StringEqualsLiteral(lin, "filename.js"));

  // Source, including self-hosted frames
  result = JS::GetSavedFrameSource(cx, principals, selfHostedFrame, &str,
                                   JS::SavedFrameSelfHosted::Include);
  CHECK(result == JS::SavedFrameResult::Ok);
  lin = str->ensureLinear(cx);
  CHECK(lin);
  CHECK(js::StringEqualsLiteral(lin, "self-hosted"));

  // Line
  uint32_t line = 123;
  result = JS::GetSavedFrameLine(cx, principals, selfHostedFrame, &line,
                                 JS::SavedFrameSelfHosted::Exclude);
  CHECK(result == JS::SavedFrameResult::Ok);
  CHECK_EQUAL(line, 3U);

  // Column
  JS::TaggedColumnNumberOneOrigin column(JS::LimitedColumnNumberOneOrigin(123));
  result = JS::GetSavedFrameColumn(cx, principals, selfHostedFrame, &column,
                                   JS::SavedFrameSelfHosted::Exclude);
  CHECK(result == JS::SavedFrameResult::Ok);
  CHECK_EQUAL(column.oneOriginValue(), 9U);

  // Function display name
  result = JS::GetSavedFrameFunctionDisplayName(
      cx, principals, selfHostedFrame, &str, JS::SavedFrameSelfHosted::Exclude);
  CHECK(result == JS::SavedFrameResult::Ok);
  lin = str->ensureLinear(cx);
  CHECK(lin);
  CHECK(js::StringEqualsLiteral(lin, "one"));

  // Parent
  JS::RootedObject parent(cx);
  result = JS::GetSavedFrameParent(cx, principals, savedFrame, &parent,
                                   JS::SavedFrameSelfHosted::Exclude);
  CHECK(result == JS::SavedFrameResult::Ok);
  // JS::GetSavedFrameParent does this super funky and potentially unexpected
  // thing where it doesn't return the next subsumed parent but any next
  // parent. This so that callers can still get the "asyncParent" property
  // which is only on the first frame of the async parent stack and that frame
  // might not be subsumed by the caller. It is expected that callers will
  // still interact with the frame through the JSAPI accessors, so this should
  // be safe and should not leak privileged info to unprivileged
  // callers. However, because of that, we don't test that the parent we get
  // here is the selfHostedFrame's parent (because, as just explained, it
  // isn't) and instead check that asking for the source property gives us the
  // expected value.
  result = JS::GetSavedFrameSource(cx, principals, parent, &str,
                                   JS::SavedFrameSelfHosted::Exclude);
  CHECK(result == JS::SavedFrameResult::Ok);
  lin = str->ensureLinear(cx);
  CHECK(lin);
  CHECK(js::StringEqualsLiteral(lin, "filename.js"));

  return true;
}
END_TEST(testSavedStacks_selfHostedFrames)

BEGIN_TEST(test_GetPendingExceptionStack) {
  CHECK(js::DefineTestingFunctions(cx, global, false, false));

  JSPrincipals* principals = cx->realm()->principals();

  static const char sourceText[] =
      //          1         2         3
      // 123456789012345678901234567890123456789
      "(function one() {                      \n"   // 1
      "  (function two() {                    \n"   // 2
      "    (function three() {                \n"   // 3
      "      throw 5;                         \n"   // 4
      "    }());                              \n"   // 5
      "  }());                                \n"   // 6
      "}())                                   \n";  // 7

  JS::CompileOptions opts(cx);
  opts.setFileAndLine("filename.js", 1U);

  JS::SourceText<mozilla::Utf8Unit> srcBuf;
  CHECK(srcBuf.init(cx, sourceText, js_strlen(sourceText),
                    JS::SourceOwnership::Borrowed));

  JS::RootedValue val(cx);
  bool ok = JS::Evaluate(cx, opts, srcBuf, &val);

  CHECK(!ok);
  CHECK(JS_IsExceptionPending(cx));
  CHECK(val.isUndefined());

  JS::ExceptionStack exnStack(cx);
  CHECK(JS::GetPendingExceptionStack(cx, &exnStack));
  CHECK(exnStack.stack());
  CHECK(exnStack.stack()->is<js::SavedFrame>());
  JS::Rooted<js::SavedFrame*> savedFrameStack(
      cx, &exnStack.stack()->as<js::SavedFrame>());

  CHECK(exnStack.exception().isInt32());
  CHECK(exnStack.exception().toInt32() == 5);

  struct {
    uint32_t line;
    uint32_t column;
    const char* source;
    const char* functionDisplayName;
  } expected[] = {{4, 7, "filename.js", "three"},
                  {5, 6, "filename.js", "two"},
                  {6, 4, "filename.js", "one"},
                  {7, 2, "filename.js", nullptr}};

  size_t i = 0;
  for (JS::Handle<js::SavedFrame*> frame :
       js::SavedFrame::RootedRange(cx, savedFrameStack)) {
    CHECK(i < 4);

    // Line
    uint32_t line = 123;
    JS::SavedFrameResult result = JS::GetSavedFrameLine(
        cx, principals, frame, &line, JS::SavedFrameSelfHosted::Exclude);
    CHECK(result == JS::SavedFrameResult::Ok);
    CHECK_EQUAL(line, expected[i].line);

    // Column
    JS::TaggedColumnNumberOneOrigin column(
        JS::LimitedColumnNumberOneOrigin(123));
    result = JS::GetSavedFrameColumn(cx, principals, frame, &column,
                                     JS::SavedFrameSelfHosted::Exclude);
    CHECK(result == JS::SavedFrameResult::Ok);
    CHECK_EQUAL(column.oneOriginValue(), expected[i].column);

    // Source
    JS::RootedString str(cx);
    result = JS::GetSavedFrameSource(cx, principals, frame, &str,
                                     JS::SavedFrameSelfHosted::Exclude);
    CHECK(result == JS::SavedFrameResult::Ok);
    JSLinearString* linear = str->ensureLinear(cx);
    CHECK(linear);
    CHECK(js::StringEqualsAscii(linear, expected[i].source));

    // Function display name
    result = JS::GetSavedFrameFunctionDisplayName(
        cx, principals, frame, &str, JS::SavedFrameSelfHosted::Exclude);
    CHECK(result == JS::SavedFrameResult::Ok);
    if (auto expectedName = expected[i].functionDisplayName) {
      CHECK(str);
      linear = str->ensureLinear(cx);
      CHECK(linear);
      CHECK(js::StringEqualsAscii(linear, expectedName));
    } else {
      CHECK(!str);
    }

    i++;
  }

  return true;
}
END_TEST(test_GetPendingExceptionStack)