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