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: 2016 Endless Mobile, Inc.
4 : : // SPDX-FileContributor: Authored by: Philip Chimento <philip@endlessm.com>
5 : :
6 : : #pragma once
7 : :
8 : : #include <config.h>
9 : :
10 : : #include <stdint.h>
11 : :
12 : : #include <type_traits> // for enable_if, is_enum, is_same
13 : : #include <utility> // for move
14 : :
15 : : #include <glib.h>
16 : :
17 : : #include <js/CallArgs.h>
18 : : #include <js/Conversions.h>
19 : : #include <js/Exception.h>
20 : : #include <js/Result.h>
21 : : #include <js/RootingAPI.h>
22 : : #include <js/TypeDecls.h>
23 : : #include <js/Utility.h> // for UniqueChars
24 : : #include <js/Value.h>
25 : : #include <mozilla/Result.h> // for GenericErrorResult
26 : : #include <mozilla/ResultVariant.h> // IWYU pragma: keep
27 : :
28 : : #include "gjs/auto.h"
29 : : #include "gjs/jsapi-util.h"
30 : : #include "gjs/macros.h"
31 : :
32 : : namespace detail {
33 : :
34 : : [[nodiscard]] GJS_ALWAYS_INLINE
35 : : static inline bool check_nullable(const char*& fchar, const char*& fmt_string) {
36 [ + + + - : 96443 : if (*fchar != '?')
+ + + - +
+ + - + +
- - + + -
- + + - -
+ - - - +
- - - + +
- - + + +
- + + +
- ]
37 : 96434 : return false;
38 : :
39 : 9 : fchar++;
40 : 9 : fmt_string++;
41 : 9 : g_assert(*fchar != '\0' &&
42 : : "Invalid format string, parameter required after '?'");
43 : 9 : return true;
44 : : }
45 : :
46 : : class ParseArgsErr {
47 : : Gjs::AutoChar m_message;
48 : :
49 : : public:
50 : 7 : explicit ParseArgsErr(const char* literal_msg)
51 : 7 : : m_message(literal_msg, Gjs::TakeOwnership{}) {}
52 : : template <typename F>
53 : 10 : ParseArgsErr(const char* format_string, F param)
54 : 10 : : m_message(g_strdup_printf(format_string, param)) {}
55 : :
56 : 17 : [[nodiscard]] const char* message() const { return m_message.get(); }
57 : : };
58 : :
59 : : template <typename... Args>
60 : 17 : constexpr auto Err(Args... args) {
61 : : return mozilla::GenericErrorResult{
62 : 27 : ParseArgsErr{std::forward<Args>(args)...}};
63 : : }
64 : :
65 : : using ParseArgsResult = JS::Result<JS::Ok, ParseArgsErr>;
66 : :
67 : : /* This preserves the previous behaviour of gjs_parse_args(), but maybe we want
68 : : * to use JS::ToBoolean instead? */
69 : : GJS_ALWAYS_INLINE
70 : : static inline ParseArgsResult assign(JSContext*, char c, bool nullable,
71 : : JS::HandleValue value, bool* ref) {
72 : 353 : if (c != 'b')
73 : 1 : return Err("Wrong type for %c, got bool*", c);
74 [ + + ]: 352 : if (!value.isBoolean())
75 : 1 : return Err("Not a boolean");
76 [ + + ]: 351 : if (nullable)
77 : 1 : return Err("Invalid format string combination ?b");
78 : 350 : *ref = value.toBoolean();
79 : 350 : return JS::Ok();
80 : : }
81 : :
82 : : GJS_ALWAYS_INLINE
83 : : static inline ParseArgsResult assign(JSContext*, char c, bool nullable,
84 : : JS::HandleValue value,
85 : : JS::MutableHandleObject ref) {
86 : 23912 : if (c != 'o')
87 : 1 : return Err("Wrong type for %c, got JS::MutableHandleObject", c);
88 [ + + + - : 23911 : if (nullable && value.isNull()) {
+ + ]
89 : 1 : ref.set(nullptr);
90 : 1 : return JS::Ok();
91 : : }
92 [ + + ]: 23910 : if (!value.isObject())
93 : 1 : return Err("Not an object");
94 : 23909 : ref.set(&value.toObject());
95 : 23909 : return JS::Ok();
96 : : }
97 : :
98 : : GJS_ALWAYS_INLINE
99 : : static inline ParseArgsResult assign(JSContext* cx, char c, bool nullable,
100 : : JS::HandleValue value,
101 : : JS::UniqueChars* ref) {
102 : 40680 : if (c != 's')
103 : 1 : return Err("Wrong type for %c, got JS::UniqueChars*", c);
104 [ + + + - : 40679 : if (nullable && value.isNull()) {
+ + ]
105 : 1 : ref->reset();
106 : 1 : return JS::Ok();
107 : : }
108 : 40678 : JS::UniqueChars tmp = gjs_string_to_utf8(cx, value);
109 [ - + ]: 40678 : if (!tmp)
110 : 0 : return Err("Couldn't convert to string");
111 : 40678 : *ref = std::move(tmp);
112 : 40678 : return JS::Ok();
113 : 40678 : }
114 : :
115 : : GJS_ALWAYS_INLINE
116 : : static inline ParseArgsResult assign(JSContext* cx, char c, bool nullable,
117 : : JS::HandleValue value,
118 : : Gjs::AutoChar* ref) {
119 : 6 : if (c != 'F')
120 : 1 : return Err("Wrong type for %c, got Gjs::AutoChar*", c);
121 [ + + + - : 5 : if (nullable && value.isNull()) {
+ + ]
122 : 1 : ref->release();
123 : 1 : return JS::Ok();
124 : : }
125 [ - + ]: 4 : if (!gjs_string_to_filename(cx, value, ref))
126 : 0 : return Err("Couldn't convert to filename");
127 : 4 : return JS::Ok();
128 : : }
129 : :
130 : : GJS_ALWAYS_INLINE
131 : : static inline ParseArgsResult assign(JSContext*, char c, bool nullable,
132 : : JS::HandleValue value,
133 : : JS::MutableHandleString ref) {
134 : 30966 : if (c != 'S')
135 : 1 : return Err("Wrong type for %c, got JS::MutableHandleString", c);
136 [ + + + - : 30965 : if (nullable && value.isNull()) {
+ + ]
137 : 1 : ref.set(nullptr);
138 : 1 : return JS::Ok();
139 : : }
140 [ - + ]: 30964 : if (!value.isString())
141 : 0 : return Err("Not a string");
142 : 30964 : ref.set(value.toString());
143 : 30964 : return JS::Ok();
144 : : }
145 : :
146 : : GJS_ALWAYS_INLINE
147 : : static inline ParseArgsResult assign(JSContext* cx, char c, bool nullable,
148 : : JS::HandleValue value, int32_t* ref) {
149 : 441 : if (c != 'i')
150 : 1 : return Err("Wrong type for %c, got int32_t*", c);
151 [ - + - + : 440 : if (nullable)
+ + - + -
+ - + - +
- + ]
152 : 1 : return Err("Invalid format string combination ?i");
153 [ - + - + : 439 : if (!JS::ToInt32(cx, value, ref))
- + - + -
+ - + - +
- + ]
154 : 0 : return Err("Couldn't convert to integer");
155 : 439 : return JS::Ok();
156 : : }
157 : :
158 : : GJS_ALWAYS_INLINE
159 : : static inline ParseArgsResult assign(JSContext* cx, char c, bool nullable,
160 : : JS::HandleValue value, uint32_t* ref) {
161 : : double num;
162 : :
163 : 4 : if (c != 'u')
164 : 1 : return Err("Wrong type for %c, got uint32_t*", c);
165 [ + + ]: 3 : if (nullable)
166 : 1 : return Err("Invalid format string combination ?u");
167 [ + - - + : 2 : if (!value.isNumber() || !JS::ToNumber(cx, value, &num))
- + ]
168 : 0 : return Err("Couldn't convert to unsigned integer");
169 [ + - + + ]: 2 : if (num > INT32_MAX || num < 0)
170 : 1 : return Err("Value %f is out of range", num);
171 : 1 : *ref = num;
172 : 1 : return JS::Ok();
173 : : }
174 : :
175 : : GJS_ALWAYS_INLINE
176 : : static inline ParseArgsResult assign(JSContext* cx, char c, bool nullable,
177 : : JS::HandleValue value, int64_t* ref) {
178 : 3 : if (c != 't')
179 : 1 : return Err("Wrong type for %c, got int64_t*", c);
180 [ + + ]: 2 : if (nullable)
181 : 1 : return Err("Invalid format string combination ?t");
182 [ - + ]: 1 : if (!JS::ToInt64(cx, value, ref))
183 : 0 : return Err("Couldn't convert to 64-bit integer");
184 : 1 : return JS::Ok();
185 : : }
186 : :
187 : : GJS_ALWAYS_INLINE
188 : : static inline ParseArgsResult assign(JSContext* cx, char c, bool nullable,
189 : : JS::HandleValue value, double* ref) {
190 : 78 : if (c != 'f')
191 : 1 : return Err("Wrong type for %c, got double*", c);
192 [ + + ]: 77 : if (nullable)
193 : 1 : return Err("Invalid format string combination ?f");
194 [ - + ]: 76 : if (!JS::ToNumber(cx, value, ref))
195 : 0 : return Err("Couldn't convert to double");
196 : 76 : return JS::Ok();
197 : : }
198 : :
199 : : /* Special case: treat pointer-to-enum as pointer-to-int, but use enable_if to
200 : : * prevent instantiation for any other types besides pointer-to-enum */
201 : : template <typename T, typename std::enable_if_t<std::is_enum_v<T>, int> = 0>
202 : : GJS_ALWAYS_INLINE
203 : : static inline ParseArgsResult assign(JSContext* cx, char c, bool nullable,
204 : : JS::HandleValue value, T* ref) {
205 : : /* Sadly, we cannot use std::underlying_type<T> here; the underlying type of
206 : : * an enum is implementation-defined, so it would not be clear what letter
207 : : * to use in the format string. For the same reason, we can only support
208 : : * enum types that are the same width as int.
209 : : *
210 : : * Additionally, it would be nice to be able to check whether the resulting
211 : : * value was in range for the enum, but that is not possible (yet?) */
212 : : static_assert(sizeof(T) == sizeof(int),
213 : : "Short or wide enum types not supported");
214 : 140 : return assign(cx, c, nullable, value, reinterpret_cast<int*>(ref));
215 : : }
216 : :
217 : : template <typename T>
218 : 1 : static inline void free_if_necessary(T param_ref [[maybe_unused]]) {}
219 : :
220 : : template <typename T>
221 : : GJS_ALWAYS_INLINE
222 : : static inline void free_if_necessary(JS::Rooted<T>* param_ref) {
223 : : // This is not exactly right, since before we consumed a JS::Value there may
224 : : // have been something different inside the handle. But it has already been
225 : : // clobbered at this point anyhow.
226 : 2 : JS::MutableHandle<T>(param_ref).set(nullptr);
227 : 2 : }
228 : :
229 : : template <typename T>
230 : : GJS_JSAPI_RETURN_CONVENTION
231 : 96807 : static bool parse_call_args_helper(JSContext* cx, const char* function_name,
232 : : const JS::CallArgs& args,
233 : : const char*& fmt_required,
234 : : const char*& fmt_optional, unsigned param_ix,
235 : : const char* param_name, T param_ref) {
236 : 96807 : bool nullable = false;
237 : 96807 : const char* fchar = fmt_required;
238 : :
239 : 96807 : g_return_val_if_fail(param_name, false);
240 : :
241 [ + + ]: 96807 : if (*fchar != '\0') {
242 : 96085 : nullable = check_nullable(fchar, fmt_required);
243 : 96085 : fmt_required++;
244 : : } else {
245 : : // No more args passed in JS, only optional formats left
246 [ + + ]: 722 : if (args.length() <= param_ix)
247 : 364 : return true;
248 : :
249 : 358 : fchar = fmt_optional;
250 : 358 : g_assert(*fchar != '\0' &&
251 : : "Wrong number of parameters passed to gjs_parse_call_args()");
252 : 358 : nullable = check_nullable(fchar, fmt_optional);
253 : 358 : fmt_optional++;
254 : : }
255 : :
256 [ + + ]: 96443 : ParseArgsResult res =
257 : 96443 : assign(cx, *fchar, nullable, args[param_ix], param_ref);
258 [ + + ]: 96443 : if (res.isErr()) {
259 : : /* Our error messages are going to be more useful than whatever was
260 : : * thrown by the various conversion functions */
261 : 17 : const char* message = res.inspectErr().message();
262 : 17 : JS_ClearPendingException(cx);
263 : 17 : gjs_throw(cx, "Error invoking %s, at argument %d (%s): %s",
264 : : function_name, param_ix, param_name, message);
265 : 17 : return false;
266 : : }
267 : :
268 : 96426 : return true;
269 : 96443 : }
270 : :
271 : : template <typename T, typename... Args>
272 : : GJS_JSAPI_RETURN_CONVENTION
273 : 20173 : static bool parse_call_args_helper(JSContext* cx, const char* function_name,
274 : : const JS::CallArgs& args,
275 : : const char*& fmt_required,
276 : : const char*& fmt_optional, unsigned param_ix,
277 : : const char* param_name, T param_ref,
278 : : Args... params) {
279 [ - + ]: 20173 : if (!parse_call_args_helper(cx, function_name, args, fmt_required,
280 : : fmt_optional, param_ix, param_name, param_ref))
281 : 0 : return false;
282 : :
283 : 20173 : bool retval = parse_call_args_helper(cx, function_name, args, fmt_required,
284 : : fmt_optional, ++param_ix, params...);
285 : :
286 : : // We still own JSString/JSObject in the error case, free any we converted
287 [ + + ]: 20173 : if (!retval)
288 : 1 : free_if_necessary(param_ref);
289 : 20173 : return retval;
290 : : }
291 : :
292 : : } // namespace detail
293 : :
294 : : // Empty-args version of the template
295 : : GJS_JSAPI_RETURN_CONVENTION [[maybe_unused]]
296 : 91 : static bool gjs_parse_call_args(JSContext* cx, const char* function_name,
297 : : const JS::CallArgs& args, const char* format) {
298 : 91 : bool ignore_trailing_args = false;
299 : :
300 [ + + ]: 91 : if (*format == '!') {
301 : 1 : ignore_trailing_args = true;
302 : 1 : format++;
303 : : }
304 : :
305 : 91 : g_assert(*format == '\0' &&
306 : : "Wrong number of parameters passed to gjs_parse_call_args()");
307 : :
308 [ + + + + : 91 : if (!ignore_trailing_args && args.length() > 0) {
+ + ]
309 : 1 : gjs_throw(cx, "Error invoking %s: Expected 0 arguments, got %d",
310 : : function_name, args.length());
311 : 1 : return false;
312 : : }
313 : :
314 : 90 : return true;
315 : : }
316 : :
317 : : /**
318 : : * gjs_parse_call_args:
319 : : * @cx:
320 : : * @function_name: The name of the function being called
321 : : * @args: #JS::CallArgs from #JSNative function
322 : : * @format: Printf-like format specifier containing the expected arguments
323 : : * @params: for each character in @format, a pair of const char * which is the
324 : : * name of the argument, and a location to store the value. The type of location
325 : : * argument depends on the format character, as described below.
326 : : *
327 : : * This function is inspired by Python's PyArg_ParseTuple for those familiar
328 : : * with it. It takes a format specifier which gives the types of the expected
329 : : * arguments, and a list of argument names and value location pairs. The
330 : : * currently accepted format specifiers are:
331 : : *
332 : : * - b: A boolean (pass a bool *)
333 : : * - s: A string, converted into UTF-8 (pass a JS::UniqueChars*)
334 : : * - F: A string, converted into "filename encoding" (i.e. active locale) (pass
335 : : * a Gjs::AutoChar *)
336 : : * - S: A string, no conversion (pass a JS::MutableHandleString)
337 : : * - i: A number, will be converted to a 32-bit int (pass an int32_t * or a
338 : : * pointer to an enum type)
339 : : * - u: A number, converted into a 32-bit unsigned int (pass a uint32_t *)
340 : : * - t: A 64-bit number, converted into a 64-bit int (pass an int64_t *)
341 : : * - f: A number, will be converted into a double (pass a double *)
342 : : * - o: A JavaScript object (pass a JS::MutableHandleObject)
343 : : *
344 : : * If the first character in the format string is a '!', then JS is allowed to
345 : : * pass extra arguments that are ignored, to the function.
346 : : *
347 : : * The '|' character introduces optional arguments. All format specifiers after
348 : : * a '|' when not specified, do not cause any changes in the C value location.
349 : : *
350 : : * A prefix character '?' in front of 's', 'F', 'S', or 'o' means that the next
351 : : * value may be null. For 's' or 'F' a null pointer is returned, for 'S' or 'o'
352 : : * the handle is set to null.
353 : : */
354 : : template <typename... Args>
355 : : GJS_JSAPI_RETURN_CONVENTION
356 : 76642 : static bool gjs_parse_call_args(JSContext* cx, const char* function_name,
357 : : const JS::CallArgs& args, const char* format,
358 : : Args... params) {
359 : 76642 : unsigned n_required = 0, n_total = 0;
360 : 76642 : bool optional_args = false, ignore_trailing_args = false;
361 : :
362 [ + + ]: 76642 : if (*format == '!') {
363 : 2312 : ignore_trailing_args = true;
364 : 2312 : format++;
365 : : }
366 : :
367 [ + + ]: 174199 : for (const char* fmt_iter = format; *fmt_iter; fmt_iter++) {
368 [ + + + ]: 97557 : switch (*fmt_iter) {
369 : 719 : case '|':
370 : 719 : n_required = n_total;
371 : 719 : optional_args = true;
372 : 719 : continue;
373 : 9 : case '?':
374 : 9 : continue;
375 : 96829 : default:
376 : 96829 : n_total++;
377 : : }
378 : : }
379 : :
380 [ + + ]: 76642 : if (!optional_args)
381 : 75923 : n_required = n_total;
382 : :
383 : 76642 : g_assert(sizeof...(Args) / 2 == n_total &&
384 : : "Wrong number of parameters passed to gjs_parse_call_args()");
385 : :
386 [ + + ]: 76642 : if (!args.requireAtLeast(cx, function_name, n_required))
387 : 6 : return false;
388 [ + + + + : 76636 : if (!ignore_trailing_args && args.length() > n_total) {
+ + ]
389 [ + + ]: 2 : if (n_required == n_total) {
390 : 1 : gjs_throw(cx, "Error invoking %s: Expected %d arguments, got %d",
391 : : function_name, n_required, args.length());
392 : : } else {
393 : 1 : gjs_throw(cx,
394 : : "Error invoking %s: Expected minimum %d arguments (and "
395 : : "%d optional), got %d",
396 : : function_name, n_required, n_total - n_required,
397 : : args.length());
398 : : }
399 : 2 : return false;
400 : : }
401 : :
402 : 76634 : Gjs::AutoStrv parts{g_strsplit(format, "|", 2)};
403 : 76634 : const char* fmt_required = parts.get()[0];
404 : 76634 : const char* fmt_optional = parts.get()[1]; // may be null
405 : :
406 : 76634 : return detail::parse_call_args_helper(cx, function_name, args, fmt_required,
407 : 76634 : fmt_optional, 0, params...);
408 : 76634 : }
|