diff options
Diffstat (limited to '')
-rw-r--r-- | js/src/debugger/Source.cpp | 689 |
1 files changed, 689 insertions, 0 deletions
diff --git a/js/src/debugger/Source.cpp b/js/src/debugger/Source.cpp new file mode 100644 index 0000000000..3d0f563cc7 --- /dev/null +++ b/js/src/debugger/Source.cpp @@ -0,0 +1,689 @@ +/* -*- 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 "debugger/Source.h" + +#include "mozilla/Assertions.h" // for AssertionConditionType, MOZ_ASSERT +#include "mozilla/Maybe.h" // for Some, Maybe, Nothing +#include "mozilla/Variant.h" // for AsVariant, Variant + +#include <stdint.h> // for uint32_t +#include <string.h> // for memcpy +#include <utility> // for move + +#include "jsapi.h" // for JS_ReportErrorNumberASCII, JS_CopyStringCharsZ + +#include "debugger/Debugger.h" // for DebuggerSourceReferent, Debugger +#include "debugger/Script.h" // for DebuggerScript +#include "gc/Tracer.h" // for TraceManuallyBarrieredCrossCompartmentEdge +#include "js/CompilationAndEvaluation.h" // for Compile +#include "js/experimental/TypedData.h" // for JS_NewUint8Array +#include "js/friend/ErrorMessages.h" // for GetErrorMessage, JSMSG_* +#include "js/SourceText.h" // for JS::SourceOwnership +#include "vm/BytecodeUtil.h" // for JSDVG_SEARCH_STACK +#include "vm/JSContext.h" // for JSContext (ptr only) +#include "vm/JSObject.h" // for JSObject, RequireObject +#include "vm/JSScript.h" // for ScriptSource, ScriptSourceObject +#include "vm/ObjectGroup.h" // for TenuredObject +#include "vm/StringType.h" // for NewStringCopyZ, JSString (ptr only) +#include "vm/TypedArrayObject.h" // for TypedArrayObject, JSObject::is +#include "wasm/WasmCode.h" // for Metadata +#include "wasm/WasmDebug.h" // for DebugState +#include "wasm/WasmInstance.h" // for Instance +#include "wasm/WasmJS.h" // for WasmInstanceObject +#include "wasm/WasmTypes.h" // for Bytes, RootedWasmInstanceObject + +#include "debugger/Debugger-inl.h" // for Debugger::fromJSObject +#include "vm/JSObject-inl.h" // for InitClass +#include "vm/NativeObject-inl.h" // for NewTenuredObjectWithGivenProto + +namespace js { +class GlobalObject; +} + +using namespace js; + +using mozilla::AsVariant; +using mozilla::Maybe; +using mozilla::Nothing; +using mozilla::Some; + +const JSClassOps DebuggerSource::classOps_ = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + nullptr, // finalize + nullptr, // call + nullptr, // hasInstance + nullptr, // construct + CallTraceMethod<DebuggerSource>, // trace +}; + +const JSClass DebuggerSource::class_ = { + "Source", JSCLASS_HAS_PRIVATE | JSCLASS_HAS_RESERVED_SLOTS(RESERVED_SLOTS), + &classOps_}; + +/* static */ +NativeObject* DebuggerSource::initClass(JSContext* cx, + Handle<GlobalObject*> global, + HandleObject debugCtor) { + return InitClass(cx, debugCtor, nullptr, &class_, construct, 0, properties_, + methods_, nullptr, nullptr); +} + +/* static */ +DebuggerSource* DebuggerSource::create(JSContext* cx, HandleObject proto, + Handle<DebuggerSourceReferent> referent, + HandleNativeObject debugger) { + Rooted<DebuggerSource*> sourceObj( + cx, NewTenuredObjectWithGivenProto<DebuggerSource>(cx, proto)); + if (!sourceObj) { + return nullptr; + } + sourceObj->setReservedSlot(OWNER_SLOT, ObjectValue(*debugger)); + referent.get().match( + [&](auto sourceHandle) { sourceObj->setPrivateGCThing(sourceHandle); }); + + return sourceObj; +} + +Debugger* DebuggerSource::owner() const { + MOZ_ASSERT(isInstance()); + JSObject* dbgobj = &getReservedSlot(OWNER_SLOT).toObject(); + return Debugger::fromJSObject(dbgobj); +} + +// For internal use only. +NativeObject* DebuggerSource::getReferentRawObject() const { + return static_cast<NativeObject*>(getPrivate()); +} + +DebuggerSourceReferent DebuggerSource::getReferent() const { + if (NativeObject* referent = getReferentRawObject()) { + if (referent->is<ScriptSourceObject>()) { + return AsVariant(&referent->as<ScriptSourceObject>()); + } + return AsVariant(&referent->as<WasmInstanceObject>()); + } + return AsVariant(static_cast<ScriptSourceObject*>(nullptr)); +} + +void DebuggerSource::trace(JSTracer* trc) { + // There is a barrier on private pointers, so the Unbarriered marking + // is okay. + if (JSObject* referent = getReferentRawObject()) { + TraceManuallyBarrieredCrossCompartmentEdge( + trc, static_cast<JSObject*>(this), &referent, + "Debugger.Source referent"); + setPrivateUnbarriered(referent); + } +} + +/* static */ +bool DebuggerSource::construct(JSContext* cx, unsigned argc, Value* vp) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NO_CONSTRUCTOR, + "Debugger.Source"); + return false; +} + +/* static */ +DebuggerSource* DebuggerSource::check(JSContext* cx, HandleValue thisv) { + JSObject* thisobj = RequireObject(cx, thisv); + if (!thisobj) { + return nullptr; + } + if (!thisobj->is<DebuggerSource>()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_INCOMPATIBLE_PROTO, "Debugger.Source", + "method", thisobj->getClass()->name); + return nullptr; + } + + DebuggerSource* thisSourceObj = &thisobj->as<DebuggerSource>(); + + if (!thisSourceObj->isInstance()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_INCOMPATIBLE_PROTO, "Debugger.Source", + "method", "prototype object"); + return nullptr; + } + + return thisSourceObj; +} + +struct MOZ_STACK_CLASS DebuggerSource::CallData { + JSContext* cx; + const CallArgs& args; + + HandleDebuggerSource obj; + Rooted<DebuggerSourceReferent> referent; + + CallData(JSContext* cx, const CallArgs& args, HandleDebuggerSource obj) + : cx(cx), args(args), obj(obj), referent(cx, obj->getReferent()) {} + + bool getText(); + bool getBinary(); + bool getURL(); + bool getStartLine(); + bool getId(); + bool getDisplayURL(); + bool getElement(); + bool getElementProperty(); + bool getIntroductionScript(); + bool getIntroductionOffset(); + bool getIntroductionType(); + bool setSourceMapURL(); + bool getSourceMapURL(); + bool reparse(); + + using Method = bool (CallData::*)(); + + template <Method MyMethod> + static bool ToNative(JSContext* cx, unsigned argc, Value* vp); +}; + +template <DebuggerSource::CallData::Method MyMethod> +/* static */ +bool DebuggerSource::CallData::ToNative(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + RootedDebuggerSource obj(cx, DebuggerSource::check(cx, args.thisv())); + if (!obj) { + return false; + } + + CallData data(cx, args, obj); + return (data.*MyMethod)(); +} + +class DebuggerSourceGetTextMatcher { + JSContext* cx_; + + public: + explicit DebuggerSourceGetTextMatcher(JSContext* cx) : cx_(cx) {} + + using ReturnType = JSString*; + + ReturnType match(HandleScriptSourceObject sourceObject) { + ScriptSource* ss = sourceObject->source(); + bool hasSourceText; + if (!ScriptSource::loadSource(cx_, ss, &hasSourceText)) { + return nullptr; + } + if (!hasSourceText) { + return NewStringCopyZ<CanGC>(cx_, "[no source]"); + } + + if (ss->isFunctionBody()) { + return ss->functionBodyString(cx_); + } + + return ss->substring(cx_, 0, ss->length()); + } + + ReturnType match(Handle<WasmInstanceObject*> instanceObj) { + wasm::Instance& instance = instanceObj->instance(); + const char* msg; + if (!instance.debugEnabled()) { + msg = "Restart with developer tools open to view WebAssembly source."; + } else { + msg = "[debugger missing wasm binary-to-text conversion]"; + } + return NewStringCopyZ<CanGC>(cx_, msg); + } +}; + +bool DebuggerSource::CallData::getText() { + Value textv = obj->getReservedSlot(TEXT_SLOT); + if (!textv.isUndefined()) { + MOZ_ASSERT(textv.isString()); + args.rval().set(textv); + return true; + } + + DebuggerSourceGetTextMatcher matcher(cx); + JSString* str = referent.match(matcher); + if (!str) { + return false; + } + + args.rval().setString(str); + obj->setReservedSlot(TEXT_SLOT, args.rval()); + return true; +} + +bool DebuggerSource::CallData::getBinary() { + if (!referent.is<WasmInstanceObject*>()) { + ReportValueError(cx, JSMSG_DEBUG_BAD_REFERENT, JSDVG_SEARCH_STACK, + args.thisv(), nullptr, "a wasm source"); + return false; + } + + RootedWasmInstanceObject instanceObj(cx, referent.as<WasmInstanceObject*>()); + wasm::Instance& instance = instanceObj->instance(); + + if (!instance.debugEnabled()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_DEBUG_NO_BINARY_SOURCE); + return false; + } + + const wasm::Bytes& bytecode = instance.debug().bytecode(); + RootedObject arr(cx, JS_NewUint8Array(cx, bytecode.length())); + if (!arr) { + return false; + } + + memcpy(arr->as<TypedArrayObject>().dataPointerUnshared(), bytecode.begin(), + bytecode.length()); + + args.rval().setObject(*arr); + return true; +} + +class DebuggerSourceGetURLMatcher { + JSContext* cx_; + + public: + explicit DebuggerSourceGetURLMatcher(JSContext* cx) : cx_(cx) {} + + using ReturnType = Maybe<JSString*>; + + ReturnType match(HandleScriptSourceObject sourceObject) { + ScriptSource* ss = sourceObject->source(); + MOZ_ASSERT(ss); + if (ss->filename()) { + JSString* str = NewStringCopyZ<CanGC>(cx_, ss->filename()); + return Some(str); + } + return Nothing(); + } + ReturnType match(Handle<WasmInstanceObject*> instanceObj) { + return Some(instanceObj->instance().createDisplayURL(cx_)); + } +}; + +bool DebuggerSource::CallData::getURL() { + DebuggerSourceGetURLMatcher matcher(cx); + Maybe<JSString*> str = referent.match(matcher); + if (str.isSome()) { + if (!*str) { + return false; + } + args.rval().setString(*str); + } else { + args.rval().setNull(); + } + return true; +} + +class DebuggerSourceGetStartLineMatcher { + public: + using ReturnType = uint32_t; + + ReturnType match(HandleScriptSourceObject sourceObject) { + ScriptSource* ss = sourceObject->source(); + return ss->startLine(); + } + ReturnType match(Handle<WasmInstanceObject*> instanceObj) { return 0; } +}; + +bool DebuggerSource::CallData::getStartLine() { + DebuggerSourceGetStartLineMatcher matcher; + uint32_t line = referent.match(matcher); + args.rval().setNumber(line); + return true; +} + +class DebuggerSourceGetIdMatcher { + public: + using ReturnType = uint32_t; + + ReturnType match(HandleScriptSourceObject sourceObject) { + ScriptSource* ss = sourceObject->source(); + return ss->id(); + } + ReturnType match(Handle<WasmInstanceObject*> instanceObj) { return 0; } +}; + +bool DebuggerSource::CallData::getId() { + DebuggerSourceGetIdMatcher matcher; + uint32_t id = referent.match(matcher); + args.rval().setNumber(id); + return true; +} + +struct DebuggerSourceGetDisplayURLMatcher { + using ReturnType = const char16_t*; + ReturnType match(HandleScriptSourceObject sourceObject) { + ScriptSource* ss = sourceObject->source(); + MOZ_ASSERT(ss); + return ss->hasDisplayURL() ? ss->displayURL() : nullptr; + } + ReturnType match(Handle<WasmInstanceObject*> wasmInstance) { + return wasmInstance->instance().metadata().displayURL(); + } +}; + +bool DebuggerSource::CallData::getDisplayURL() { + DebuggerSourceGetDisplayURLMatcher matcher; + if (const char16_t* displayURL = referent.match(matcher)) { + JSString* str = JS_NewUCStringCopyZ(cx, displayURL); + if (!str) { + return false; + } + args.rval().setString(str); + } else { + args.rval().setNull(); + } + return true; +} + +struct DebuggerSourceGetElementMatcher { + JSContext* mCx = nullptr; + explicit DebuggerSourceGetElementMatcher(JSContext* cx_) : mCx(cx_) {} + using ReturnType = JSObject*; + ReturnType match(HandleScriptSourceObject sourceObject) { + return sourceObject->unwrappedElement(mCx); + } + ReturnType match(Handle<WasmInstanceObject*> wasmInstance) { return nullptr; } +}; + +bool DebuggerSource::CallData::getElement() { + DebuggerSourceGetElementMatcher matcher(cx); + if (JSObject* element = referent.match(matcher)) { + args.rval().setObjectOrNull(element); + if (!obj->owner()->wrapDebuggeeValue(cx, args.rval())) { + return false; + } + } else { + args.rval().setUndefined(); + } + return true; +} + +struct DebuggerSourceGetElementPropertyMatcher { + using ReturnType = Value; + ReturnType match(HandleScriptSourceObject sourceObject) { + return sourceObject->unwrappedElementAttributeName(); + } + ReturnType match(Handle<WasmInstanceObject*> wasmInstance) { + return UndefinedValue(); + } +}; + +bool DebuggerSource::CallData::getElementProperty() { + DebuggerSourceGetElementPropertyMatcher matcher; + args.rval().set(referent.match(matcher)); + return obj->owner()->wrapDebuggeeValue(cx, args.rval()); +} + +class DebuggerSourceGetIntroductionScriptMatcher { + JSContext* cx_; + Debugger* dbg_; + MutableHandleValue rval_; + + public: + DebuggerSourceGetIntroductionScriptMatcher(JSContext* cx, Debugger* dbg, + MutableHandleValue rval) + : cx_(cx), dbg_(dbg), rval_(rval) {} + + using ReturnType = bool; + + ReturnType match(HandleScriptSourceObject sourceObject) { + Rooted<BaseScript*> script(cx_, + sourceObject->unwrappedIntroductionScript()); + if (script) { + RootedObject scriptDO(cx_, dbg_->wrapScript(cx_, script)); + if (!scriptDO) { + return false; + } + rval_.setObject(*scriptDO); + } else { + rval_.setUndefined(); + } + return true; + } + + ReturnType match(Handle<WasmInstanceObject*> wasmInstance) { + RootedObject ds(cx_, dbg_->wrapWasmScript(cx_, wasmInstance)); + if (!ds) { + return false; + } + rval_.setObject(*ds); + return true; + } +}; + +bool DebuggerSource::CallData::getIntroductionScript() { + Debugger* dbg = obj->owner(); + DebuggerSourceGetIntroductionScriptMatcher matcher(cx, dbg, args.rval()); + return referent.match(matcher); +} + +struct DebuggerGetIntroductionOffsetMatcher { + using ReturnType = Value; + ReturnType match(HandleScriptSourceObject sourceObject) { + // Regardless of what's recorded in the ScriptSourceObject and + // ScriptSource, only hand out the introduction offset if we also have + // the script within which it applies. + ScriptSource* ss = sourceObject->source(); + if (ss->hasIntroductionOffset() && + sourceObject->unwrappedIntroductionScript()) { + return Int32Value(ss->introductionOffset()); + } + return UndefinedValue(); + } + ReturnType match(Handle<WasmInstanceObject*> wasmInstance) { + return UndefinedValue(); + } +}; + +bool DebuggerSource::CallData::getIntroductionOffset() { + DebuggerGetIntroductionOffsetMatcher matcher; + args.rval().set(referent.match(matcher)); + return true; +} + +struct DebuggerSourceGetIntroductionTypeMatcher { + using ReturnType = const char*; + ReturnType match(HandleScriptSourceObject sourceObject) { + ScriptSource* ss = sourceObject->source(); + MOZ_ASSERT(ss); + return ss->hasIntroductionType() ? ss->introductionType() : nullptr; + } + ReturnType match(Handle<WasmInstanceObject*> wasmInstance) { return "wasm"; } +}; + +bool DebuggerSource::CallData::getIntroductionType() { + DebuggerSourceGetIntroductionTypeMatcher matcher; + if (const char* introductionType = referent.match(matcher)) { + JSString* str = NewStringCopyZ<CanGC>(cx, introductionType); + if (!str) { + return false; + } + args.rval().setString(str); + } else { + args.rval().setUndefined(); + } + + return true; +} + +ScriptSourceObject* EnsureSourceObject(JSContext* cx, + HandleDebuggerSource obj) { + if (!obj->getReferent().is<ScriptSourceObject*>()) { + RootedValue v(cx, ObjectValue(*obj)); + ReportValueError(cx, JSMSG_DEBUG_BAD_REFERENT, JSDVG_SEARCH_STACK, v, + nullptr, "a JS source"); + return nullptr; + } + return obj->getReferent().as<ScriptSourceObject*>(); +} + +bool DebuggerSource::CallData::setSourceMapURL() { + RootedScriptSourceObject sourceObject(cx, EnsureSourceObject(cx, obj)); + if (!sourceObject) { + return false; + } + ScriptSource* ss = sourceObject->source(); + MOZ_ASSERT(ss); + + if (!args.requireAtLeast(cx, "set sourceMapURL", 1)) { + return false; + } + + JSString* str = ToString<CanGC>(cx, args[0]); + if (!str) { + return false; + } + + UniqueTwoByteChars chars = JS_CopyStringCharsZ(cx, str); + if (!chars) { + return false; + } + + if (!ss->setSourceMapURL(cx, std::move(chars))) { + return false; + } + + args.rval().setUndefined(); + return true; +} + +class DebuggerSourceGetSourceMapURLMatcher { + JSContext* cx_; + MutableHandleString result_; + + public: + explicit DebuggerSourceGetSourceMapURLMatcher(JSContext* cx, + MutableHandleString result) + : cx_(cx), result_(result) {} + + using ReturnType = bool; + ReturnType match(HandleScriptSourceObject sourceObject) { + ScriptSource* ss = sourceObject->source(); + MOZ_ASSERT(ss); + if (!ss->hasSourceMapURL()) { + result_.set(nullptr); + return true; + } + JSString* str = JS_NewUCStringCopyZ(cx_, ss->sourceMapURL()); + if (!str) { + return false; + } + result_.set(str); + return true; + } + ReturnType match(Handle<WasmInstanceObject*> instanceObj) { + wasm::Instance& instance = instanceObj->instance(); + if (!instance.debugEnabled()) { + result_.set(nullptr); + return true; + } + + RootedString str(cx_); + if (!instance.debug().getSourceMappingURL(cx_, &str)) { + return false; + } + + result_.set(str); + return true; + } +}; + +bool DebuggerSource::CallData::getSourceMapURL() { + RootedString result(cx); + DebuggerSourceGetSourceMapURLMatcher matcher(cx, &result); + if (!referent.match(matcher)) { + return false; + } + if (result) { + args.rval().setString(result); + } else { + args.rval().setNull(); + } + return true; +} + +template <typename Unit> +static JSScript* ReparseSource(JSContext* cx, HandleScriptSourceObject sso) { + AutoRealm ar(cx, sso); + ScriptSource* ss = sso->source(); + + JS::CompileOptions options(cx); + options.hideScriptFromDebugger = true; + options.setFileAndLine(ss->filename(), ss->startLine()); + + UncompressedSourceCache::AutoHoldEntry holder; + + ScriptSource::PinnedUnits<Unit> units(cx, ss, holder, 0, ss->length()); + if (!units.get()) { + return nullptr; + } + + JS::SourceText<Unit> srcBuf; + if (!srcBuf.init(cx, units.get(), ss->length(), + JS::SourceOwnership::Borrowed)) { + return nullptr; + } + + return JS::Compile(cx, options, srcBuf); +} + +bool DebuggerSource::CallData::reparse() { + RootedScriptSourceObject sourceObject(cx, EnsureSourceObject(cx, obj)); + if (!sourceObject) { + return false; + } + + if (!sourceObject->source()->hasSourceText()) { + JS_ReportErrorASCII(cx, "Source object missing text"); + return false; + } + + RootedScript script(cx); + if (sourceObject->source()->hasSourceType<mozilla::Utf8Unit>()) { + script = ReparseSource<mozilla::Utf8Unit>(cx, sourceObject); + } else { + script = ReparseSource<char16_t>(cx, sourceObject); + } + + if (!script) { + return false; + } + + Debugger* dbg = obj->owner(); + RootedObject scriptDO(cx, dbg->wrapScript(cx, script)); + if (!scriptDO) { + return false; + } + + args.rval().setObject(*scriptDO); + return true; +} + +const JSPropertySpec DebuggerSource::properties_[] = { + JS_DEBUG_PSG("text", getText), + JS_DEBUG_PSG("binary", getBinary), + JS_DEBUG_PSG("url", getURL), + JS_DEBUG_PSG("startLine", getStartLine), + JS_DEBUG_PSG("id", getId), + JS_DEBUG_PSG("element", getElement), + JS_DEBUG_PSG("displayURL", getDisplayURL), + JS_DEBUG_PSG("introductionScript", getIntroductionScript), + JS_DEBUG_PSG("introductionOffset", getIntroductionOffset), + JS_DEBUG_PSG("introductionType", getIntroductionType), + JS_DEBUG_PSG("elementAttributeName", getElementProperty), + JS_DEBUG_PSGS("sourceMapURL", getSourceMapURL, setSourceMapURL), + JS_PS_END}; + +const JSFunctionSpec DebuggerSource::methods_[] = { + JS_DEBUG_FN("reparse", reparse, 0), JS_FS_END}; |