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