/* -*- 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"nsIConsoleAPIStorage.h" #include"nsIException.h"// for nsIStackFrame #include"nsIInterfaceRequestorUtils.h" #include"nsILoadContext.h" #include"nsISensitiveInfoHiddenURI.h" #include"nsISupportsPrimitives.h" #include"nsIWebNavigation.h" #include"nsIXPConnect.h"
// The maximum allowed number of concurrent timers per page. #define MAX_PAGE_TIMERS 10000
// The maximum allowed number of concurrent counters per page. #define MAX_PAGE_COUNTERS 10000
// The maximum stacktrace depth when populating the stacktrace array used for // console.trace(). #define DEFAULT_MAX_STACKTRACE_DEPTH 200
// This tags are used in the Structured Clone Algorithm to move js values from // worker thread to main thread #define CONSOLE_TAG_BLOB JS_SCTAG_USER_MIN
// This value is taken from ConsoleAPIStorage.js #define STORAGE_MAX_EVENTS 1000
/** * Console API in workers uses the Structured Clone Algorithm to move any value * from the worker thread to the main-thread. Some object cannot be moved and, * in these cases, we convert them to strings. * It's not the best, but at least we are able to show something.
*/
class ConsoleCallData final { public:
NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ConsoleCallData)
// These values are set in the owning thread and they contain the timestamp of // when the new timer has started, the name of it and the status of the // creation of it. If status is false, something went wrong. User // DOMHighResTimeStamp instead mozilla::TimeStamp because we use // monotonicTimer from Performance.now(); // They will be set on the owning thread and never touched again on that // thread. They will be used in order to create a ConsoleTimerStart dictionary // when console.time() is used.
DOMHighResTimeStamp mStartTimerValue MOZ_GUARDED_BY(mMutex);
nsString mStartTimerLabel MOZ_GUARDED_BY(mMutex);
Console::TimerStatus mStartTimerStatus MOZ_GUARDED_BY(mMutex);
// These values are set in the owning thread and they contain the duration, // the name and the status of the LogTimer method. If status is false, // something went wrong. They will be set on the owning thread and never // touched again on that thread. They will be used in order to create a // ConsoleTimerLogOrEnd dictionary. This members are set when // console.timeEnd() or console.timeLog() are called. double mLogTimerDuration MOZ_GUARDED_BY(mMutex);
nsString mLogTimerLabel MOZ_GUARDED_BY(mMutex);
Console::TimerStatus mLogTimerStatus MOZ_GUARDED_BY(mMutex);
// These 2 values are set by IncreaseCounter or ResetCounter on the owning // thread and they are used by CreateCounterOrResetCounterValue. // These members are set when console.count() or console.countReset() are // called.
nsString mCountLabel MOZ_GUARDED_BY(mMutex);
uint32_t mCountValue MOZ_GUARDED_BY(mMutex);
// The concept of outerID and innerID is misleading because when a // ConsoleCallData is created from a window, these are the window IDs, but // when the object is created from a SharedWorker, a ServiceWorker or a // subworker of a ChromeWorker these IDs are the type of worker and the // filename of the callee. // In Console.sys.mjs the ID is 'jsm'. enum { eString, eNumber, eUnknown } mIDType MOZ_GUARDED_BY(mMutex);
// Stack management is complicated, because we want to do it as // lazily as possible. Therefore, we have the following behavior: // 1) mTopStackFrame is initialized whenever we have any JS on the stack // 2) mReifiedStack is initialized if we're created in a worker. // 3) mStack is set (possibly to null if there is no JS on the stack) if // we're created on main thread.
Maybe<ConsoleStackEntry> mTopStackFrame MOZ_GUARDED_BY(mMutex);
Maybe<nsTArray<ConsoleStackEntry>> mReifiedStack MOZ_GUARDED_BY(mMutex);
nsCOMPtr<nsIStackFrame> mStack MOZ_GUARDED_BY(mMutex);
private:
~ConsoleCallData() = default;
NS_DECL_OWNINGTHREAD;
};
// MainThreadConsoleData instances are created on the Console thread and // referenced from both main and Console threads in order to provide the same // object for any ConsoleRunnables relating to the same Console. A Console // owns a MainThreadConsoleData; MainThreadConsoleData does not keep its // Console alive. class MainThreadConsoleData final {
NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MainThreadConsoleData);
JSObject* GetOrCreateSandbox(JSContext* aCx, nsIPrincipal* aPrincipal); // This method must receive aCx and aArguments in the same JS::Compartment. void ProcessCallData(JSContext* aCx, ConsoleCallData* aData, const Sequence<JS::Value>& aArguments);
// All members, except for mRefCnt, are accessed only on the main thread, // except in MainThreadConsoleData destruction, at which point there are no // other references.
nsCOMPtr<nsIConsoleAPIStorage> mStorage;
RefPtr<JSObjectHolder> mSandbox;
nsTArray<nsString> mGroupStack;
};
// This base class must be extended for Worker and for Worklet. class ConsoleRunnable : public StructuredCloneHolderBase { public:
~ConsoleRunnable() override {
MOZ_ASSERT(!mClonedData.mGlobal, "mClonedData.mGlobal is set and cleared in a main thread scope"); // Clear the StructuredCloneHolderBase class.
Clear();
}
// This is the same policy as when writing from the other side, in // WriteData.
JS::CloneDataPolicy cloneDataPolicy;
cloneDataPolicy.allowIntraClusterClonableSharedObjects();
cloneDataPolicy.allowSharedMemoryObjects();
JS::Rooted<JS::Value> argumentsValue(aCx); if (!Read(aCx, &argumentsValue, cloneDataPolicy)) { return;
}
bool WriteData(JSContext* aCx, JS::Handle<JS::Value> aValue) { // We use structuredClone to send the JSValue to the main-thread, in order // to store it into the Console API Service. The consumer will be the // console panel in the devtools and, because of this, we want to allow the // cloning of sharedArrayBuffers and WASM modules.
JS::CloneDataPolicy cloneDataPolicy;
cloneDataPolicy.allowIntraClusterClonableSharedObjects();
cloneDataPolicy.allowSharedMemoryObjects();
if (NS_WARN_IF(
!Write(aCx, aValue, JS::UndefinedHandleValue, cloneDataPolicy))) { // Ignore the message. returnfalse;
}
returntrue;
}
ConsoleStructuredCloneData mClonedData;
};
class ConsoleWorkletRunnable : public Runnable, public ConsoleRunnable { protected: explicit ConsoleWorkletRunnable(Console* aConsole)
: Runnable("dom::console::ConsoleWorkletRunnable"),
mConsoleData(aConsole->GetOrCreateMainThreadData()) {
WorkletThread::AssertIsOnWorkletThread();
nsCOMPtr<WorkletGlobalScope> global = do_QueryInterface(aConsole->mGlobal);
MOZ_ASSERT(global);
mWorkletImpl = global->Impl();
MOZ_ASSERT(mWorkletImpl);
}
// This runnable appends a CallData object into the Console queue running on // the main-thread. class ConsoleCallDataWorkletRunnable final : public ConsoleWorkletRunnable { public: static already_AddRefed<ConsoleCallDataWorkletRunnable> Create(
JSContext* aCx, Console* aConsole, ConsoleCallData* aConsoleData, const Sequence<JS::Value>& aArguments) {
WorkletThread::AssertIsOnWorkletThread();
RefPtr<ConsoleCallDataWorkletRunnable> runnable = new ConsoleCallDataWorkletRunnable(aConsole, aConsoleData);
if (!runnable->WriteArguments(aCx, aArguments)) { return nullptr;
}
// The CreateSandbox call returns a proxy to the actual sandbox object. We // don't need a proxy here.
global = js::UncheckedUnwrap(global);
JSAutoRealm ar(cx, global);
// We don't need to set a parent object in mCallData bacause there are not // DOM objects exposed to worklet.
ProcessCallData(cx, mConsoleData, mCallData);
}
return NS_OK;
}
RefPtr<ConsoleCallData> mCallData;
};
class ConsoleWorkerRunnable : public WorkerProxyToMainThreadRunnable, public ConsoleRunnable { public: explicit ConsoleWorkerRunnable(Console* aConsole)
: mConsoleData(aConsole->GetOrCreateMainThreadData()) {}
if (NS_WARN_IF(!WriteArguments(aCx, aArguments))) {
RunBackOnWorkerThreadForCleanup(workerPrivate); returnfalse;
}
if (NS_WARN_IF(!WorkerProxyToMainThreadRunnable::Dispatch(workerPrivate))) { // RunBackOnWorkerThreadForCleanup() will be called by // WorkerProxyToMainThreadRunnable::Dispatch(). returnfalse;
}
// This method is called in the main-thread. virtualvoid RunConsole(JSContext* aCx, nsIGlobalObject* aGlobal,
WorkerPrivate* aWorkerPrivate,
nsPIDOMWindowOuter* aOuterWindow,
nsPIDOMWindowInner* aInnerWindow) = 0;
// This runnable appends a CallData object into the Console queue running on // the main-thread. class ConsoleCallDataWorkerRunnable final : public ConsoleWorkerRunnable { public:
ConsoleCallDataWorkerRunnable(Console* aConsole, ConsoleCallData* aCallData)
: ConsoleWorkerRunnable(aConsole), mCallData(aCallData) {
MOZ_ASSERT(aCallData);
mCallData->AssertIsOnOwningThread();
}
// The windows have to run in parallel.
MOZ_ASSERT(!!aOuterWindow == !!aInnerWindow);
{
MutexAutoLock lock(mCallData->mMutex); if (aOuterWindow) {
mCallData->SetIDs(aOuterWindow->WindowID(), aInnerWindow->WindowID());
} else {
ConsoleStackEntry frame; if (mCallData->mTopStackFrame) {
frame = *mCallData->mTopStackFrame;
}
nsCString id = frame.mFilename;
nsString innerID; if (aWorkerPrivate->IsSharedWorker()) {
innerID = u"SharedWorker"_ns;
} elseif (aWorkerPrivate->IsServiceWorker()) {
innerID = u"ServiceWorker"_ns; // Use scope as ID so the webconsole can decide if the message should // show up per tab
id = aWorkerPrivate->ServiceWorkerScope();
} else {
innerID = u"Worker"_ns;
}
// This runnable calls ProfileMethod() on the console on the main-thread. class ConsoleProfileWorkletRunnable final : public ConsoleWorkletRunnable { public: static already_AddRefed<ConsoleProfileWorkletRunnable> Create(
JSContext* aCx, Console* aConsole, Console::MethodName aName, const nsAString& aAction, const Sequence<JS::Value>& aArguments) {
WorkletThread::AssertIsOnWorkletThread();
RefPtr<ConsoleProfileWorkletRunnable> runnable = new ConsoleProfileWorkletRunnable(aConsole, aName, aAction);
if (!runnable->WriteArguments(aCx, aArguments)) { return nullptr;
}
// The CreateSandbox call returns a proxy to the actual sandbox object. We // don't need a proxy here.
global = js::UncheckedUnwrap(global);
JSAutoRealm ar(cx, global);
// We don't need to set a parent object in mCallData bacause there are not // DOM objects exposed to worklet.
ProcessProfileData(cx, mName, mAction);
return NS_OK;
}
Console::MethodName mName;
nsString mAction;
};
// This runnable calls ProfileMethod() on the console on the main-thread. class ConsoleProfileWorkerRunnable final : public ConsoleWorkerRunnable { public:
ConsoleProfileWorkerRunnable(Console* aConsole, Console::MethodName aName, const nsAString& aAction)
: ConsoleWorkerRunnable(aConsole), mName(aName), mAction(aAction) {
MOZ_ASSERT(aConsole);
}
NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(Console) for (uint32_t i = 0; i < tmp->mArgumentStorage.length(); ++i) {
tmp->mArgumentStorage[i].Trace(aCallbacks, aClosure);
}
NS_IMPL_CYCLE_COLLECTION_TRACE_END
if (aWindow) {
innerWindowID = aWindow->WindowID();
// Without outerwindow any console message coming from this object will not // shown in the devtools webconsole. But this should be fine because // probably we are shutting down, or the window is CCed/GCed.
nsPIDOMWindowOuter* outerWindow = aWindow->GetOuterWindow(); if (outerWindow) {
outerWindowID = outerWindow->WindowID();
}
}
RefPtr<Console> console = new Console(aCx, nsGlobalWindowInner::Cast(aWindow),
outerWindowID, innerWindowID);
console->Initialize(aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr;
}
RefPtr<Console> console = new Console(aCx, aGlobal, aOuterWindowID, aInnerWindowID);
console->Initialize(aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr;
}
return console.forget();
}
Console::Console(JSContext* aCx, nsIGlobalObject* aGlobal,
uint64_t aOuterWindowID, uint64_t aInnerWindowID, const nsAString& aPrefix)
: mGlobal(aGlobal),
mOuterID(aOuterWindowID),
mInnerID(aInnerWindowID),
mDumpToStdout(false),
mLogModule(nullptr),
mPrefix(aPrefix),
mChromeInstance(false),
mCurrentLogLevel(WebIDLLogLevelToInteger(ConsoleLogLevel::All)),
mStatus(eUnknown),
mCreationTimeStamp(TimeStamp::Now()) { // Let's enable the dumping to stdout by default for chrome. if (nsContentUtils::ThreadsafeIsSystemCaller(aCx)) {
mDumpToStdout = StaticPrefs::devtools_console_stdout_chrome();
} else {
mDumpToStdout = StaticPrefs::devtools_console_stdout_content();
}
// By default, the console uses "console" MOZ_LOG module name, // but ConsoleInstance may pass a custom prefix which we will use a module // name.
mLogModule = mPrefix.IsEmpty()
? LogModule::Get("console")
: LogModule::Get(NS_ConvertUTF16toUTF8(mPrefix).get());
if (!NS_IsMainThread()) { // Here we are in a worker thread.
RefPtr<ConsoleProfileWorkerRunnable> runnable = new ConsoleProfileWorkerRunnable(this, aMethodName, aAction);
RefPtr<ConsoleCallData> callData = new ConsoleCallData(aMethodName, aMethodString, this);
MutexAutoLock lock(callData->mMutex);
if (!StoreCallData(aCx, callData, aData)) { return;
}
OriginAttributes oa;
if (NS_IsMainThread()) { if (mGlobal) { // Save the principal's OriginAttributes in the console event data // so that we will be able to filter messages by origin attributes.
nsCOMPtr<nsIScriptObjectPrincipal> sop = do_QueryInterface(mGlobal); if (NS_WARN_IF(!sop)) { return;
}
nsCOMPtr<nsIPrincipal> principal = sop->GetPrincipal(); if (NS_WARN_IF(!principal)) { return;
}
oa = principal->OriginAttributesRef();
callData->SetAddonId(principal);
#ifdef DEBUG if (!principal->IsSystemPrincipal()) {
nsCOMPtr<nsIWebNavigation> webNav = do_GetInterface(mGlobal); if (webNav) {
nsCOMPtr<nsILoadContext> loadContext = do_QueryInterface(webNav);
MOZ_ASSERT(loadContext);
bool pb; if (NS_SUCCEEDED(loadContext->GetUsePrivateBrowsing(&pb))) {
MOZ_ASSERT(pb == oa.IsPrivateBrowsing());
}
}
} #endif
}
} elseif (WorkletThread::IsOnWorkletThread()) {
nsCOMPtr<WorkletGlobalScope> global = do_QueryInterface(mGlobal);
MOZ_ASSERT(global);
oa = global->Impl()->OriginAttributesRef();
} else {
WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate();
MOZ_ASSERT(workerPrivate);
oa = workerPrivate->GetOriginAttributes();
}
if (stack) {
callData->mTopStackFrame.emplace();
StackFrameToStackEntry(aCx, stack, *callData->mTopStackFrame);
}
if (NS_IsMainThread()) {
callData->mStack = stack;
} else { // nsIStackFrame is not threadsafe, so we need to snapshot it now, // before we post our runnable to the main thread.
callData->mReifiedStack.emplace();
ReifyStack(aCx, stack, *callData->mReifiedStack);
}
// Before processing this CallData differently, it's time to call the dump // function. // // Only log the stack trace for console.trace() and console.assert() if (aMethodName == MethodTrace || aMethodName == MethodAssert) {
MaybeExecuteDumpFunction(aCx, aMethodName, aMethodString, aData, stack,
monotonicTimer);
} else {
MaybeExecuteDumpFunction(aCx, aMethodName, aMethodString, aData, nullptr,
monotonicTimer);
}
if (NS_IsMainThread()) { if (mInnerID) {
callData->SetIDs(mOuterID, mInnerID);
} elseif (!mPassedInnerID.IsEmpty()) {
callData->SetIDs(u"jsm"_ns, mPassedInnerID);
} else {
nsAutoCString filename; if (callData->mTopStackFrame.isSome()) {
filename = callData->mTopStackFrame->mFilename;
}
callData->SetIDs(u"jsm"_ns, NS_ConvertUTF8toUTF16(filename));
}
// Just because we don't want to expose // retrieveConsoleEvents/setConsoleEventHandler to main-thread, we can // cleanup the mCallDataStorage:
UnstoreCallData(callData); return;
}
if (WorkletThread::IsOnWorkletThread()) {
RefPtr<ConsoleCallDataWorkletRunnable> runnable =
ConsoleCallDataWorkletRunnable::Create(aCx, this, callData, aData); if (!runnable) { return;
}
NS_DispatchToMainThread(runnable); return;
}
// We do this only in workers for now.
NotifyHandler(aCx, aData, callData);
if (StaticPrefs::dom_worker_console_dispatch_events_to_main_thread()) {
RefPtr<ConsoleCallDataWorkerRunnable> runnable = new ConsoleCallDataWorkerRunnable(this, callData);
Unused << NS_WARN_IF(!runnable->Dispatch(aCx, aData));
}
}
if (!mMainThreadData) {
mMainThreadData = new MainThreadConsoleData();
}
return mMainThreadData;
}
// We store information to lazily compute the stack in the reserved slots of // LazyStackGetter. The first slot always stores a JS object: it's either the // JS wrapper of the nsIStackFrame or the actual reified stack representation. // The second slot is a PrivateValue() holding an nsIStackFrame* when we haven't // reified the stack yet, or an UndefinedValue() otherwise. enum { SLOT_STACKOBJ, SLOT_RAW_STACK };
// We want to create a console event object and pass it to our // nsIConsoleAPIStorage implementation. We want to define some accessor // properties on this object, and those will need to keep an nsIStackFrame // alive. But nsIStackFrame cannot be wrapped in an untrusted scope. And // further, passing untrusted objects to system code is likely to run afoul of // Object Xrays. So we want to wrap in a system-principal scope here. But // which one? We could cheat and try to get the underlying JSObject* of // mStorage, but that's a bit fragile. Instead, we just use the junk scope, // with explicit permission from the XPConnect module owner. If you're // tempted to do that anywhere else, talk to said module owner first.
// aCx and aArguments are in the same compartment.
JS::Rooted<JSObject*> targetScope(aCx, xpc::PrivilegedJunkScope()); if (NS_WARN_IF(!Console::PopulateConsoleNotificationInTheTargetScope(
aCx, aArguments, targetScope, &eventValue, aData, &mGroupStack))) { return;
}
if (!mStorage) {
mStorage = do_GetService("@mozilla.org/consoleAPI-storage;1");
}
if (!mStorage) {
NS_WARNING("Failed to get the ConsoleAPIStorage service."); return;
}
switch (aData->mMethodName) { case MethodLog: case MethodInfo: case MethodWarn: case MethodError: case MethodException: case MethodDebug: case MethodAssert: case MethodGroup: case MethodGroupCollapsed: case MethodTrace:
event.mArguments.Construct();
event.mStyles.Construct(); if (NS_WARN_IF(!ProcessArguments(aCx, aArguments,
event.mArguments.Value(),
event.mStyles.Value()))) { returnfalse;
}
break;
default:
event.mArguments.Construct(); if (NS_WARN_IF(
!event.mArguments.Value().AppendElements(aArguments, fallible))) { returnfalse;
}
}
if (ShouldIncludeStackTrace(aData->mMethodName)) { // Now define the "stacktrace" property on eventObj. There are two cases // here. Either we came from a worker and have a reified stack, or we want // to define a getter that will lazily reify the stack. if (aData->mReifiedStack) {
JS::Rooted<JS::Value> stacktrace(aCx); if (NS_WARN_IF(!ToJSValue(aCx, *aData->mReifiedStack, &stacktrace)) ||
NS_WARN_IF(!JS_DefineProperty(aCx, eventObj, "stacktrace", stacktrace,
JSPROP_ENUMERATE))) { returnfalse;
}
} else {
JSFunction* fun =
js::NewFunctionWithReserved(aCx, LazyStackGetter, 0, 0, "stacktrace"); if (NS_WARN_IF(!fun)) { returnfalse;
}
// We want to store our stack in the function and have it stay alive. But // we also need sane access to the C++ nsIStackFrame. So store both a JS // wrapper and the raw pointer: the former will keep the latter alive.
JS::Rooted<JS::Value> stackVal(aCx);
nsresult rv = nsContentUtils::WrapNative(aCx, aData->mStack, &stackVal); if (NS_WARN_IF(NS_FAILED(rv))) { returnfalse;
}
if (aMantissa >= 0) {
aFormat.Append('.');
aFormat.AppendInt(aMantissa);
}
aFormat.Append(aCh);
}
// If the first JS::Value of the array is a string, this method uses it to // format a string. The supported sequences are: // %s - string // %d,%i - integer // %f - double // %o,%O - a JS object. // %c - style string. // The output is an array where any object is a separated item, the rest is // unified in a format string. // Example if the input is: // "string: %s, integer: %d, object: %o, double: %f", 's', 1, window, 0.9 // The output will be: // [ "string: s, integer: 1, object: ", window, ", double: 0.9" ] // // The aStyles array is populated with the style strings that the function // finds based the format string. The index of the styles matches the indexes // of elements that need the custom styling from aSequence. For elements with // no custom styling the array is padded with null elements. staticbool ProcessArguments(JSContext* aCx, const Sequence<JS::Value>& aData,
Sequence<JS::Value>& aSequence,
Sequence<nsString>& aStyles) { // This method processes the arguments as format strings (%d, %i, %s...) // only if the first element of them is a valid and not-empty string.
JS::Rooted<JS::Value> v(aCx); if (index < aData.Length()) {
v = aData[index++];
}
if (NS_WARN_IF(!aSequence.AppendElement(v, fallible))) { returnfalse;
}
break;
}
case'c': { // If there isn't any output but there's already a style, then // discard the previous style and use the next one instead. if (output.IsEmpty() && !aStyles.IsEmpty()) {
aStyles.RemoveLastElement();
}
if (NS_WARN_IF(!FlushOutput(aCx, aSequence, output))) { returnfalse;
}
if (index < aData.Length()) {
JS::Rooted<JS::Value> v(aCx, aData[index++]);
JS::Rooted<JSString*> jsString(aCx, JS::ToString(aCx, v)); if (NS_WARN_IF(!jsString)) { returnfalse;
}
int32_t diff = aSequence.Length() - aStyles.Length(); if (diff > 0) { for (int32_t i = 0; i < diff; i++) { if (NS_WARN_IF(!aStyles.AppendElement(VoidString(), fallible))) { returnfalse;
}
}
}
nsAutoJSString string; if (NS_WARN_IF(!string.init(aCx, jsString))) { returnfalse;
}
if (NS_WARN_IF(!aStyles.AppendElement(string, fallible))) { returnfalse;
}
} break;
}
case's': if (index < aData.Length()) {
JS::Rooted<JS::Value> value(aCx, aData[index++]);
JS::Rooted<JSString*> jsString(aCx, JS::ToString(aCx, value)); if (NS_WARN_IF(!jsString)) { returnfalse;
}
nsAutoJSString v; if (NS_WARN_IF(!v.init(aCx, jsString))) { returnfalse;
}
output.Append(v);
} break;
case'd': case'i': if (index < aData.Length()) {
JS::Rooted<JS::Value> value(aCx, aData[index++]);
if (value.isBigInt()) {
JS::Rooted<JSString*> jsString(aCx, JS::ToString(aCx, value)); if (NS_WARN_IF(!jsString)) { returnfalse;
}
case'f': if (index < aData.Length()) {
JS::Rooted<JS::Value> value(aCx, aData[index++]);
double v; if (NS_WARN_IF(!JS::ToNumber(aCx, value, &v))) { returnfalse;
}
// nspr returns "nan", but we want to expose it as "NaN" if (std::isnan(v)) {
output.AppendFloat(v);
} else {
nsCString format;
MakeFormatString(format, integer, std::min(mantissa, 15), 'f');
output.AppendPrintf(format.get(), v);
}
} break;
default:
output.Append(tmp); break;
}
}
if (NS_WARN_IF(!FlushOutput(aCx, aSequence, output))) { returnfalse;
}
// Discard trailing style element if there is no output to apply it to. if (aStyles.Length() > aSequence.Length()) {
aStyles.TruncateLength(aSequence.Length());
}
// The rest of the array, if unused by the format string. for (; index < aData.Length(); ++index) { if (NS_WARN_IF(!aSequence.AppendElement(aData[index], fallible))) { returnfalse;
}
}
returntrue;
}
// Stringify and Concat all the JS::Value in a single string using ' ' as // separator. The new group name will be stored in aGroupStack array. staticvoid ComposeAndStoreGroupName(JSContext* aCx, const Sequence<JS::Value>& aData,
nsAString& aName,
nsTArray<nsString>* aGroupStack) {
StringJoinAppend(
aName, u" "_ns, aData, [aCx](nsAString& dest, const JS::Value& valueRef) {
JS::Rooted<JS::Value> value(aCx, valueRef);
JS::Rooted<JSString*> jsString(aCx, JS::ToString(aCx, value)); if (!jsString) { return;
}
nsAutoJSString string; if (!string.init(aCx, jsString)) { return;
}
dest.Append(string);
});
aGroupStack->AppendElement(aName);
}
// Remove the last group name and return that name. It returns false if // aGroupStack is empty. staticbool UnstoreGroupName(nsAString& aName,
nsTArray<nsString>* aGroupStack) { if (aGroupStack->IsEmpty()) { returnfalse;
}
¤ Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.0.71Bemerkung:
(vorverarbeitet)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung ist noch experimentell.