@gjsify/node-gi 0.13.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/variant.cc ADDED
@@ -0,0 +1,587 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // GVariant build / unpack (the GLib.Variant ergonomics).
3
+
4
+ #include "common.h"
5
+
6
+ namespace nodegi {
7
+
8
+ // ====================================================================
9
+ // ---- GVariant build / unpack (the GLib.Variant ergonomics) ---------
10
+ // ====================================================================
11
+ //
12
+ // The native half of GJS's `GLib.Variant` override (gjs/modules/core/overrides/
13
+ // GLib.js): a recursive packer `new GLib.Variant(signature, value)` and an
14
+ // unpacker driving `.unpack()` / `.deepUnpack()` / `.recursiveUnpack()`. The L1
15
+ // layer (gi.js) wires the GJS-shaped surface on top of these primitives.
16
+ //
17
+ // SCOPE — the common signatures real GAction/GSettings/DBus code hits: the
18
+ // basics `b y n q i u x t h d s o g`, `v` (variant), `m*` (maybe), `a*` arrays
19
+ // (with the `as` strv + `ay` bytestring fast-paths and `a{..}` dictionaries via
20
+ // dict-entries), `(...)` tuples, and `{kv}` dict-entries. Genuinely exotic
21
+ // element types fall out of the type grammar with a clear "Invalid GVariant
22
+ // signature" error.
23
+ //
24
+ // OWNERSHIP (the leak/UAF surface — scrutinise here): every `g_variant_new_*`
25
+ // and `g_variant_builder_end` returns a FLOATING ref. A child handed to
26
+ // g_variant_builder_add_value / g_variant_new_maybe / g_variant_new_dict_entry
27
+ // is CONSUMED (its floating ref is taken), so the success path never leaks. The
28
+ // single top-level result is g_variant_ref_sink'd exactly once and owned by the
29
+ // boxed handle (g_variant_unref on GC). On the UNPACK side g_variant_get_child_
30
+ // value / get_variant / get_maybe each return a NEW ref (transfer full): a child
31
+ // that "stays a Variant" hands that ref straight to the boxed handle (no extra
32
+ // ref), and a child we recurse into is unref'd after the read.
33
+
34
+ static bool VariantIsSimpleType(char c) {
35
+ switch (c) {
36
+ case 'b': case 'y': case 'n': case 'q': case 'i': case 'u':
37
+ case 'x': case 't': case 'h': case 'd': case 's': case 'o': case 'g':
38
+ return true;
39
+ default:
40
+ return false;
41
+ }
42
+ }
43
+
44
+ // Consume one complete type from `sig` starting at *pos, advancing *pos past it,
45
+ // and return that type's string (mirrors GJS `_readSingleType`). `forceSimple`
46
+ // rejects a non-basic type (dict keys must be basic). Throws + sets *ok=false on
47
+ // a malformed signature.
48
+ static std::string VariantReadSingleType(Napi::Env env, const std::string& sig, size_t* pos,
49
+ bool forceSimple, bool* ok) {
50
+ if (*pos >= sig.size()) {
51
+ Napi::TypeError::New(env, "Invalid GVariant signature (reached end while expecting a type)")
52
+ .ThrowAsJavaScriptException();
53
+ *ok = false;
54
+ return "";
55
+ }
56
+ char c = sig[(*pos)++];
57
+ bool simple = VariantIsSimpleType(c);
58
+ if (!simple && forceSimple) {
59
+ Napi::TypeError::New(env, "Invalid GVariant signature (a simple type was expected)")
60
+ .ThrowAsJavaScriptException();
61
+ *ok = false;
62
+ return "";
63
+ }
64
+ if (c == 'm' || c == 'a') {
65
+ std::string inner = VariantReadSingleType(env, sig, pos, false, ok);
66
+ if (!*ok) return "";
67
+ return std::string(1, c) + inner;
68
+ }
69
+ if (c == '{') {
70
+ std::string key = VariantReadSingleType(env, sig, pos, true, ok);
71
+ if (!*ok) return "";
72
+ std::string val = VariantReadSingleType(env, sig, pos, false, ok);
73
+ if (!*ok) return "";
74
+ if (*pos >= sig.size() || sig[*pos] != '}') {
75
+ Napi::TypeError::New(env, "Invalid GVariant signature for type DICT_ENTRY (expected \"}\")")
76
+ .ThrowAsJavaScriptException();
77
+ *ok = false;
78
+ return "";
79
+ }
80
+ (*pos)++;
81
+ return std::string("{") + key + val + "}";
82
+ }
83
+ if (c == '(') {
84
+ std::string res = "(";
85
+ while (true) {
86
+ if (*pos >= sig.size()) {
87
+ Napi::TypeError::New(env, "Invalid GVariant signature for type TUPLE (expected \")\")")
88
+ .ThrowAsJavaScriptException();
89
+ *ok = false;
90
+ return "";
91
+ }
92
+ if (sig[*pos] == ')') {
93
+ (*pos)++;
94
+ res += ")";
95
+ return res;
96
+ }
97
+ std::string el = VariantReadSingleType(env, sig, pos, false, ok);
98
+ if (!*ok) return "";
99
+ res += el;
100
+ }
101
+ }
102
+ if (!simple && c != 'v') {
103
+ Napi::TypeError::New(env, std::string("Invalid GVariant signature (") + c + " is not a valid type)")
104
+ .ThrowAsJavaScriptException();
105
+ *ok = false;
106
+ return "";
107
+ }
108
+ return std::string(1, c);
109
+ }
110
+
111
+ // Read a JS value into a byte vector for an `ay` array (Uint8Array/Buffer/
112
+ // number[]; a string is UTF-8 encoded with a trailing NUL, matching GJS).
113
+ // null/undefined pack as the EMPTY byte array: GJS routes 'ay' through
114
+ // `new GLib.Bytes(value)` (modules/core/overrides/GLib.js _packVariant), and
115
+ // GLib.Bytes(null) is g_bytes_new(NULL, 0) — `new GLib.Variant('ay', null)`
116
+ // prints `@ay []` under gjs. Real consumers rely on it: `@gjsify/webgl` passes
117
+ // Uint8ArrayToVariant(null) for WebGL calls that allocate without data
118
+ // (texImage2D(..., null) — Excalibur's blank-texture allocation at renderer
119
+ // init was the exposing call on node-gi).
120
+ static bool VariantExtractBytes(Napi::Env env, Napi::Value v, std::vector<guint8>* out) {
121
+ if (v.IsNull() || v.IsUndefined()) {
122
+ out->clear();
123
+ return true;
124
+ }
125
+ if (v.IsBuffer()) {
126
+ Napi::Buffer<uint8_t> b = v.As<Napi::Buffer<uint8_t>>();
127
+ out->assign(b.Data(), b.Data() + b.Length());
128
+ return true;
129
+ }
130
+ if (v.IsTypedArray()) {
131
+ Napi::TypedArray ta = v.As<Napi::TypedArray>();
132
+ const uint8_t* base = static_cast<uint8_t*>(ta.ArrayBuffer().Data()) + ta.ByteOffset();
133
+ out->assign(base, base + ta.ByteLength());
134
+ return true;
135
+ }
136
+ if (v.IsArray()) {
137
+ Napi::Array a = v.As<Napi::Array>();
138
+ uint32_t n = a.Length();
139
+ out->resize(n);
140
+ for (uint32_t i = 0; i < n; i++)
141
+ (*out)[i] = static_cast<guint8>(a.Get(i).ToNumber().Uint32Value());
142
+ return true;
143
+ }
144
+ if (v.IsString()) {
145
+ std::string s = v.As<Napi::String>().Utf8Value();
146
+ out->assign(s.begin(), s.end());
147
+ out->push_back(0);
148
+ return true;
149
+ }
150
+ Napi::TypeError::New(env, "GVariant 'ay' expects a Uint8Array, Buffer, number[] or string")
151
+ .ThrowAsJavaScriptException();
152
+ return false;
153
+ }
154
+
155
+ // Recursively build a (floating) GVariant from `sig` (starting at *pos) + `value`
156
+ // — the C twin of GJS `_packVariant`. Returns a FLOATING ref (or nullptr with a
157
+ // pending exception + *ok=false). The caller consumes the float.
158
+ static GVariant* VariantPack(Napi::Env env, const std::string& sig, size_t* pos,
159
+ Napi::Value value, bool* ok) {
160
+ if (*pos >= sig.size()) {
161
+ Napi::TypeError::New(env, "GVariant signature cannot be empty").ThrowAsJavaScriptException();
162
+ *ok = false;
163
+ return nullptr;
164
+ }
165
+ char c = sig[(*pos)++];
166
+ switch (c) {
167
+ case 'b': return g_variant_new_boolean(value.ToBoolean().Value());
168
+ case 'y': return g_variant_new_byte(static_cast<guint8>(value.ToNumber().Uint32Value()));
169
+ case 'n': return g_variant_new_int16(static_cast<gint16>(value.ToNumber().Int32Value()));
170
+ case 'q': return g_variant_new_uint16(static_cast<guint16>(value.ToNumber().Uint32Value()));
171
+ case 'i': return g_variant_new_int32(value.ToNumber().Int32Value());
172
+ case 'u': return g_variant_new_uint32(value.ToNumber().Uint32Value());
173
+ // GJS accepts a BigInt for 64-bit types (js_value_to_c<int64_t> branches on
174
+ // isBigInt before ToInt64); a BigInt must never reach ToNumber() (fatal abort).
175
+ // Shared with the GI/GValue marshallers via JsValueTo{Int,Uint}64 (common.h).
176
+ case 'x': return g_variant_new_int64(JsValueToInt64(value));
177
+ case 't': return g_variant_new_uint64(JsValueToUint64(value));
178
+ case 'h': return g_variant_new_handle(value.ToNumber().Int32Value());
179
+ case 'd': return g_variant_new_double(value.ToNumber().DoubleValue());
180
+ case 's': {
181
+ // GJS marshals 's' via new_string, whose UTF8 arg is type-strict (isString
182
+ // or null), so `new GLib.Variant('s', 42)` throws rather than packing "42".
183
+ if (!value.IsString()) {
184
+ Napi::TypeError::New(env, "GVariant 's' expects a string")
185
+ .ThrowAsJavaScriptException();
186
+ *ok = false;
187
+ return nullptr;
188
+ }
189
+ std::string s = value.ToString().Utf8Value();
190
+ return g_variant_new_string(s.c_str());
191
+ }
192
+ case 'o': {
193
+ if (!value.IsString()) {
194
+ Napi::TypeError::New(env, "GVariant 'o' expects an object-path string")
195
+ .ThrowAsJavaScriptException();
196
+ *ok = false;
197
+ return nullptr;
198
+ }
199
+ std::string s = value.ToString().Utf8Value();
200
+ if (!g_variant_is_object_path(s.c_str())) {
201
+ Napi::TypeError::New(env, std::string("Invalid GVariant object path: ") + s)
202
+ .ThrowAsJavaScriptException();
203
+ *ok = false;
204
+ return nullptr;
205
+ }
206
+ return g_variant_new_object_path(s.c_str());
207
+ }
208
+ case 'g': {
209
+ if (!value.IsString()) {
210
+ Napi::TypeError::New(env, "GVariant 'g' expects a signature string")
211
+ .ThrowAsJavaScriptException();
212
+ *ok = false;
213
+ return nullptr;
214
+ }
215
+ std::string s = value.ToString().Utf8Value();
216
+ if (!g_variant_is_signature(s.c_str())) {
217
+ Napi::TypeError::New(env, std::string("Invalid GVariant signature string: ") + s)
218
+ .ThrowAsJavaScriptException();
219
+ *ok = false;
220
+ return nullptr;
221
+ }
222
+ return g_variant_new_signature(s.c_str());
223
+ }
224
+ case 'v': {
225
+ gpointer p = nullptr;
226
+ if (!TryGetBoxedPtr(value, &p) || p == nullptr) {
227
+ Napi::TypeError::New(env, "GVariant 'v' expects a GLib.Variant value")
228
+ .ThrowAsJavaScriptException();
229
+ *ok = false;
230
+ return nullptr;
231
+ }
232
+ return g_variant_new_variant(static_cast<GVariant*>(p));
233
+ }
234
+ case 'm': {
235
+ if (!value.IsNull() && !value.IsUndefined()) {
236
+ GVariant* child = VariantPack(env, sig, pos, value, ok);
237
+ if (!*ok) return nullptr;
238
+ return g_variant_new_maybe(nullptr, child); // consumes the floating child
239
+ }
240
+ std::string elem = VariantReadSingleType(env, sig, pos, false, ok);
241
+ if (!*ok) return nullptr;
242
+ GVariantType* t = g_variant_type_new(elem.c_str());
243
+ GVariant* r = g_variant_new_maybe(t, nullptr);
244
+ g_variant_type_free(t);
245
+ return r;
246
+ }
247
+ case 'a': {
248
+ std::string elemType = VariantReadSingleType(env, sig, pos, false, ok);
249
+ if (!*ok) return nullptr;
250
+ char e0 = elemType.empty() ? '\0' : elemType[0];
251
+ if (e0 == 's') { // array of strings → g_variant_new_strv
252
+ if (!value.IsArray()) {
253
+ Napi::TypeError::New(env, "GVariant 'as' expects an array of strings")
254
+ .ThrowAsJavaScriptException();
255
+ *ok = false;
256
+ return nullptr;
257
+ }
258
+ Napi::Array a = value.As<Napi::Array>();
259
+ uint32_t n = a.Length();
260
+ std::vector<std::string> store(n);
261
+ std::vector<const gchar*> ptrs(n + 1, nullptr);
262
+ for (uint32_t i = 0; i < n; i++) {
263
+ store[i] = a.Get(i).ToString().Utf8Value();
264
+ ptrs[i] = store[i].c_str();
265
+ }
266
+ return g_variant_new_strv(ptrs.data(), static_cast<gssize>(n)); // copies
267
+ }
268
+ if (e0 == 'y') { // byte array
269
+ std::vector<guint8> bytes;
270
+ if (!VariantExtractBytes(env, value, &bytes)) {
271
+ *ok = false;
272
+ return nullptr;
273
+ }
274
+ return g_variant_new_fixed_array(G_VARIANT_TYPE_BYTE, bytes.data(), bytes.size(),
275
+ sizeof(guint8)); // copies
276
+ }
277
+ std::string fullType = "a" + elemType;
278
+ GVariantType* at = g_variant_type_new(fullType.c_str());
279
+ GVariantBuilder builder;
280
+ g_variant_builder_init(&builder, at);
281
+ if (e0 == '{') { // dictionary: a plain object keyed by the entry's key type
282
+ if (!value.IsObject()) {
283
+ g_variant_builder_clear(&builder);
284
+ g_variant_type_free(at);
285
+ Napi::TypeError::New(env, "GVariant dictionary expects a plain object")
286
+ .ThrowAsJavaScriptException();
287
+ *ok = false;
288
+ return nullptr;
289
+ }
290
+ Napi::Object obj = value.As<Napi::Object>();
291
+ Napi::Array keys = obj.GetPropertyNames();
292
+ uint32_t n = keys.Length();
293
+ for (uint32_t i = 0; i < n; i++) {
294
+ Napi::Value k = keys.Get(i);
295
+ Napi::Array pair = Napi::Array::New(env, 2);
296
+ pair.Set(static_cast<uint32_t>(0), k);
297
+ pair.Set(static_cast<uint32_t>(1), obj.Get(k));
298
+ size_t p2 = 0;
299
+ GVariant* child = VariantPack(env, elemType, &p2, pair, ok); // packs the {kv} entry
300
+ if (!*ok) {
301
+ g_variant_builder_clear(&builder);
302
+ g_variant_type_free(at);
303
+ return nullptr;
304
+ }
305
+ g_variant_builder_add_value(&builder, child); // consumes the floating entry
306
+ }
307
+ } else { // generic array: re-parse elemType per element
308
+ if (!value.IsArray()) {
309
+ g_variant_builder_clear(&builder);
310
+ g_variant_type_free(at);
311
+ Napi::TypeError::New(env, "GVariant array expects an array value")
312
+ .ThrowAsJavaScriptException();
313
+ *ok = false;
314
+ return nullptr;
315
+ }
316
+ Napi::Array a = value.As<Napi::Array>();
317
+ uint32_t n = a.Length();
318
+ for (uint32_t i = 0; i < n; i++) {
319
+ size_t p2 = 0;
320
+ GVariant* child = VariantPack(env, elemType, &p2, a.Get(i), ok);
321
+ if (!*ok) {
322
+ g_variant_builder_clear(&builder);
323
+ g_variant_type_free(at);
324
+ return nullptr;
325
+ }
326
+ g_variant_builder_add_value(&builder, child);
327
+ }
328
+ }
329
+ GVariant* r = g_variant_builder_end(&builder);
330
+ g_variant_type_free(at);
331
+ return r;
332
+ }
333
+ case '(': { // tuple: drive children off the signature until ')'
334
+ if (!value.IsArray()) {
335
+ Napi::TypeError::New(env, "GVariant tuple expects an array value")
336
+ .ThrowAsJavaScriptException();
337
+ *ok = false;
338
+ return nullptr;
339
+ }
340
+ Napi::Array a = value.As<Napi::Array>();
341
+ uint32_t n = a.Length();
342
+ GVariantBuilder builder;
343
+ g_variant_builder_init(&builder, G_VARIANT_TYPE_TUPLE);
344
+ // Drive children off the JS array length (GJS: `for i < value.length`), not
345
+ // the signature — a missing position must NOT coerce undefined→garbage.
346
+ for (uint32_t i = 0; i < n; i++) {
347
+ if (*pos < sig.size() && sig[*pos] == ')') break; // more values than types: stop
348
+ GVariant* child = VariantPack(env, sig, pos, a.Get(i), ok);
349
+ if (!*ok) {
350
+ g_variant_builder_clear(&builder);
351
+ return nullptr;
352
+ }
353
+ g_variant_builder_add_value(&builder, child);
354
+ }
355
+ // Every value consumed: the signature must now close the tuple. If types
356
+ // remain (too few values) this throws, exactly as GJS does.
357
+ if (*pos >= sig.size() || sig[*pos] != ')') {
358
+ g_variant_builder_clear(&builder);
359
+ Napi::TypeError::New(env, "Invalid GVariant signature for type TUPLE (expected \")\")")
360
+ .ThrowAsJavaScriptException();
361
+ *ok = false;
362
+ return nullptr;
363
+ }
364
+ (*pos)++; // consume ')'
365
+ return g_variant_builder_end(&builder);
366
+ }
367
+ case '{': { // dict-entry: value is [key, val]
368
+ if (!value.IsArray()) {
369
+ Napi::TypeError::New(env, "GVariant dict-entry expects a [key, value] array")
370
+ .ThrowAsJavaScriptException();
371
+ *ok = false;
372
+ return nullptr;
373
+ }
374
+ Napi::Array a = value.As<Napi::Array>();
375
+ GVariant* key = VariantPack(env, sig, pos, a.Get(static_cast<uint32_t>(0)), ok);
376
+ if (!*ok) return nullptr;
377
+ GVariant* val = VariantPack(env, sig, pos, a.Get(static_cast<uint32_t>(1)), ok);
378
+ if (!*ok) {
379
+ g_variant_unref(g_variant_take_ref(key));
380
+ return nullptr;
381
+ }
382
+ if (*pos >= sig.size() || sig[*pos] != '}') {
383
+ g_variant_unref(g_variant_take_ref(key));
384
+ g_variant_unref(g_variant_take_ref(val));
385
+ Napi::TypeError::New(env, "Invalid GVariant signature for type DICT_ENTRY (expected \"}\")")
386
+ .ThrowAsJavaScriptException();
387
+ *ok = false;
388
+ return nullptr;
389
+ }
390
+ (*pos)++;
391
+ return g_variant_new_dict_entry(key, val); // consumes both floating children
392
+ }
393
+ default:
394
+ Napi::TypeError::New(
395
+ env, std::string("Invalid GVariant signature (unexpected character ") + c + ")")
396
+ .ThrowAsJavaScriptException();
397
+ *ok = false;
398
+ return nullptr;
399
+ }
400
+ }
401
+
402
+ static Napi::Value VariantToJs(Napi::Env env, GVariant* v, bool deep, bool recursive);
403
+
404
+ // Shared array/tuple/dict-entry reader: each child either fully unpacks (deep) or
405
+ // stays a Variant handle (shallow). The shallow handle adopts the new ref from
406
+ // g_variant_get_child_value; the deep path unrefs after the recursive read.
407
+ static Napi::Array VariantContainerToArray(Napi::Env env, GVariant* v, bool deep, bool recursive) {
408
+ gsize n = g_variant_n_children(v);
409
+ Napi::Array out = Napi::Array::New(env, n);
410
+ for (gsize i = 0; i < n; i++) {
411
+ GVariant* child = g_variant_get_child_value(v, i); // transfer full
412
+ if (deep) {
413
+ Napi::Value cj = VariantToJs(env, child, deep, recursive);
414
+ g_variant_unref(child);
415
+ out.Set(static_cast<uint32_t>(i), cj);
416
+ } else {
417
+ out.Set(static_cast<uint32_t>(i), MakeBoxedHandle(env, child, G_TYPE_VARIANT, true));
418
+ }
419
+ }
420
+ return out;
421
+ }
422
+
423
+ // The C twin of GJS `_unpackVariant(variant, deep, recursive)`. `deep` controls
424
+ // whether container children are unpacked at all; `recursive` ONLY affects `v`
425
+ // (variant) values — deep keeps a nested `v` as a Variant, recursive unwraps it.
426
+ // This is the exact distinction behind unpack() / deepUnpack() / recursiveUnpack.
427
+ static Napi::Value VariantToJs(Napi::Env env, GVariant* v, bool deep, bool recursive) {
428
+ switch (g_variant_classify(v)) {
429
+ case G_VARIANT_CLASS_BOOLEAN: return Napi::Boolean::New(env, g_variant_get_boolean(v));
430
+ case G_VARIANT_CLASS_BYTE: return Napi::Number::New(env, g_variant_get_byte(v));
431
+ case G_VARIANT_CLASS_INT16: return Napi::Number::New(env, g_variant_get_int16(v));
432
+ case G_VARIANT_CLASS_UINT16: return Napi::Number::New(env, g_variant_get_uint16(v));
433
+ case G_VARIANT_CLASS_INT32: return Napi::Number::New(env, g_variant_get_int32(v));
434
+ case G_VARIANT_CLASS_UINT32: return Napi::Number::New(env, g_variant_get_uint32(v));
435
+ // GJS unpacks a 64-bit variant to a Number, warning when it can't be stored
436
+ // exactly (|v| > 2^53-1) — same as every other 64-bit OUT path.
437
+ case G_VARIANT_CLASS_INT64: {
438
+ gint64 x = g_variant_get_int64(v);
439
+ WarnIfUnsafeInt64(x);
440
+ return Napi::Number::New(env, static_cast<double>(x));
441
+ }
442
+ case G_VARIANT_CLASS_UINT64: {
443
+ guint64 x = g_variant_get_uint64(v);
444
+ WarnIfUnsafeUint64(x);
445
+ return Napi::Number::New(env, static_cast<double>(x));
446
+ }
447
+ case G_VARIANT_CLASS_HANDLE: return Napi::Number::New(env, g_variant_get_handle(v));
448
+ case G_VARIANT_CLASS_DOUBLE: return Napi::Number::New(env, g_variant_get_double(v));
449
+ case G_VARIANT_CLASS_STRING:
450
+ case G_VARIANT_CLASS_OBJECT_PATH:
451
+ case G_VARIANT_CLASS_SIGNATURE:
452
+ return Napi::String::New(env, g_variant_get_string(v, nullptr));
453
+ case G_VARIANT_CLASS_VARIANT: {
454
+ GVariant* inner = g_variant_get_variant(v); // transfer full
455
+ if (deep && recursive) {
456
+ Napi::Value r = VariantToJs(env, inner, deep, recursive);
457
+ g_variant_unref(inner);
458
+ return r;
459
+ }
460
+ return MakeBoxedHandle(env, inner, G_TYPE_VARIANT, true); // stays a Variant
461
+ }
462
+ case G_VARIANT_CLASS_MAYBE: {
463
+ GVariant* inner = g_variant_get_maybe(v); // transfer full, or NULL
464
+ if (inner == nullptr) return env.Null();
465
+ if (deep) {
466
+ Napi::Value r = VariantToJs(env, inner, deep, recursive);
467
+ g_variant_unref(inner);
468
+ return r;
469
+ }
470
+ return MakeBoxedHandle(env, inner, G_TYPE_VARIANT, true);
471
+ }
472
+ case G_VARIANT_CLASS_ARRAY: {
473
+ if (g_variant_is_of_type(v, G_VARIANT_TYPE_DICTIONARY)) { // a{?*}
474
+ Napi::Object out = Napi::Object::New(env);
475
+ gsize n = g_variant_n_children(v);
476
+ for (gsize i = 0; i < n; i++) {
477
+ GVariant* entry = g_variant_get_child_value(v, i);
478
+ GVariant* keyv = g_variant_get_child_value(entry, 0);
479
+ GVariant* valv = g_variant_get_child_value(entry, 1);
480
+ // The key is always fully unpacked (it must be usable as an object
481
+ // key); the value follows the deep/recursive rule.
482
+ Napi::Value keyJs = VariantToJs(env, keyv, true, recursive);
483
+ Napi::Value valJs = deep
484
+ ? VariantToJs(env, valv, deep, recursive)
485
+ : MakeBoxedHandle(env, g_variant_ref(valv), G_TYPE_VARIANT, true);
486
+ out.Set(keyJs, valJs);
487
+ g_variant_unref(keyv);
488
+ g_variant_unref(valv);
489
+ g_variant_unref(entry);
490
+ }
491
+ return out;
492
+ }
493
+ if (g_variant_is_of_type(v, G_VARIANT_TYPE_BYTESTRING)) { // ay
494
+ gsize n = 0;
495
+ const void* data = g_variant_get_fixed_array(v, &n, sizeof(guint8));
496
+ Napi::Uint8Array arr = Napi::Uint8Array::New(env, n);
497
+ if (n > 0 && data != nullptr) memcpy(arr.Data(), data, n);
498
+ return arr;
499
+ }
500
+ return VariantContainerToArray(env, v, deep, recursive);
501
+ }
502
+ case G_VARIANT_CLASS_TUPLE:
503
+ case G_VARIANT_CLASS_DICT_ENTRY:
504
+ return VariantContainerToArray(env, v, deep, recursive);
505
+ default:
506
+ Napi::Error::New(env, "Unsupported GVariant type in unpack").ThrowAsJavaScriptException();
507
+ return env.Undefined();
508
+ }
509
+ }
510
+
511
+ // Read a GVariant pointer from a node-gi GLib.Variant handle (a boxed handle
512
+ // tagged with G_TYPE_VARIANT). Throws + returns nullptr otherwise.
513
+ static GVariant* UnwrapVariant(Napi::Env env, Napi::Value handle) {
514
+ if (!handle.IsExternal() ||
515
+ !handle.As<Napi::External<BoxedHandle>>().CheckTypeTag(&kBoxedHandleTag)) {
516
+ Napi::TypeError::New(env, "expected a node-gi GLib.Variant handle").ThrowAsJavaScriptException();
517
+ return nullptr;
518
+ }
519
+ BoxedHandle* h = handle.As<Napi::External<BoxedHandle>>().Data();
520
+ if (h == nullptr || h->ptr == nullptr || h->gtype != G_TYPE_VARIANT) {
521
+ Napi::TypeError::New(env, "expected a node-gi GLib.Variant handle").ThrowAsJavaScriptException();
522
+ return nullptr;
523
+ }
524
+ return static_cast<GVariant*>(h->ptr);
525
+ }
526
+
527
+ // variantNew(signature, value?) -> boxed GLib.Variant handle
528
+ Napi::Value VariantNew(const Napi::CallbackInfo& info) {
529
+ Napi::Env env = info.Env();
530
+ if (info.Length() < 1 || !info[0].IsString()) {
531
+ Napi::TypeError::New(env, "variantNew(signature: string, value?: unknown)")
532
+ .ThrowAsJavaScriptException();
533
+ return env.Null();
534
+ }
535
+ std::string sig = info[0].As<Napi::String>().Utf8Value();
536
+ if (sig.empty()) {
537
+ Napi::TypeError::New(env, "GVariant signature cannot be empty").ThrowAsJavaScriptException();
538
+ return env.Null();
539
+ }
540
+ Napi::Value value = info.Length() >= 2 ? info[1] : env.Undefined();
541
+ size_t pos = 0;
542
+ bool ok = true;
543
+ GVariant* v = VariantPack(env, sig, &pos, value, &ok);
544
+ if (!ok) return env.Null(); // exception already pending
545
+ if (pos != sig.size()) {
546
+ if (v != nullptr) g_variant_unref(g_variant_take_ref(v));
547
+ Napi::TypeError::New(env, "Invalid GVariant signature (more than one single complete type)")
548
+ .ThrowAsJavaScriptException();
549
+ return env.Null();
550
+ }
551
+ g_variant_ref_sink(v); // sink the floating ref → own exactly one
552
+ return MakeBoxedHandle(env, v, G_TYPE_VARIANT, true);
553
+ }
554
+
555
+ // variantUnpack(handle, deep?, recursive?) -> unknown
556
+ Napi::Value VariantUnpack(const Napi::CallbackInfo& info) {
557
+ Napi::Env env = info.Env();
558
+ GVariant* v = UnwrapVariant(env, info.Length() >= 1 ? info[0] : env.Undefined());
559
+ if (v == nullptr) return env.Null();
560
+ bool deep = info.Length() >= 2 && info[1].ToBoolean().Value();
561
+ bool recursive = info.Length() >= 3 && info[2].ToBoolean().Value();
562
+ return VariantToJs(env, v, deep, recursive);
563
+ }
564
+
565
+ // variantGetTypeString(handle) -> string
566
+ Napi::Value VariantGetTypeString(const Napi::CallbackInfo& info) {
567
+ Napi::Env env = info.Env();
568
+ GVariant* v = UnwrapVariant(env, info.Length() >= 1 ? info[0] : env.Undefined());
569
+ if (v == nullptr) return env.Null();
570
+ return Napi::String::New(env, g_variant_get_type_string(v));
571
+ }
572
+
573
+ // isVariantHandle(value) -> boolean (a boxed handle tagged with G_TYPE_VARIANT)
574
+ Napi::Value IsVariantHandle(const Napi::CallbackInfo& info) {
575
+ Napi::Env env = info.Env();
576
+ bool is = false;
577
+ if (info.Length() >= 1 && info[0].IsExternal()) {
578
+ Napi::External<BoxedHandle> ext = info[0].As<Napi::External<BoxedHandle>>();
579
+ if (ext.CheckTypeTag(&kBoxedHandleTag)) {
580
+ BoxedHandle* h = ext.Data();
581
+ is = h != nullptr && h->gtype == G_TYPE_VARIANT;
582
+ }
583
+ }
584
+ return Napi::Boolean::New(env, is);
585
+ }
586
+
587
+ } // namespace nodegi
package/system.d.ts ADDED
@@ -0,0 +1,49 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // @gjsify/node-gi/system — types for the GJS `System` module on Node.
3
+
4
+ /** Exit the process (GJS `System.exit`). */
5
+ export function exit(code?: number): void;
6
+ /** Trigger a garbage collection if the host exposed `globalThis.gc`. */
7
+ export function gc(): void;
8
+ /** The SpiderMonkey version number — reported as 0 on Node. */
9
+ export const version: number;
10
+ /** The script arguments (GJS ARGV) — `process.argv.slice(2)`. */
11
+ export const programArgs: string[];
12
+ /** The running script's invocation name — `process.argv[1]`. */
13
+ export const programInvocationName: string;
14
+ /** The running script's path — `process.argv[1]` (or null). */
15
+ export const programPath: string | null;
16
+ /** Return the address of a JS object as a string (Node stub). */
17
+ export function addressOf(): string;
18
+ /** Return the address of a GObject as a string (Node stub). */
19
+ export function addressOfGObject(): string;
20
+ /** Return the refcount of a GObject (Node stub). */
21
+ export function refcount(): number;
22
+ /** Trigger a debugger breakpoint (no-op on Node). */
23
+ export function breakpoint(): void;
24
+ /** Clear the Date timezone caches (no-op on Node). */
25
+ export function clearDateCaches(): void;
26
+ /** Dump the JS heap to a file (no-op on Node). */
27
+ export function dumpHeap(): void;
28
+ /** Dump memory info to a file (no-op on Node). */
29
+ export function dumpMemoryInfo(): void;
30
+
31
+ /** The GJS `System` module object (`import System from 'system'`). */
32
+ export interface SystemModule {
33
+ exit(code?: number): void;
34
+ gc(): void;
35
+ version: number;
36
+ readonly programArgs: string[];
37
+ readonly programInvocationName: string;
38
+ readonly programPath: string | null;
39
+ addressOf(): string;
40
+ addressOfGObject(): string;
41
+ refcount(): number;
42
+ breakpoint(): void;
43
+ clearDateCaches(): void;
44
+ dumpHeap(): void;
45
+ dumpMemoryInfo(): void;
46
+ }
47
+
48
+ declare const System: SystemModule;
49
+ export default System;