@gjsify/node-gi 0.50.0 → 0.51.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/binding.gyp +6 -1
- package/gettext.d.ts +18 -18
- package/gettext.js +141 -54
- package/gi.js +94 -29
- package/globals.d.ts +1 -1
- package/gtk-runtime.js +22 -0
- package/index.js +20 -0
- package/overrides/package.js +6 -6
- package/package.json +14 -13
- package/prebuilds/darwin-arm64/node_gi.node +0 -0
- package/prebuilds/darwin-x64/node_gi.node +0 -0
- package/prebuilds/linux-arm64/node_gi.node +0 -0
- package/prebuilds/linux-x64/node_gi.node +0 -0
- package/prebuilds/win32-x64/node_gi.node +0 -0
- package/scripts/mac-intl-gyp-flags.mjs +70 -0
- package/scripts/win-gi-gyp-flags.mjs +22 -4
- package/src/addon.cc +17 -0
- package/src/calls.cc +57 -0
- package/src/class.cc +135 -0
- package/src/common.h +35 -0
- package/src/private.cc +229 -0
- package/src/repo.cc +1 -0
- package/src/toggle.cc +17 -0
package/src/private.cc
CHANGED
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
// node-gi mirrors that architecture 1:1 so the JS-visible contract matches gjs
|
|
19
19
|
// byte-for-byte (verified: scratch gold runs vs gjs 1.88 — see the test file).
|
|
20
20
|
|
|
21
|
+
#include <libintl.h> // bindtextdomain / bind_textdomain_codeset / textdomain
|
|
22
|
+
#include <locale.h> // setlocale + the LC_* categories (and, on POSIX, locale_t)
|
|
23
|
+
|
|
21
24
|
#include <cstring>
|
|
22
25
|
|
|
23
26
|
#include "common.h"
|
|
@@ -348,4 +351,230 @@ Napi::Value BindingGroupBindFull(const Napi::CallbackInfo& info) {
|
|
|
348
351
|
return env.Undefined();
|
|
349
352
|
}
|
|
350
353
|
|
|
354
|
+
// ---- locale + gettext ------------------------------------------------------
|
|
355
|
+
//
|
|
356
|
+
// THE DEFECT THIS CLOSES. A C program starts in the "C" locale, where GNU gettext
|
|
357
|
+
// returns the untranslated msgid whatever LANG says and whatever bindtextdomain was
|
|
358
|
+
// told. Node never leaves it and neither did node-gtk, so every `--app node`
|
|
359
|
+
// application shipped untranslated with its catalogs present and correctly bound;
|
|
360
|
+
// gjs's entry point opens with `setlocale(LC_ALL, "")` (refs/gjs/gjs/console.cpp),
|
|
361
|
+
// so the SAME source translated under `--app gjs` and not under `--app node`.
|
|
362
|
+
// Why it hid for so long, why LC_NUMERIC is taken along, and the divergences from
|
|
363
|
+
// gjs: docs/node-gi-gjs-surface.md § Locale and gettext.
|
|
364
|
+
|
|
365
|
+
// LC_MESSAGES is a POSIX category MSVC's <locale.h> does not define; GNU gettext
|
|
366
|
+
// answers for it anyway and its <libintl.h> picks 1729 for the constant. Mirror
|
|
367
|
+
// that rather than dropping the category, so the JS-visible enum has the same
|
|
368
|
+
// members everywhere.
|
|
369
|
+
#ifndef LC_MESSAGES
|
|
370
|
+
#define LC_MESSAGES 1729
|
|
371
|
+
#endif
|
|
372
|
+
|
|
373
|
+
void NodeGiInitProcessLocale() {
|
|
374
|
+
static std::once_flag once;
|
|
375
|
+
std::call_once(once, [] {
|
|
376
|
+
// gjs ignores the result here; we log at debug level instead of warning,
|
|
377
|
+
// because the failure is silent in a way that has already cost a release:
|
|
378
|
+
// a LANG naming a locale the host never generated leaves the process in C
|
|
379
|
+
// and the whole UI in English, with nothing printed anywhere.
|
|
380
|
+
if (setlocale(LC_ALL, "") == nullptr)
|
|
381
|
+
g_debug("node-gi: setlocale(LC_ALL, \"\") failed — the locale named by the "
|
|
382
|
+
"environment is not available on this host; messages stay untranslated");
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// The per-thread locale gjs keeps behind GjsPrivate.set_thread_locale. The NAME
|
|
387
|
+
// cache is not an optimisation: gjs reports the locale through `setlocale(category,
|
|
388
|
+
// NULL)`, which answers for the GLOBAL locale and is therefore unchanged by the
|
|
389
|
+
// `uselocale` that did the work. Measured against gjs 1.88 with LC_ALL=de_DE.UTF-8:
|
|
390
|
+
// setting LC_MESSAGES to "C" stops translation immediately, yet the next query
|
|
391
|
+
// still answers "de_DE.UTF-8". Keeping the cache reproduces that answer exactly.
|
|
392
|
+
namespace {
|
|
393
|
+
|
|
394
|
+
struct ThreadLocale {
|
|
395
|
+
std::map<int, std::string> names; // last name set on this thread, per category
|
|
396
|
+
#ifndef _WIN32
|
|
397
|
+
locale_t id = static_cast<locale_t>(0);
|
|
398
|
+
|
|
399
|
+
// Cleanup beside creation: a worker thread that set a locale owns a locale_t
|
|
400
|
+
// nothing else can reach, so without this it leaks once per such thread.
|
|
401
|
+
// Uninstall BEFORE freeing — freelocale() on the locale still installed by
|
|
402
|
+
// uselocale() is undefined, and the thread is ending, so the global locale is
|
|
403
|
+
// the right thing to fall back to. gjs frees the same object from its GPrivate
|
|
404
|
+
// destructor for the same reason.
|
|
405
|
+
~ThreadLocale() {
|
|
406
|
+
if (id == static_cast<locale_t>(0)) return;
|
|
407
|
+
uselocale(LC_GLOBAL_LOCALE);
|
|
408
|
+
freelocale(id);
|
|
409
|
+
id = static_cast<locale_t>(0);
|
|
410
|
+
}
|
|
411
|
+
#endif
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
thread_local ThreadLocale t_locale;
|
|
415
|
+
|
|
416
|
+
#ifndef _WIN32
|
|
417
|
+
int LocaleCategoryMask(int category) {
|
|
418
|
+
// The header tells you not to compute this as (1 << category).
|
|
419
|
+
switch (category) {
|
|
420
|
+
case LC_ALL: return LC_ALL_MASK;
|
|
421
|
+
case LC_COLLATE: return LC_COLLATE_MASK;
|
|
422
|
+
case LC_CTYPE: return LC_CTYPE_MASK;
|
|
423
|
+
case LC_MONETARY: return LC_MONETARY_MASK;
|
|
424
|
+
case LC_NUMERIC: return LC_NUMERIC_MASK;
|
|
425
|
+
case LC_TIME: return LC_TIME_MASK;
|
|
426
|
+
#ifdef LC_MESSAGES_MASK
|
|
427
|
+
case LC_MESSAGES: return LC_MESSAGES_MASK;
|
|
428
|
+
#endif
|
|
429
|
+
default: return 0;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
#endif
|
|
433
|
+
|
|
434
|
+
// Returns the prior locale name for `category`, or nullptr when the locale could
|
|
435
|
+
// not be set — gjs's contract, and the JS layer turns nullptr into null.
|
|
436
|
+
const char* SetThreadLocaleImpl(int category, const char* name) {
|
|
437
|
+
auto reportCurrent = [category]() -> const char* {
|
|
438
|
+
auto it = t_locale.names.find(category);
|
|
439
|
+
if (it != t_locale.names.end()) return it->second.c_str();
|
|
440
|
+
return setlocale(category, nullptr);
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
if (name == nullptr) return reportCurrent(); // query form
|
|
444
|
+
|
|
445
|
+
const char* current = reportCurrent();
|
|
446
|
+
std::string prior = current == nullptr ? std::string() : std::string(current);
|
|
447
|
+
|
|
448
|
+
#ifdef _WIN32
|
|
449
|
+
// MSVC has no newlocale/uselocale. `_configthreadlocale` switches setlocale
|
|
450
|
+
// itself to per-thread, which is the same contract in a different spelling.
|
|
451
|
+
//
|
|
452
|
+
// LC_MESSAGES goes through the SAME call as every other category, deliberately.
|
|
453
|
+
// The MSVC CRT has no such category and answers NULL for it, and null is the
|
|
454
|
+
// honest report: the caller learns the category did not take. Special-casing it
|
|
455
|
+
// to "cache the name and skip the call" was worse than useless — it returned the
|
|
456
|
+
// PRIOR name for `setlocale(MESSAGES, 'no-such-locale.invalid')`, so a failure
|
|
457
|
+
// was indistinguishable from success. Messages still resolve on Windows, because
|
|
458
|
+
// GNU gettext reads them from the environment there rather than from the CRT
|
|
459
|
+
// locale; that is the divergence, and it is documented rather than papered over.
|
|
460
|
+
_configthreadlocale(_ENABLE_PER_THREAD_LOCALE);
|
|
461
|
+
if (setlocale(category, name) == nullptr) return nullptr;
|
|
462
|
+
#else
|
|
463
|
+
int mask = LocaleCategoryMask(category);
|
|
464
|
+
if (mask == 0) return nullptr;
|
|
465
|
+
|
|
466
|
+
locale_t base = uselocale(static_cast<locale_t>(0));
|
|
467
|
+
// duplocale() of LC_GLOBAL_LOCALE is the documented way to start from the
|
|
468
|
+
// global locale; newlocale() takes ownership of what it is handed.
|
|
469
|
+
locale_t seed = duplocale(base == LC_GLOBAL_LOCALE ? LC_GLOBAL_LOCALE : base);
|
|
470
|
+
if (seed == static_cast<locale_t>(0)) return nullptr;
|
|
471
|
+
|
|
472
|
+
locale_t composed = newlocale(mask, name, seed);
|
|
473
|
+
if (composed == static_cast<locale_t>(0)) {
|
|
474
|
+
freelocale(seed); // not consumed on failure
|
|
475
|
+
return nullptr;
|
|
476
|
+
}
|
|
477
|
+
if (uselocale(composed) == static_cast<locale_t>(0)) {
|
|
478
|
+
freelocale(composed);
|
|
479
|
+
return nullptr;
|
|
480
|
+
}
|
|
481
|
+
// Only now is the previous object unreferenced — freeing it while installed
|
|
482
|
+
// would leave every subsequent locale-sensitive call reading freed memory.
|
|
483
|
+
if (t_locale.id != static_cast<locale_t>(0)) freelocale(t_locale.id);
|
|
484
|
+
t_locale.id = composed;
|
|
485
|
+
#endif
|
|
486
|
+
|
|
487
|
+
t_locale.names[category] = prior; // gjs answers queries with the PRIOR name
|
|
488
|
+
// gjs returns prior_name, and the cache above now holds the same string — so
|
|
489
|
+
// hand back the cached copy rather than `prior`, which is about to go out of scope.
|
|
490
|
+
return t_locale.names[category].c_str();
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
} // namespace
|
|
494
|
+
|
|
495
|
+
// setThreadLocale(category: number, locale: string | null) -> string | null
|
|
496
|
+
Napi::Value ApplyThreadLocale(const Napi::CallbackInfo& info) {
|
|
497
|
+
Napi::Env env = info.Env();
|
|
498
|
+
if (info.Length() < 1 || !info[0].IsNumber()) {
|
|
499
|
+
Napi::TypeError::New(env, "setThreadLocale: expected (category: number, locale: string|null)")
|
|
500
|
+
.ThrowAsJavaScriptException();
|
|
501
|
+
return env.Null();
|
|
502
|
+
}
|
|
503
|
+
int category = info[0].As<Napi::Number>().Int32Value();
|
|
504
|
+
std::string name;
|
|
505
|
+
bool query = info.Length() < 2 || info[1].IsNull() || info[1].IsUndefined();
|
|
506
|
+
if (!query) {
|
|
507
|
+
if (!info[1].IsString()) {
|
|
508
|
+
Napi::TypeError::New(env, "setThreadLocale: locale must be a string or null")
|
|
509
|
+
.ThrowAsJavaScriptException();
|
|
510
|
+
return env.Null();
|
|
511
|
+
}
|
|
512
|
+
name = info[1].As<Napi::String>().Utf8Value();
|
|
513
|
+
}
|
|
514
|
+
const char* result = SetThreadLocaleImpl(category, query ? nullptr : name.c_str());
|
|
515
|
+
if (result == nullptr) return env.Null();
|
|
516
|
+
return Napi::String::New(env, result);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// textdomain(domain: string | null) -> string | null
|
|
520
|
+
Napi::Value Textdomain(const Napi::CallbackInfo& info) {
|
|
521
|
+
Napi::Env env = info.Env();
|
|
522
|
+
bool query = info.Length() < 1 || info[0].IsNull() || info[0].IsUndefined();
|
|
523
|
+
std::string domain;
|
|
524
|
+
if (!query) {
|
|
525
|
+
if (!info[0].IsString()) {
|
|
526
|
+
Napi::TypeError::New(env, "textdomain: expected a string or null")
|
|
527
|
+
.ThrowAsJavaScriptException();
|
|
528
|
+
return env.Null();
|
|
529
|
+
}
|
|
530
|
+
domain = info[0].As<Napi::String>().Utf8Value();
|
|
531
|
+
}
|
|
532
|
+
const char* result = textdomain(query ? nullptr : domain.c_str());
|
|
533
|
+
if (result == nullptr) return env.Null();
|
|
534
|
+
return Napi::String::New(env, result);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// bindtextdomain(domain: string, location: string | null) -> string | null
|
|
538
|
+
//
|
|
539
|
+
// The codeset is pinned to UTF-8 exactly as gjs_bindtextdomain does it: JS strings
|
|
540
|
+
// are UTF-16 and every marshalling path here assumes UTF-8 on the C side, so a
|
|
541
|
+
// catalog delivered in the locale's legacy charset would arrive as mojibake.
|
|
542
|
+
Napi::Value Bindtextdomain(const Napi::CallbackInfo& info) {
|
|
543
|
+
Napi::Env env = info.Env();
|
|
544
|
+
if (info.Length() < 1 || !info[0].IsString()) {
|
|
545
|
+
Napi::TypeError::New(env, "bindtextdomain: expected (domain: string, location: string|null)")
|
|
546
|
+
.ThrowAsJavaScriptException();
|
|
547
|
+
return env.Null();
|
|
548
|
+
}
|
|
549
|
+
std::string domain = info[0].As<Napi::String>().Utf8Value();
|
|
550
|
+
std::string location;
|
|
551
|
+
bool query = info.Length() < 2 || info[1].IsNull() || info[1].IsUndefined();
|
|
552
|
+
if (!query) {
|
|
553
|
+
if (!info[1].IsString()) {
|
|
554
|
+
Napi::TypeError::New(env, "bindtextdomain: location must be a string or null")
|
|
555
|
+
.ThrowAsJavaScriptException();
|
|
556
|
+
return env.Null();
|
|
557
|
+
}
|
|
558
|
+
location = info[1].As<Napi::String>().Utf8Value();
|
|
559
|
+
}
|
|
560
|
+
const char* result = bindtextdomain(domain.c_str(), query ? nullptr : location.c_str());
|
|
561
|
+
if (!query) bind_textdomain_codeset(domain.c_str(), "UTF-8");
|
|
562
|
+
if (result == nullptr) return env.Null();
|
|
563
|
+
return Napi::String::New(env, result);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// localeCategories() -> { ALL, COLLATE, CTYPE, MESSAGES, MONETARY, NUMERIC, TIME }
|
|
567
|
+
Napi::Value LocaleCategories(const Napi::CallbackInfo& info) {
|
|
568
|
+
Napi::Env env = info.Env();
|
|
569
|
+
Napi::Object out = Napi::Object::New(env);
|
|
570
|
+
out.Set("ALL", Napi::Number::New(env, LC_ALL));
|
|
571
|
+
out.Set("COLLATE", Napi::Number::New(env, LC_COLLATE));
|
|
572
|
+
out.Set("CTYPE", Napi::Number::New(env, LC_CTYPE));
|
|
573
|
+
out.Set("MESSAGES", Napi::Number::New(env, LC_MESSAGES));
|
|
574
|
+
out.Set("MONETARY", Napi::Number::New(env, LC_MONETARY));
|
|
575
|
+
out.Set("NUMERIC", Napi::Number::New(env, LC_NUMERIC));
|
|
576
|
+
out.Set("TIME", Napi::Number::New(env, LC_TIME));
|
|
577
|
+
return out;
|
|
578
|
+
}
|
|
579
|
+
|
|
351
580
|
} // namespace nodegi
|
package/src/repo.cc
CHANGED
|
@@ -19,6 +19,7 @@ void NodeGiEnvDataFinalize(napi_env env, void* data, void* /*hint*/) {
|
|
|
19
19
|
if (d->cairoWrappers != nullptr) napi_delete_reference(env, d->cairoWrappers);
|
|
20
20
|
if (d->microtaskDrain != nullptr) napi_delete_reference(env, d->microtaskDrain);
|
|
21
21
|
if (d->constructCallback != nullptr) napi_delete_reference(env, d->constructCallback);
|
|
22
|
+
if (d->propertySetCallback != nullptr) napi_delete_reference(env, d->propertySetCallback);
|
|
22
23
|
delete d;
|
|
23
24
|
}
|
|
24
25
|
|
package/src/toggle.cc
CHANGED
|
@@ -807,6 +807,23 @@ Napi::Value WrapGObject(Napi::Env env, GObject* obj, GITransfer transfer) {
|
|
|
807
807
|
return MakeGObjectHandle(env, obj);
|
|
808
808
|
}
|
|
809
809
|
|
|
810
|
+
// The canonical wrapper `obj` ALREADY has in this env, or an empty value when it has
|
|
811
|
+
// none. Never creates one and never touches a refcount — the whole point is to ask
|
|
812
|
+
// "is there a JS object to call into yet?" without bringing one into being, which is
|
|
813
|
+
// gjs's `priv->wrapper()` test (refs/gjs/gi/gobject.cpp gjs_object_set_gproperty
|
|
814
|
+
// bails when it is null). Same cross-env gate as WrapGObject's cache hit: a napi_ref
|
|
815
|
+
// belongs to inst->env and dereferencing it from another env is a UAF.
|
|
816
|
+
Napi::Value PeekGObjectHandle(Napi::Env env, GObject* obj) {
|
|
817
|
+
if (obj == nullptr) return Napi::Value();
|
|
818
|
+
NodeGiInstance* inst =
|
|
819
|
+
static_cast<NodeGiInstance*>(g_object_get_qdata(obj, NodeGiWrapperQuark()));
|
|
820
|
+
if (inst == nullptr || inst->env != static_cast<napi_env>(env)) return Napi::Value();
|
|
821
|
+
napi_value cached = nullptr;
|
|
822
|
+
if (napi_get_reference_value(env, inst->handle_ref, &cached) != napi_ok || cached == nullptr)
|
|
823
|
+
return Napi::Value(); // collected, teardown pending — there is nothing live to call
|
|
824
|
+
return Napi::Value(env, cached);
|
|
825
|
+
}
|
|
826
|
+
|
|
810
827
|
// ============================ TEST-ONLY ===============================
|
|
811
828
|
// __stressRefUnrefOffThread(handle, iterations) — the GLib-thread vehicle the
|
|
812
829
|
// cross-thread GC stress test needs (there is no headless JS way to make another
|