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