Source code
Revision control
Copy as Markdown
Other Tools
/* -*- 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
#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)