Branch data Line data Source code
1 : : /* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */
2 : : // SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
3 : : // SPDX-FileCopyrightText: 2008 litl, LLC
4 : :
5 : : #include <config.h>
6 : :
7 : : #include <stdarg.h>
8 : : #include <stdint.h>
9 : : #include <string.h>
10 : :
11 : : #include <glib.h>
12 : :
13 : : #include <js/AllocPolicy.h>
14 : : #include <js/CharacterEncoding.h>
15 : : #include <js/ErrorReport.h>
16 : : #include <js/Exception.h>
17 : : #include <js/GCHashTable.h> // for GCHashSet
18 : : #include <js/HashTable.h> // for DefaultHasher
19 : : #include <js/PropertyAndElement.h>
20 : : #include <js/RootingAPI.h>
21 : : #include <js/SavedFrameAPI.h>
22 : : #include <js/Stack.h> // for BuildStackString
23 : : #include <js/String.h> // for JS_NewStringCopyUTF8Z
24 : : #include <js/TypeDecls.h>
25 : : #include <js/Utility.h> // for UniqueChars
26 : : #include <js/Value.h>
27 : : #include <mozilla/ScopeExit.h>
28 : :
29 : : #include "gjs/atoms.h"
30 : : #include "gjs/context-private.h"
31 : : #include "gjs/jsapi-util.h"
32 : : #include "gjs/macros.h"
33 : : #include "util/log.h"
34 : : #include "util/misc.h"
35 : :
36 : : using CauseSet = JS::GCHashSet<JSObject*, js::DefaultHasher<JSObject*>,
37 : : js::SystemAllocPolicy>;
38 : :
39 : : GJS_JSAPI_RETURN_CONVENTION
40 : 97 : static bool get_last_cause(JSContext* cx, JS::HandleValue v_exc,
41 : : JS::MutableHandleObject last_cause,
42 : : JS::MutableHandle<CauseSet> seen_causes) {
43 [ + + ]: 97 : if (!v_exc.isObject()) {
44 : 2 : last_cause.set(nullptr);
45 : 2 : return true;
46 : : }
47 : 95 : JS::RootedObject exc(cx, &v_exc.toObject());
48 : 95 : CauseSet::AddPtr entry = seen_causes.lookupForAdd(exc);
49 [ - + ]: 95 : if (entry) {
50 : 0 : last_cause.set(nullptr);
51 : 0 : return true;
52 : : }
53 [ - + ]: 95 : if (!seen_causes.add(entry, exc)) {
54 : 0 : JS_ReportOutOfMemory(cx);
55 : 0 : return false;
56 : : }
57 : :
58 : 95 : JS::RootedValue v_cause(cx);
59 : 95 : const GjsAtoms& atoms = GjsContextPrivate::atoms(cx);
60 [ - + ]: 95 : if (!JS_GetPropertyById(cx, exc, atoms.cause(), &v_cause))
61 : 0 : return false;
62 : :
63 [ + + ]: 95 : if (v_cause.isUndefined()) {
64 : 94 : last_cause.set(exc);
65 : 94 : return true;
66 : : }
67 : :
68 : 1 : return get_last_cause(cx, v_cause, last_cause, seen_causes);
69 : 95 : }
70 : :
71 : : GJS_JSAPI_RETURN_CONVENTION
72 : 96 : static bool append_new_cause(JSContext* cx, JS::HandleValue thrown,
73 : : JS::HandleValue new_cause, bool* appended) {
74 : 96 : g_assert(appended && "forgot out parameter");
75 : 96 : *appended = false;
76 : :
77 : 96 : JS::Rooted<CauseSet> seen_causes(cx);
78 : 96 : JS::RootedObject last_cause{cx};
79 [ - + ]: 96 : if (!get_last_cause(cx, thrown, &last_cause, &seen_causes))
80 : 0 : return false;
81 [ + + ]: 96 : if (!last_cause)
82 : 2 : return true;
83 : :
84 : 94 : const GjsAtoms& atoms = GjsContextPrivate::atoms(cx);
85 [ - + ]: 94 : if (!JS_SetPropertyById(cx, last_cause, atoms.cause(), new_cause))
86 : 0 : return false;
87 : :
88 : 94 : *appended = true;
89 : 94 : return true;
90 : 96 : }
91 : :
92 : 2966 : [[gnu::format(printf, 4, 0)]] static void gjs_throw_valist(
93 : : JSContext* cx, JSExnType error_kind, const char* error_name,
94 : : const char* format, va_list args) {
95 : 2966 : GjsAutoChar s = g_strdup_vprintf(format, args);
96 : 5932 : auto fallback = mozilla::MakeScopeExit([cx, &s]() {
97 : : // try just reporting it to error handler? should not
98 : : // happen though pretty much
99 : 0 : JS_ReportErrorUTF8(cx, "Failed to throw exception '%s'", s.get());
100 : 2966 : });
101 : :
102 : 2966 : JS::ConstUTF8CharsZ chars{s.get(), strlen(s.get())};
103 : 2966 : JS::RootedString message{cx, JS_NewStringCopyUTF8Z(cx, chars)};
104 [ - + ]: 2966 : if (!message)
105 : 0 : return;
106 : :
107 : 2966 : JS::RootedObject saved_frame{cx};
108 [ - + ]: 2966 : if (!JS::CaptureCurrentStack(cx, &saved_frame))
109 : 0 : return;
110 : :
111 : 2966 : JS::RootedString source_string{cx};
112 : 2966 : JS::GetSavedFrameSource(cx, /* principals = */ nullptr, saved_frame,
113 : : &source_string);
114 : : uint32_t line_num;
115 : 2966 : JS::GetSavedFrameLine(cx, nullptr, saved_frame, &line_num);
116 : : uint32_t column_num;
117 : 2966 : JS::GetSavedFrameColumn(cx, nullptr, saved_frame, &column_num);
118 : :
119 : 2966 : JS::RootedValue v_exc{cx};
120 [ - + ]: 2966 : if (!JS::CreateError(cx, error_kind, saved_frame, source_string, line_num,
121 : : column_num, /* report = */ nullptr, message,
122 : : /* cause = */ JS::NothingHandleValue, &v_exc))
123 : 0 : return;
124 : :
125 [ + + ]: 2966 : if (error_name) {
126 : 2727 : const GjsAtoms& atoms = GjsContextPrivate::atoms(cx);
127 : 2727 : JS::RootedValue v_name{cx};
128 : 2727 : JS::RootedObject exc{cx, &v_exc.toObject()};
129 [ + - ]: 5454 : if (!gjs_string_from_utf8(cx, error_name, &v_name) ||
130 [ - + - + ]: 5454 : !JS_SetPropertyById(cx, exc, atoms.name(), v_name))
131 : 0 : return;
132 [ + - + - ]: 2727 : }
133 : :
134 [ + + ]: 2966 : if (JS_IsExceptionPending(cx)) {
135 : : // Often it's unclear whether a given jsapi.h function will throw an
136 : : // exception, so we will throw ourselves "just in case"; in those cases,
137 : : // we append the new exception as the cause of the original exception.
138 : : // The second exception may add more info.
139 : 96 : JS::RootedValue pending(cx);
140 : 96 : JS_GetPendingException(cx, &pending);
141 : 96 : JS::AutoSaveExceptionState saved_exc{cx};
142 : : bool appended;
143 [ - + ]: 96 : if (!append_new_cause(cx, pending, v_exc, &appended))
144 : 0 : saved_exc.restore();
145 [ + + ]: 96 : if (!appended)
146 : 2 : gjs_debug(GJS_DEBUG_CONTEXT, "Ignoring second exception: '%s'",
147 : : s.get());
148 : 96 : } else {
149 : 2870 : JS_SetPendingException(cx, v_exc);
150 : : }
151 : :
152 : 2966 : fallback.release();
153 [ + - + - : 2966 : }
+ - + - +
- + - ]
154 : :
155 : : /* Throws an exception, like "throw new Error(message)"
156 : : *
157 : : * If an exception is already set in the context, this will
158 : : * NOT overwrite it. That's an important semantic since
159 : : * we want the "root cause" exception. To overwrite,
160 : : * use JS_ClearPendingException() first.
161 : : */
162 : : void
163 : 228 : gjs_throw(JSContext *context,
164 : : const char *format,
165 : : ...)
166 : : {
167 : : va_list args;
168 : :
169 : 228 : va_start(args, format);
170 : 228 : gjs_throw_valist(context, JSEXN_ERR, nullptr, format, args);
171 : 228 : va_end(args);
172 : 228 : }
173 : :
174 : : /*
175 : : * Like gjs_throw, but allows to customize the error
176 : : * class and 'name' property. Mainly used for throwing TypeError instead of
177 : : * error.
178 : : */
179 : 2738 : void gjs_throw_custom(JSContext *cx, JSExnType kind, const char *error_name,
180 : : const char *format, ...) {
181 : : va_list args;
182 : :
183 : 2738 : va_start(args, format);
184 : 2738 : gjs_throw_valist(cx, kind, error_name, format, args);
185 : 2738 : va_end(args);
186 : 2738 : }
187 : :
188 : : /**
189 : : * gjs_throw_literal:
190 : : *
191 : : * Similar to gjs_throw(), but does not treat its argument as
192 : : * a format string.
193 : : */
194 : : void
195 : 0 : gjs_throw_literal(JSContext *context,
196 : : const char *string)
197 : : {
198 : 0 : gjs_throw(context, "%s", string);
199 : 0 : }
200 : :
201 : : /**
202 : : * gjs_throw_gerror_message:
203 : : *
204 : : * Similar to gjs_throw_gerror(), but does not marshal the GError structure into
205 : : * JavaScript. Instead, it creates a regular JavaScript Error object and copies
206 : : * the GError's message into it.
207 : : *
208 : : * Use this when handling a GError in an internal function, where the error code
209 : : * and domain don't matter. So, for example, don't use it to throw errors
210 : : * around calling from JS into C code.
211 : : */
212 : 0 : bool gjs_throw_gerror_message(JSContext* cx, GjsAutoError const& error) {
213 : 0 : g_return_val_if_fail(error, false);
214 : 0 : gjs_throw_literal(cx, error->message);
215 : 0 : return false;
216 : : }
217 : :
218 : : /**
219 : : * format_saved_frame:
220 : : * @cx: the #JSContext
221 : : * @saved_frame: a SavedFrame #JSObject
222 : : * @indent: (optional): spaces of indentation
223 : : *
224 : : * Formats a stack trace as a UTF-8 string. If there are errors, ignores them
225 : : * and returns null.
226 : : * If you print this to stderr, you will need to re-encode it in filename
227 : : * encoding with g_filename_from_utf8().
228 : : *
229 : : * Returns (nullable) (transfer full): unique string
230 : : */
231 : 100 : JS::UniqueChars format_saved_frame(JSContext* cx, JS::HandleObject saved_frame,
232 : : size_t indent /* = 0 */) {
233 : 100 : JS::AutoSaveExceptionState saved_exc(cx);
234 : :
235 : 100 : JS::RootedString stack_trace(cx);
236 : 100 : JS::UniqueChars stack_utf8;
237 [ + - ]: 100 : if (JS::BuildStackString(cx, nullptr, saved_frame, &stack_trace, indent))
238 : 100 : stack_utf8 = JS_EncodeStringToUTF8(cx, stack_trace);
239 : :
240 : 100 : saved_exc.restore();
241 : :
242 : 100 : return stack_utf8;
243 : 100 : }
244 : :
245 : 2 : void gjs_warning_reporter(JSContext*, JSErrorReport* report) {
246 : : const char *warning;
247 : : GLogLevelFlags level;
248 : :
249 : 2 : g_assert(report);
250 : :
251 : 2 : if (gjs_environment_variable_is_set("GJS_ABORT_ON_OOM") &&
252 [ - + - - : 2 : !report->isWarning() && report->errorNumber == 137) {
- - - + ]
253 : : /* 137, JSMSG_OUT_OF_MEMORY */
254 : 0 : g_error("GJS ran out of memory at %s: %i.",
255 : : report->filename,
256 : : report->lineno);
257 : : }
258 : :
259 [ + - ]: 2 : if (report->isWarning()) {
260 : 2 : warning = "WARNING";
261 : 2 : level = G_LOG_LEVEL_MESSAGE;
262 : :
263 : : /* suppress bogus warnings. See mozilla/js/src/js.msg */
264 [ - + ]: 2 : if (report->errorNumber == 162) {
265 : : /* 162, JSMSG_UNDEFINED_PROP: warns every time a lazy property
266 : : * is resolved, since the property starts out
267 : : * undefined. When this is a real bug it should usually
268 : : * fail somewhere else anyhow.
269 : : */
270 : 0 : return;
271 : : }
272 : : } else {
273 : 0 : warning = "REPORTED";
274 : 0 : level = G_LOG_LEVEL_WARNING;
275 : : }
276 : :
277 : 2 : g_log(G_LOG_DOMAIN, level, "JS %s: [%s %d]: %s", warning, report->filename,
278 : 4 : report->lineno, report->message().c_str());
279 : : }
|