/* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */ // SPDX-License-Identifier: MIT OR LGPL-2.0-or-later // SPDX-FileCopyrightText: 2008 litl, LLC #include #include #include #include #include #include #include #include #include // for GCHashSet #include // for DefaultHasher #include #include #include // for BuildStackString #include #include // for UniqueChars #include #include #include // for JS_GetClassObject #include // for JSProtoKey, JSProto_Error, JSProto... #include "gjs/atoms.h" #include "gjs/context-private.h" #include "gjs/jsapi-util.h" #include "gjs/macros.h" #include "util/log.h" #include "util/misc.h" using CauseSet = JS::GCHashSet, js::SystemAllocPolicy>; GJS_JSAPI_RETURN_CONVENTION static bool get_last_cause_impl(JSContext* cx, JS::HandleValue v_exc, JS::MutableHandleObject last_cause, JS::MutableHandle seen_causes) { if (!v_exc.isObject()) { last_cause.set(nullptr); return true; } JS::RootedObject exc(cx, &v_exc.toObject()); CauseSet::AddPtr entry = seen_causes.lookupForAdd(exc); if (entry) { last_cause.set(nullptr); return true; } if (!seen_causes.add(entry, exc)) { JS_ReportOutOfMemory(cx); return false; } JS::RootedValue v_cause(cx); const GjsAtoms& atoms = GjsContextPrivate::atoms(cx); if (!JS_GetPropertyById(cx, exc, atoms.cause(), &v_cause)) return false; if (v_cause.isUndefined()) { last_cause.set(exc); return true; } return get_last_cause_impl(cx, v_cause, last_cause, seen_causes); } GJS_JSAPI_RETURN_CONVENTION static bool get_last_cause(JSContext* cx, JS::HandleValue v_exc, JS::MutableHandleObject last_cause) { JS::Rooted seen_causes(cx); return get_last_cause_impl(cx, v_exc, last_cause, &seen_causes); } /* * See: * https://bugzilla.mozilla.org/show_bug.cgi?id=166436 * https://bugzilla.mozilla.org/show_bug.cgi?id=215173 * * Very surprisingly, jsapi.h lacks any way to "throw new Error()" * * So here is an awful hack inspired by * http://egachine.berlios.de/embedding-sm-best-practice/embedding-sm-best-practice.html#error-handling */ [[gnu::format(printf, 4, 0)]] static void gjs_throw_valist( JSContext* context, JSProtoKey error_kind, const char* error_name, const char* format, va_list args) { char *s; bool result; s = g_strdup_vprintf(format, args); JS::RootedObject constructor(context); JS::RootedValue v_constructor(context), exc_val(context); JS::RootedObject new_exc(context); JS::RootedValueArray<1> error_args(context); result = false; if (!gjs_string_from_utf8(context, s, error_args[0])) { JS_ReportErrorUTF8(context, "Failed to copy exception string"); goto out; } if (!JS_GetClassObject(context, error_kind, &constructor)) goto out; v_constructor.setObject(*constructor); /* throw new Error(message) */ if (!JS::Construct(context, v_constructor, error_args, &new_exc)) goto out; if (!new_exc) goto out; if (error_name) { const GjsAtoms& atoms = GjsContextPrivate::atoms(context); JS::RootedValue name_value(context); if (!gjs_string_from_utf8(context, error_name, &name_value) || !JS_SetPropertyById(context, new_exc, atoms.name(), name_value)) goto out; } exc_val.setObject(*new_exc); if (JS_IsExceptionPending(context)) { // Often it's unclear whether a given jsapi.h function will throw an // exception, so we will throw ourselves "just in case"; in those cases, // we append the new exception as the cause of the original exception. // The second exception may add more info. const GjsAtoms& atoms = GjsContextPrivate::atoms(context); JS::RootedValue pending(context); JS_GetPendingException(context, &pending); JS::RootedObject last_cause(context); if (!get_last_cause(context, pending, &last_cause)) goto out; if (last_cause) { if (!JS_SetPropertyById(context, last_cause, atoms.cause(), exc_val)) goto out; } else { gjs_debug(GJS_DEBUG_CONTEXT, "Ignoring second exception: '%s'", s); } } else { JS_SetPendingException(context, exc_val); } result = true; out: if (!result) { /* try just reporting it to error handler? should not * happen though pretty much */ JS_ReportErrorUTF8(context, "Failed to throw exception '%s'", s); } g_free(s); } /* Throws an exception, like "throw new Error(message)" * * If an exception is already set in the context, this will * NOT overwrite it. That's an important semantic since * we want the "root cause" exception. To overwrite, * use JS_ClearPendingException() first. */ void gjs_throw(JSContext *context, const char *format, ...) { va_list args; va_start(args, format); gjs_throw_valist(context, JSProto_Error, nullptr, format, args); va_end(args); } /* * Like gjs_throw, but allows to customize the error * class and 'name' property. Mainly used for throwing TypeError instead of * error. */ void gjs_throw_custom(JSContext *cx, JSProtoKey kind, const char *error_name, const char *format, ...) { va_list args; switch (kind) { case JSProto_Error: case JSProto_EvalError: case JSProto_InternalError: case JSProto_RangeError: case JSProto_ReferenceError: case JSProto_SyntaxError: case JSProto_TypeError: case JSProto_URIError: break; default: g_return_if_reached(); } va_start(args, format); gjs_throw_valist(cx, kind, error_name, format, args); va_end(args); } /** * gjs_throw_literal: * * Similar to gjs_throw(), but does not treat its argument as * a format string. */ void gjs_throw_literal(JSContext *context, const char *string) { gjs_throw(context, "%s", string); } /** * gjs_throw_gerror_message: * * Similar to gjs_throw_gerror(), but does not marshal the GError structure into * JavaScript. Instead, it creates a regular JavaScript Error object and copies * the GError's message into it. * * Use this when handling a GError in an internal function, where the error code * and domain don't matter. So, for example, don't use it to throw errors * around calling from JS into C code. * * Frees the GError. */ bool gjs_throw_gerror_message(JSContext* cx, GError* error) { g_return_val_if_fail(error, false); gjs_throw_literal(cx, error->message); g_error_free(error); return false; } /** * gjs_format_stack_trace: * @cx: the #JSContext * @saved_frame: a SavedFrame #JSObject * * Formats a stack trace as a string in filename encoding, suitable for * printing to stderr. Ignores any errors. * * Returns: unique string in filename encoding, or nullptr if no stack trace */ GjsAutoChar gjs_format_stack_trace(JSContext *cx, JS::HandleObject saved_frame) { JS::AutoSaveExceptionState saved_exc(cx); JS::RootedString stack_trace(cx); JS::UniqueChars stack_utf8; if (JS::BuildStackString(cx, nullptr, saved_frame, &stack_trace, 2)) stack_utf8 = JS_EncodeStringToUTF8(cx, stack_trace); saved_exc.restore(); if (!stack_utf8) return nullptr; return g_filename_from_utf8(stack_utf8.get(), -1, nullptr, nullptr, nullptr); } void gjs_warning_reporter(JSContext*, JSErrorReport* report) { const char *warning; GLogLevelFlags level; g_assert(report); if (gjs_environment_variable_is_set("GJS_ABORT_ON_OOM") && !report->isWarning() && report->errorNumber == 137) { /* 137, JSMSG_OUT_OF_MEMORY */ g_error("GJS ran out of memory at %s: %i.", report->filename, report->lineno); } if (report->isWarning()) { warning = "WARNING"; level = G_LOG_LEVEL_MESSAGE; /* suppress bogus warnings. See mozilla/js/src/js.msg */ if (report->errorNumber == 162) { /* 162, JSMSG_UNDEFINED_PROP: warns every time a lazy property * is resolved, since the property starts out * undefined. When this is a real bug it should usually * fail somewhere else anyhow. */ return; } } else { warning = "REPORTED"; level = G_LOG_LEVEL_WARNING; } g_log(G_LOG_DOMAIN, level, "JS %s: [%s %d]: %s", warning, report->filename, report->lineno, report->message().c_str()); }