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