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