/* -*- 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 // for uint32_t #include // for memcpy #include // 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, // trace }; const JSClass DebuggerSource::class_ = { "Source", JSCLASS_HAS_PRIVATE | JSCLASS_HAS_RESERVED_SLOTS(RESERVED_SLOTS), &classOps_}; /* static */ NativeObject* DebuggerSource::initClass(JSContext* cx, Handle global, HandleObject debugCtor) { return InitClass(cx, debugCtor, nullptr, &class_, construct, 0, properties_, methods_, nullptr, nullptr); } /* static */ DebuggerSource* DebuggerSource::create(JSContext* cx, HandleObject proto, Handle referent, HandleNativeObject debugger) { Rooted sourceObj( cx, NewTenuredObjectWithGivenProto(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(getPrivate()); } DebuggerSourceReferent DebuggerSource::getReferent() const { if (NativeObject* referent = getReferentRawObject()) { if (referent->is()) { return AsVariant(&referent->as()); } return AsVariant(&referent->as()); } return AsVariant(static_cast(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(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()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO, "Debugger.Source", "method", thisobj->getClass()->name); return nullptr; } DebuggerSource* thisSourceObj = &thisobj->as(); 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 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 static bool ToNative(JSContext* cx, unsigned argc, Value* vp); }; template /* 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(cx_, "[no source]"); } if (ss->isFunctionBody()) { return ss->functionBodyString(cx_); } return ss->substring(cx_, 0, ss->length()); } ReturnType match(Handle 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(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()) { ReportValueError(cx, JSMSG_DEBUG_BAD_REFERENT, JSDVG_SEARCH_STACK, args.thisv(), nullptr, "a wasm source"); return false; } RootedWasmInstanceObject instanceObj(cx, referent.as()); 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().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; ReturnType match(HandleScriptSourceObject sourceObject) { ScriptSource* ss = sourceObject->source(); MOZ_ASSERT(ss); if (ss->filename()) { JSString* str = NewStringCopyZ(cx_, ss->filename()); return Some(str); } return Nothing(); } ReturnType match(Handle instanceObj) { return Some(instanceObj->instance().createDisplayURL(cx_)); } }; bool DebuggerSource::CallData::getURL() { DebuggerSourceGetURLMatcher matcher(cx); Maybe 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 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 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 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 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 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 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 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 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 wasmInstance) { return "wasm"; } }; bool DebuggerSource::CallData::getIntroductionType() { DebuggerSourceGetIntroductionTypeMatcher matcher; if (const char* introductionType = referent.match(matcher)) { JSString* str = NewStringCopyZ(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()) { 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(); } 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(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 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 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 units(cx, ss, holder, 0, ss->length()); if (!units.get()) { return nullptr; } JS::SourceText 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()) { script = ReparseSource(cx, sourceObject); } else { script = ReparseSource(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};