1/* This file is part of the KDE libraries
2 SPDX-FileCopyrightText: 2007 Chusslove Illich <caslav.ilic@gmx.net>
3 SPDX-FileCopyrightText: 2014 Kevin Krammer <krammer@kde.org>
4
5 SPDX-License-Identifier: LGPL-2.0-or-later
6*/
7
8#include <common_helpers_p.h>
9#include <ktranscript_p.h>
10
11#include <ktranscript_export.h>
12
13//#include <unistd.h>
14
15#include <QJSEngine>
16
17#include <QDebug>
18#include <QDir>
19#include <QFile>
20#include <QHash>
21#include <QIODevice>
22#include <QJSValueIterator>
23#include <QList>
24#include <QSet>
25#include <QStandardPaths>
26#include <QStringList>
27#include <QTextStream>
28#include <QVariant>
29#include <qendian.h>
30
31class KTranscriptImp;
32class Scriptface;
33
34typedef QHash<QString, QString> TsConfigGroup;
35typedef QHash<QString, TsConfigGroup> TsConfig;
36
37// Transcript implementation (used as singleton).
38class KTranscriptImp : public KTranscript
39{
40public:
41 KTranscriptImp();
42 ~KTranscriptImp() override;
43
44 QString eval(const QList<QVariant> &argv,
45 const QString &lang,
46 const QString &ctry,
47 const QString &msgctxt,
48 const QHash<QString, QString> &dynctxt,
49 const QString &msgid,
50 const QStringList &subs,
51 const QList<QVariant> &vals,
52 const QString &ftrans,
53 QList<QStringList> &mods,
54 QString &error,
55 bool &fallback) override;
56
57 QStringList postCalls(const QString &lang) override;
58
59 // Lexical path of the module for the executing code.
60 QString currentModulePath;
61
62private:
63 void loadModules(const QList<QStringList> &mods, QString &error);
64 void setupInterpreter(const QString &lang);
65
66 TsConfig config;
67
68 QHash<QString, Scriptface *> m_sface;
69};
70
71// Script-side transcript interface.
72class Scriptface : public QObject
73{
74 Q_OBJECT
75public:
76 explicit Scriptface(const TsConfigGroup &config, QObject *parent = nullptr);
77 ~Scriptface();
78
79 // Interface functions.
80 Q_INVOKABLE QJSValue load(const QString &name);
81 Q_INVOKABLE QJSValue setcall(const QJSValue &name, const QJSValue &func, const QJSValue &fval = QJSValue::NullValue);
82 Q_INVOKABLE QJSValue hascall(const QString &name);
83 Q_INVOKABLE QJSValue acallInternal(const QJSValue &args);
84 Q_INVOKABLE QJSValue setcallForall(const QJSValue &name, const QJSValue &func, const QJSValue &fval = QJSValue::NullValue);
85 Q_INVOKABLE QJSValue fallback();
86 Q_INVOKABLE QJSValue nsubs();
87 Q_INVOKABLE QJSValue subs(const QJSValue &index);
88 Q_INVOKABLE QJSValue vals(const QJSValue &index);
89 Q_INVOKABLE QJSValue msgctxt();
90 Q_INVOKABLE QJSValue dynctxt(const QString &key);
91 Q_INVOKABLE QJSValue msgid();
92 Q_INVOKABLE QJSValue msgkey();
93 Q_INVOKABLE QJSValue msgstrf();
94 Q_INVOKABLE void dbgputs(const QString &str);
95 Q_INVOKABLE void warnputs(const QString &str);
96 Q_INVOKABLE QJSValue localeCountry();
97 Q_INVOKABLE QJSValue normKey(const QJSValue &phrase);
98 Q_INVOKABLE QJSValue loadProps(const QString &name);
99 Q_INVOKABLE QJSValue getProp(const QJSValue &phrase, const QJSValue &prop);
100 Q_INVOKABLE QJSValue setProp(const QJSValue &phrase, const QJSValue &prop, const QJSValue &value);
101 Q_INVOKABLE QJSValue toUpperFirst(const QJSValue &str, const QJSValue &nalt = QJSValue::NullValue);
102 Q_INVOKABLE QJSValue toLowerFirst(const QJSValue &str, const QJSValue &nalt = QJSValue::NullValue);
103 Q_INVOKABLE QJSValue getConfString(const QJSValue &key, const QJSValue &dval = QJSValue::NullValue);
104 Q_INVOKABLE QJSValue getConfBool(const QJSValue &key, const QJSValue &dval = QJSValue::NullValue);
105 Q_INVOKABLE QJSValue getConfNumber(const QJSValue &key, const QJSValue &dval = QJSValue::NullValue);
106
107 // Helper methods to interface functions.
108 QJSValue load(const QJSValueList &names);
109 QJSValue loadProps(const QJSValueList &names);
110 QString loadProps_text(const QString &fpath);
111 QString loadProps_bin(const QString &fpath);
112 QString loadProps_bin_00(const QString &fpath);
113 QString loadProps_bin_01(const QString &fpath);
114
115 void put(const QString &propertyName, const QJSValue &value);
116
117 // Link to its script engine
118 QJSEngine *const scriptEngine;
119
120 // Current message data.
121 const QString *msgcontext;
122 const QHash<QString, QString> *dyncontext;
123 const QString *msgId;
124 const QStringList *subList;
125 const QList<QVariant> *valList;
126 const QString *ftrans;
127 const QString *ctry;
128
129 // Fallback request handle.
130 bool *fallbackRequest;
131
132 // Function register.
133 QHash<QString, QJSValue> funcs;
134 QHash<QString, QJSValue> fvals;
135 QHash<QString, QString> fpaths;
136
137 // Ordering of those functions which execute for all messages.
138 QList<QString> nameForalls;
139
140 // Property values per phrase (used by *Prop interface calls).
141 // Not QStrings, in order to avoid conversion from UTF-8 when
142 // loading compiled maps (less latency on startup).
143 QHash<QByteArray, QHash<QByteArray, QByteArray>> phraseProps;
144 // Unresolved property values per phrase,
145 // containing the pointer to compiled pmap file handle and offset in it.
146 struct UnparsedPropInfo {
147 QFile *pmapFile = nullptr;
148 quint64 offset = -1;
149 };
150 QHash<QByteArray, UnparsedPropInfo> phraseUnparsedProps;
151 QHash<QByteArray, QByteArray> resolveUnparsedProps(const QByteArray &phrase);
152 // Set of loaded pmap files by paths and file handle pointers.
153 QSet<QString> loadedPmapPaths;
154 QSet<QFile *> loadedPmapHandles;
155
156 // User config.
157 TsConfigGroup config;
158};
159
160// ----------------------------------------------------------------------
161// Custom debug and warning output (kdebug not available)
162#define DBGP "KTranscript: "
163void dbgout(const char *str)
164{
165#ifndef NDEBUG
166 fprintf(stderr, DBGP "%s\n", str);
167#else
168 Q_UNUSED(str);
169#endif
170}
171template<typename T1>
172void dbgout(const char *str, const T1 &a1)
173{
174#ifndef NDEBUG
175 fprintf(stderr, DBGP "%s\n", QString::fromUtf8(utf8: str).arg(a1).toLocal8Bit().data());
176#else
177 Q_UNUSED(str);
178 Q_UNUSED(a1);
179#endif
180}
181template<typename T1, typename T2>
182void dbgout(const char *str, const T1 &a1, const T2 &a2)
183{
184#ifndef NDEBUG
185 fprintf(stderr, DBGP "%s\n", QString::fromUtf8(utf8: str).arg(a1).arg(a2).toLocal8Bit().data());
186#else
187 Q_UNUSED(str);
188 Q_UNUSED(a1);
189 Q_UNUSED(a2);
190#endif
191}
192template<typename T1, typename T2, typename T3>
193void dbgout(const char *str, const T1 &a1, const T2 &a2, const T3 &a3)
194{
195#ifndef NDEBUG
196 fprintf(stderr, DBGP "%s\n", QString::fromUtf8(utf8: str).arg(a1).arg(a2).arg(a3).toLocal8Bit().data());
197#else
198 Q_UNUSED(str);
199 Q_UNUSED(a1);
200 Q_UNUSED(a2);
201 Q_UNUSED(a3);
202#endif
203}
204
205#define WARNP "KTranscript: "
206void warnout(const char *str)
207{
208 fprintf(stderr, WARNP "%s\n", str);
209}
210template<typename T1>
211void warnout(const char *str, const T1 &a1)
212{
213 fprintf(stderr, WARNP "%s\n", QString::fromUtf8(utf8: str).arg(a1).toLocal8Bit().data());
214}
215
216// ----------------------------------------------------------------------
217// Produces a string out of a script exception.
218
219QString expt2str(const QJSValue &expt)
220{
221 if (expt.isError()) {
222 const QJSValue message = expt.property(QStringLiteral("message"));
223 if (!message.isUndefined()) {
224 return QStringLiteral("Error: %1").arg(a: message.toString());
225 }
226 }
227
228 QString strexpt = expt.toString();
229 return QStringLiteral("Caught exception: %1").arg(a: strexpt);
230}
231
232// ----------------------------------------------------------------------
233// Count number of lines in the string,
234// up to and excluding the requested position.
235int countLines(const QString &s, int p)
236{
237 int n = 1;
238 int len = s.length();
239 for (int i = 0; i < p && i < len; ++i) {
240 if (s[i] == QLatin1Char('\n')) {
241 ++n;
242 }
243 }
244 return n;
245}
246
247// ----------------------------------------------------------------------
248// Normalize string key for hash lookups,
249QByteArray normKeystr(const QString &raw, bool mayHaveAcc = true)
250{
251 // NOTE: Regexes should not be used here for performance reasons.
252 // This function may potentially be called thousands of times
253 // on application startup.
254
255 QString key = raw;
256
257 // Strip all whitespace.
258 int len = key.length();
259 QString nkey;
260 for (int i = 0; i < len; ++i) {
261 QChar c = key[i];
262 if (!c.isSpace()) {
263 nkey.append(c);
264 }
265 }
266 key = nkey;
267
268 // Strip accelerator marker.
269 if (mayHaveAcc) {
270 key = removeAcceleratorMarker(label: key);
271 }
272
273 // Convert to lower case.
274 key = key.toLower();
275
276 return key.toUtf8();
277}
278
279// ----------------------------------------------------------------------
280// Trim multiline string in a "smart" way:
281// Remove leading and trailing whitespace up to and including first
282// newline from that side, if there is one; otherwise, don't touch.
283QString trimSmart(const QString &raw)
284{
285 // NOTE: This could be done by a single regex, but is not due to
286 // performance reasons.
287 // This function may potentially be called thousands of times
288 // on application startup.
289
290 int len = raw.length();
291
292 int is = 0;
293 while (is < len && raw[is].isSpace() && raw[is] != QLatin1Char('\n')) {
294 ++is;
295 }
296 if (is >= len || raw[is] != QLatin1Char('\n')) {
297 is = -1;
298 }
299
300 int ie = len - 1;
301 while (ie >= 0 && raw[ie].isSpace() && raw[ie] != QLatin1Char('\n')) {
302 --ie;
303 }
304 if (ie < 0 || raw[ie] != QLatin1Char('\n')) {
305 ie = len;
306 }
307
308 return raw.mid(position: is + 1, n: ie - is - 1);
309}
310
311// ----------------------------------------------------------------------
312// Produce a JavaScript object out of Qt variant.
313
314QJSValue variantToJsValue(const QVariant &val)
315{
316 const auto vtype = val.userType();
317 if (vtype == QMetaType::QString) {
318 return QJSValue(val.toString());
319 } else if (vtype == QMetaType::Bool) {
320 return QJSValue(val.toBool());
321 } else if (vtype == QMetaType::Double //
322 || vtype == QMetaType::Int //
323 || vtype == QMetaType::UInt //
324 || vtype == QMetaType::LongLong //
325 || vtype == QMetaType::ULongLong) {
326 return QJSValue(val.toDouble());
327 } else {
328 return QJSValue::UndefinedValue;
329 }
330}
331
332// ----------------------------------------------------------------------
333// Parse ini-style config file,
334// returning content as hash of hashes by group and key.
335// Parsing is not fussy, it will read what it can.
336TsConfig readConfig(const QString &fname)
337{
338 TsConfig config;
339 // Add empty group.
340 TsConfig::iterator configGroup;
341 configGroup = config.insert(key: QString(), value: TsConfigGroup());
342
343 QFile file(fname);
344 if (!file.open(flags: QIODevice::ReadOnly)) {
345 return config;
346 }
347 QTextStream stream(&file);
348 while (!stream.atEnd()) {
349 QString line = stream.readLine();
350 int p1;
351 int p2;
352
353 // Remove comment from the line.
354 p1 = line.indexOf(c: QLatin1Char('#'));
355 if (p1 >= 0) {
356 line.truncate(pos: p1);
357 }
358 line = line.trimmed();
359 if (line.isEmpty()) {
360 continue;
361 }
362
363 if (line[0] == QLatin1Char('[')) {
364 // Group switch.
365 p1 = 0;
366 p2 = line.indexOf(c: QLatin1Char(']'), from: p1 + 1);
367 if (p2 < 0) {
368 continue;
369 }
370 QString group = line.mid(position: p1 + 1, n: p2 - p1 - 1).trimmed();
371 configGroup = config.find(key: group);
372 if (configGroup == config.end()) {
373 // Add new group.
374 configGroup = config.insert(key: group, value: TsConfigGroup());
375 }
376 } else {
377 // Field.
378 p1 = line.indexOf(c: QLatin1Char('='));
379 if (p1 < 0) {
380 continue;
381 }
382
383 const QStringView lineView(line);
384 const QStringView field = lineView.left(n: p1).trimmed();
385 if (!field.isEmpty()) {
386 const QStringView value = lineView.mid(pos: p1 + 1).trimmed();
387 (*configGroup)[field.toString()] = value.toString();
388 }
389 }
390 }
391 file.close();
392
393 return config;
394}
395
396// ----------------------------------------------------------------------
397// throw or log error, depending on context availability
398static QJSValue throwError(QJSEngine *engine, const QString &message)
399{
400 if (engine) {
401 return engine->evaluate(QStringLiteral("new Error(%1)").arg(a: message));
402 }
403
404 qCritical() << "Script error" << message;
405 return QJSValue::UndefinedValue;
406}
407
408#ifdef KTRANSCRIPT_TESTBUILD
409
410// ----------------------------------------------------------------------
411// Test build creation/destruction hooks
412static KTranscriptImp *s_transcriptInstance = nullptr;
413
414KTranscriptImp *globalKTI()
415{
416 return s_transcriptInstance;
417}
418
419KTranscript *autotestCreateKTranscriptImp()
420{
421 Q_ASSERT(s_transcriptInstance == nullptr);
422 s_transcriptInstance = new KTranscriptImp;
423 return s_transcriptInstance;
424}
425
426void autotestDestroyKTranscriptImp()
427{
428 Q_ASSERT(s_transcriptInstance != nullptr);
429 delete s_transcriptInstance;
430 s_transcriptInstance = nullptr;
431}
432
433#else
434
435// ----------------------------------------------------------------------
436// Dynamic loading.
437Q_GLOBAL_STATIC(KTranscriptImp, globalKTI)
438extern "C" {
439KTRANSCRIPT_EXPORT KTranscript *load_transcript()
440{
441 return globalKTI();
442}
443}
444#endif
445
446// ----------------------------------------------------------------------
447// KTranscript definitions.
448
449KTranscriptImp::KTranscriptImp()
450{
451 // Load user configuration.
452
453 QString tsConfigPath = QStandardPaths::locate(type: QStandardPaths::ConfigLocation, QStringLiteral("ktranscript.ini"));
454 if (tsConfigPath.isEmpty()) {
455 tsConfigPath = QDir::homePath() + QLatin1Char('/') + QLatin1String(".transcriptrc");
456 }
457 config = readConfig(fname: tsConfigPath);
458}
459
460KTranscriptImp::~KTranscriptImp()
461{
462 qDeleteAll(c: m_sface);
463}
464
465QString KTranscriptImp::eval(const QList<QVariant> &argv,
466 const QString &lang,
467 const QString &ctry,
468 const QString &msgctxt,
469 const QHash<QString, QString> &dynctxt,
470 const QString &msgid,
471 const QStringList &subs,
472 const QList<QVariant> &vals,
473 const QString &ftrans,
474 QList<QStringList> &mods,
475 QString &error,
476 bool &fallback)
477{
478 // error = "debug"; return QString();
479
480 error.clear(); // empty error message means successful evaluation
481 fallback = false; // fallback not requested
482
483#if 0
484 // FIXME: Maybe not needed, as QJSEngine has no native outside access?
485 // Unportable (needs unistd.h)?
486
487 // If effective user id is root and real user id is not root.
488 if (geteuid() == 0 && getuid() != 0) {
489 // Since scripts are user input, and the program is running with
490 // root permissions while real user is not root, do not invoke
491 // scripting at all, to prevent exploits.
492 error = "Security block: trying to execute a script in suid environment.";
493 return QString();
494 }
495#endif
496
497 // Load any new modules and clear the list.
498 if (!mods.isEmpty()) {
499 loadModules(mods, error);
500 mods.clear();
501 if (!error.isEmpty()) {
502 return QString();
503 }
504 }
505
506 // Add interpreters for new languages.
507 // (though it should never happen here, but earlier when loading modules;
508 // this also means there are no calls set, so the unregistered call error
509 // below will be reported).
510 if (!m_sface.contains(key: lang)) {
511 setupInterpreter(lang);
512 }
513
514 // Shortcuts.
515 Scriptface *sface = m_sface[lang];
516
517 QJSEngine *engine = sface->scriptEngine;
518 QJSValue gobj = engine->globalObject();
519
520 // Link current message data for script-side interface.
521 sface->msgcontext = &msgctxt;
522 sface->dyncontext = &dynctxt;
523 sface->msgId = &msgid;
524 sface->subList = &subs;
525 sface->valList = &vals;
526 sface->ftrans = &ftrans;
527 sface->fallbackRequest = &fallback;
528 sface->ctry = &ctry;
529
530 // Find corresponding JS function.
531 int argc = argv.size();
532 if (argc < 1) {
533 // error = "At least the call name must be supplied.";
534 // Empty interpolation is OK, possibly used just to initialize
535 // at a given point (e.g. for Ts.setForall() to start having effect).
536 return QString();
537 }
538 QString funcName = argv[0].toString();
539 if (!sface->funcs.contains(key: funcName)) {
540 error = QStringLiteral("Unregistered call to '%1'.").arg(a: funcName);
541 return QString();
542 }
543
544 QJSValue func = sface->funcs[funcName];
545 QJSValue fval = sface->fvals[funcName];
546
547 // Recover module path from the time of definition of this call,
548 // for possible load calls.
549 currentModulePath = sface->fpaths[funcName];
550
551 // Execute function.
552 QJSValueList arglist;
553 arglist.reserve(asize: argc - 1);
554 for (int i = 1; i < argc; ++i) {
555 arglist.append(t: engine->toScriptValue(value: argv[i]));
556 }
557
558 QJSValue val;
559 if (fval.isObject()) {
560 val = func.callWithInstance(instance: fval, args: arglist);
561 } else { // no object associated to this function, use global
562 val = func.callWithInstance(instance: gobj, args: arglist);
563 }
564
565 if (fallback) {
566 // Fallback to ordinary translation requested.
567 return QString();
568 } else if (!val.isError()) {
569 // Evaluation successful.
570
571 if (val.isString()) {
572 // Good to go.
573
574 return val.toString();
575 } else {
576 // Accept only strings.
577
578 QString strval = val.toString();
579 error = QStringLiteral("Non-string return value: %1").arg(a: strval);
580 return QString();
581 }
582 } else {
583 // Exception raised.
584
585 error = expt2str(expt: val);
586
587 return QString();
588 }
589}
590
591QStringList KTranscriptImp::postCalls(const QString &lang)
592{
593 // Return no calls if scripting was not already set up for this language.
594 // NOTE: This shouldn't happen, as postCalls cannot be called in such case.
595 if (!m_sface.contains(key: lang)) {
596 return QStringList();
597 }
598
599 // Shortcuts.
600 Scriptface *sface = m_sface[lang];
601
602 return sface->nameForalls;
603}
604
605void KTranscriptImp::loadModules(const QList<QStringList> &mods, QString &error)
606{
607 QList<QString> modErrors;
608
609 for (const QStringList &mod : mods) {
610 QString mpath = mod[0];
611 QString mlang = mod[1];
612
613 // Add interpreters for new languages.
614 if (!m_sface.contains(key: mlang)) {
615 setupInterpreter(mlang);
616 }
617
618 // Setup current module path for loading submodules.
619 // (sort of closure over invocations of loadf)
620 int posls = mpath.lastIndexOf(c: QLatin1Char('/'));
621 if (posls < 1) {
622 modErrors.append(QStringLiteral("Funny module path '%1', skipping.").arg(a: mpath));
623 continue;
624 }
625 currentModulePath = mpath.left(n: posls);
626 QString fname = mpath.mid(position: posls + 1);
627 // Scriptface::loadf() wants no extension on the filename
628 fname = fname.left(n: fname.lastIndexOf(c: QLatin1Char('.')));
629
630 // Load the module.
631 QJSValueList alist;
632 alist.append(t: QJSValue(fname));
633
634 m_sface[mlang]->load(names: alist);
635 }
636
637 // Unset module path.
638 currentModulePath.clear();
639
640 for (const QString &merr : std::as_const(t&: modErrors)) {
641 error.append(s: merr + QLatin1Char('\n'));
642 }
643}
644
645#define SFNAME "Ts"
646void KTranscriptImp::setupInterpreter(const QString &lang)
647{
648 // Add scripting interface
649 // Creates its own script engine and registers with it
650 // NOTE: Config may not contain an entry for the language, in which case
651 // it is automatically constructed as an empty hash. This is intended.
652 Scriptface *sface = new Scriptface(config[lang]);
653
654 // Store scriptface
655 m_sface[lang] = sface;
656
657 // dbgout("=====> Created interpreter for '%1'", lang);
658}
659
660Scriptface::Scriptface(const TsConfigGroup &config_, QObject *parent)
661 : QObject(parent)
662 , scriptEngine(new QJSEngine)
663 , fallbackRequest(nullptr)
664 , config(config_)
665{
666 QJSValue object = scriptEngine->newQObject(object: this);
667 scriptEngine->globalObject().setProperty(QStringLiteral(SFNAME), value: object);
668 scriptEngine->evaluate(QStringLiteral("Ts.acall = function() { return Ts.acallInternal(Array.prototype.slice.call(arguments)); };"));
669}
670
671Scriptface::~Scriptface()
672{
673 qDeleteAll(c: loadedPmapHandles);
674 scriptEngine->deleteLater();
675}
676
677void Scriptface::put(const QString &propertyName, const QJSValue &value)
678{
679 QJSValue internalObject = scriptEngine->globalObject().property(QStringLiteral("ScriptfaceInternal"));
680 if (internalObject.isUndefined()) {
681 internalObject = scriptEngine->newObject();
682 scriptEngine->globalObject().setProperty(QStringLiteral("ScriptfaceInternal"), value: internalObject);
683 }
684
685 internalObject.setProperty(name: propertyName, value);
686}
687
688// ----------------------------------------------------------------------
689// Scriptface interface functions.
690
691#ifdef _MSC_VER
692// Work around bizarre MSVC (2013) bug preventing use of QStringLiteral for concatenated string literals
693#define SPREF(X) QString::fromLatin1(SFNAME "." X)
694#else
695#define SPREF(X) QStringLiteral(SFNAME "." X)
696#endif
697
698QJSValue Scriptface::load(const QString &name)
699{
700 QJSValueList fnames;
701 fnames << name;
702 return load(names: fnames);
703}
704
705QJSValue Scriptface::setcall(const QJSValue &name, const QJSValue &func, const QJSValue &fval)
706{
707 if (!name.isString()) {
708 return throwError(engine: scriptEngine, SPREF("setcall: expected string as first argument"));
709 }
710 if (!func.isCallable()) {
711 return throwError(engine: scriptEngine, SPREF("setcall: expected function as second argument"));
712 }
713 if (!(fval.isObject() || fval.isNull())) {
714 return throwError(engine: scriptEngine, SPREF("setcall: expected object or null as third argument"));
715 }
716
717 QString qname = name.toString();
718 funcs[qname] = func;
719 fvals[qname] = fval;
720
721 // Register values to keep GC from collecting them. Is this needed?
722 put(QStringLiteral("#:f<%1>").arg(a: qname), value: func);
723 put(QStringLiteral("#:o<%1>").arg(a: qname), value: fval);
724
725 // Set current module path as module path for this call,
726 // in case it contains load subcalls.
727 fpaths[qname] = globalKTI()->currentModulePath;
728
729 return QJSValue::UndefinedValue;
730}
731
732QJSValue Scriptface::hascall(const QString &qname)
733{
734 return QJSValue(funcs.contains(key: qname));
735}
736
737QJSValue Scriptface::acallInternal(const QJSValue &args)
738{
739 QJSValueIterator it(args);
740
741 if (!it.next()) {
742 return throwError(engine: scriptEngine, SPREF("acall: expected at least one argument (call name)"));
743 }
744 if (!it.value().isString()) {
745 return throwError(engine: scriptEngine, SPREF("acall: expected string as first argument (call name)"));
746 }
747 // Get the function and its context object.
748 QString callname = it.value().toString();
749 if (!funcs.contains(key: callname)) {
750 return throwError(engine: scriptEngine, SPREF("acall: unregistered call to '%1'").arg(a: callname));
751 }
752 QJSValue func = funcs[callname];
753 QJSValue fval = fvals[callname];
754
755 // Recover module path from the time of definition of this call,
756 // for possible load calls.
757 globalKTI()->currentModulePath = fpaths[callname];
758
759 // Execute function.
760 QJSValueList arglist;
761 while (it.next()) {
762 arglist.append(t: it.value());
763 }
764
765 QJSValue val;
766 if (fval.isObject()) {
767 // Call function with the context object.
768 val = func.callWithInstance(instance: fval, args: arglist);
769 } else {
770 // No context object associated to this function, use global.
771 val = func.callWithInstance(instance: scriptEngine->globalObject(), args: arglist);
772 }
773 return val;
774}
775
776QJSValue Scriptface::setcallForall(const QJSValue &name, const QJSValue &func, const QJSValue &fval)
777{
778 if (!name.isString()) {
779 return throwError(engine: scriptEngine, SPREF("setcallForall: expected string as first argument"));
780 }
781 if (!func.isCallable()) {
782 return throwError(engine: scriptEngine, SPREF("setcallForall: expected function as second argument"));
783 }
784 if (!(fval.isObject() || fval.isNull())) {
785 return throwError(engine: scriptEngine, SPREF("setcallForall: expected object or null as third argument"));
786 }
787
788 QString qname = name.toString();
789 funcs[qname] = func;
790 fvals[qname] = fval;
791
792 // Register values to keep GC from collecting them. Is this needed?
793 put(QStringLiteral("#:fall<%1>").arg(a: qname), value: func);
794 put(QStringLiteral("#:oall<%1>").arg(a: qname), value: fval);
795
796 // Set current module path as module path for this call,
797 // in case it contains load subcalls.
798 fpaths[qname] = globalKTI()->currentModulePath;
799
800 // Put in the queue order for execution on all messages.
801 nameForalls.append(t: qname);
802
803 return QJSValue::UndefinedValue;
804}
805
806QJSValue Scriptface::fallback()
807{
808 if (fallbackRequest) {
809 *fallbackRequest = true;
810 }
811 return QJSValue::UndefinedValue;
812}
813
814QJSValue Scriptface::nsubs()
815{
816 return QJSValue(static_cast<int>(subList->size()));
817}
818
819QJSValue Scriptface::subs(const QJSValue &index)
820{
821 if (!index.isNumber()) {
822 return throwError(engine: scriptEngine, SPREF("subs: expected number as first argument"));
823 }
824
825 int i = qRound(d: index.toNumber());
826 if (i < 0 || i >= subList->size()) {
827 return throwError(engine: scriptEngine, SPREF("subs: index out of range"));
828 }
829
830 return QJSValue(subList->at(i));
831}
832
833QJSValue Scriptface::vals(const QJSValue &index)
834{
835 if (!index.isNumber()) {
836 return throwError(engine: scriptEngine, SPREF("vals: expected number as first argument"));
837 }
838
839 int i = qRound(d: index.toNumber());
840 if (i < 0 || i >= valList->size()) {
841 return throwError(engine: scriptEngine, SPREF("vals: index out of range"));
842 }
843
844 return scriptEngine->toScriptValue(value: valList->at(i));
845 // return variantToJsValue(valList->at(i));
846}
847
848QJSValue Scriptface::msgctxt()
849{
850 return QJSValue(*msgcontext);
851}
852
853QJSValue Scriptface::dynctxt(const QString &qkey)
854{
855 auto valIt = dyncontext->constFind(key: qkey);
856 if (valIt != dyncontext->constEnd()) {
857 return QJSValue(*valIt);
858 }
859 return QJSValue::UndefinedValue;
860}
861
862QJSValue Scriptface::msgid()
863{
864 return QJSValue(*msgId);
865}
866
867QJSValue Scriptface::msgkey()
868{
869 return QJSValue(QString(*msgcontext + QLatin1Char('|') + *msgId));
870}
871
872QJSValue Scriptface::msgstrf()
873{
874 return QJSValue(*ftrans);
875}
876
877void Scriptface::dbgputs(const QString &qstr)
878{
879 dbgout(str: "[JS-debug] %1", a1: qstr);
880}
881
882void Scriptface::warnputs(const QString &qstr)
883{
884 warnout(str: "[JS-warning] %1", a1: qstr);
885}
886
887QJSValue Scriptface::localeCountry()
888{
889 return QJSValue(*ctry);
890}
891
892QJSValue Scriptface::normKey(const QJSValue &phrase)
893{
894 if (!phrase.isString()) {
895 return throwError(engine: scriptEngine, SPREF("normKey: expected string as argument"));
896 }
897
898 QByteArray nqphrase = normKeystr(raw: phrase.toString());
899 return QJSValue(QString::fromUtf8(ba: nqphrase));
900}
901
902QJSValue Scriptface::loadProps(const QString &name)
903{
904 QJSValueList fnames;
905 fnames << name;
906 return loadProps(names: fnames);
907}
908
909QJSValue Scriptface::loadProps(const QJSValueList &fnames)
910{
911 if (globalKTI()->currentModulePath.isEmpty()) {
912 return throwError(engine: scriptEngine, SPREF("loadProps: no current module path, aiiie..."));
913 }
914
915 for (int i = 0; i < fnames.size(); ++i) {
916 if (!fnames[i].isString()) {
917 return throwError(engine: scriptEngine, SPREF("loadProps: expected string as file name"));
918 }
919 }
920
921 for (int i = 0; i < fnames.size(); ++i) {
922 QString qfname = fnames[i].toString();
923 QString qfpath_base = globalKTI()->currentModulePath + QLatin1Char('/') + qfname;
924
925 // Determine which kind of map is available.
926 // Give preference to compiled map.
927 QString qfpath = qfpath_base + QLatin1String(".pmapc");
928 bool haveCompiled = true;
929 QFile file_check(qfpath);
930 if (!file_check.open(flags: QIODevice::ReadOnly)) {
931 haveCompiled = false;
932 qfpath = qfpath_base + QLatin1String(".pmap");
933 QFile file_check(qfpath);
934 if (!file_check.open(flags: QIODevice::ReadOnly)) {
935 return throwError(engine: scriptEngine, SPREF("loadProps: cannot read map '%1'").arg(a: qfpath));
936 }
937 }
938 file_check.close();
939
940 // Load from appropriate type of map.
941 if (!loadedPmapPaths.contains(value: qfpath)) {
942 QString errorString;
943 if (haveCompiled) {
944 errorString = loadProps_bin(fpath: qfpath);
945 } else {
946 errorString = loadProps_text(fpath: qfpath);
947 }
948 if (!errorString.isEmpty()) {
949 return throwError(engine: scriptEngine, message: errorString);
950 }
951 dbgout(str: "Loaded property map: %1", a1: qfpath);
952 loadedPmapPaths.insert(value: qfpath);
953 }
954 }
955
956 return QJSValue::UndefinedValue;
957}
958
959QJSValue Scriptface::getProp(const QJSValue &phrase, const QJSValue &prop)
960{
961 if (!phrase.isString()) {
962 return throwError(engine: scriptEngine, SPREF("getProp: expected string as first argument"));
963 }
964 if (!prop.isString()) {
965 return throwError(engine: scriptEngine, SPREF("getProp: expected string as second argument"));
966 }
967
968 QByteArray qphrase = normKeystr(raw: phrase.toString());
969 QHash<QByteArray, QByteArray> props = phraseProps.value(key: qphrase);
970 if (props.isEmpty()) {
971 props = resolveUnparsedProps(phrase: qphrase);
972 }
973 if (!props.isEmpty()) {
974 QByteArray qprop = normKeystr(raw: prop.toString());
975 QByteArray qval = props.value(key: qprop);
976 if (!qval.isEmpty()) {
977 return QJSValue(QString::fromUtf8(ba: qval));
978 }
979 }
980 return QJSValue::UndefinedValue;
981}
982
983QJSValue Scriptface::setProp(const QJSValue &phrase, const QJSValue &prop, const QJSValue &value)
984{
985 if (!phrase.isString()) {
986 return throwError(engine: scriptEngine, SPREF("setProp: expected string as first argument"));
987 }
988 if (!prop.isString()) {
989 return throwError(engine: scriptEngine, SPREF("setProp: expected string as second argument"));
990 }
991 if (!value.isString()) {
992 return throwError(engine: scriptEngine, SPREF("setProp: expected string as third argument"));
993 }
994
995 QByteArray qphrase = normKeystr(raw: phrase.toString());
996 QByteArray qprop = normKeystr(raw: prop.toString());
997 QByteArray qvalue = value.toString().toUtf8();
998 // Any non-existent key in first or second-level hash will be created.
999 phraseProps[qphrase][qprop] = qvalue;
1000 return QJSValue::UndefinedValue;
1001}
1002
1003static QString toCaseFirst(const QString &qstr, int qnalt, bool toupper)
1004{
1005 static const QLatin1String head("~@");
1006 static const int hlen = 2; // head.length()
1007
1008 // If the first letter is found within an alternatives directive,
1009 // change case of the first letter in each of the alternatives.
1010 QString qstrcc = qstr;
1011 const int len = qstr.length();
1012 QChar altSep;
1013 int remainingAlts = 0;
1014 bool checkCase = true;
1015 int numChcased = 0;
1016 int i = 0;
1017 while (i < len) {
1018 QChar c = qstr[i];
1019
1020 if (qnalt && !remainingAlts && QStringView(qstr).mid(pos: i, n: hlen) == head) {
1021 // An alternatives directive is just starting.
1022 i += 2;
1023 if (i >= len) {
1024 break; // malformed directive, bail out
1025 }
1026 // Record alternatives separator, set number of remaining
1027 // alternatives, reactivate case checking.
1028 altSep = qstrcc[i];
1029 remainingAlts = qnalt;
1030 checkCase = true;
1031 } else if (remainingAlts && c == altSep) {
1032 // Alternative separator found, reduce number of remaining
1033 // alternatives and reactivate case checking.
1034 --remainingAlts;
1035 checkCase = true;
1036 } else if (checkCase && c.isLetter()) {
1037 // Case check is active and the character is a letter; change case.
1038 if (toupper) {
1039 qstrcc[i] = c.toUpper();
1040 } else {
1041 qstrcc[i] = c.toLower();
1042 }
1043 ++numChcased;
1044 // No more case checks until next alternatives separator.
1045 checkCase = false;
1046 }
1047
1048 // If any letter has been changed, and there are no more alternatives
1049 // to be processed, we're done.
1050 if (numChcased > 0 && remainingAlts == 0) {
1051 break;
1052 }
1053
1054 // Go to next character.
1055 ++i;
1056 }
1057
1058 return qstrcc;
1059}
1060
1061QJSValue Scriptface::toUpperFirst(const QJSValue &str, const QJSValue &nalt)
1062{
1063 if (!str.isString()) {
1064 return throwError(engine: scriptEngine, SPREF("toUpperFirst: expected string as first argument"));
1065 }
1066 if (!(nalt.isNumber() || nalt.isNull())) {
1067 return throwError(engine: scriptEngine, SPREF("toUpperFirst: expected number as second argument"));
1068 }
1069
1070 QString qstr = str.toString();
1071 int qnalt = nalt.isNull() ? 0 : nalt.toInt();
1072
1073 QString qstruc = toCaseFirst(qstr, qnalt, toupper: true);
1074
1075 return QJSValue(qstruc);
1076}
1077
1078QJSValue Scriptface::toLowerFirst(const QJSValue &str, const QJSValue &nalt)
1079{
1080 if (!str.isString()) {
1081 return throwError(engine: scriptEngine, SPREF("toLowerFirst: expected string as first argument"));
1082 }
1083 if (!(nalt.isNumber() || nalt.isNull())) {
1084 return throwError(engine: scriptEngine, SPREF("toLowerFirst: expected number as second argument"));
1085 }
1086
1087 QString qstr = str.toString();
1088 int qnalt = nalt.isNull() ? 0 : nalt.toInt();
1089
1090 QString qstrlc = toCaseFirst(qstr, qnalt, toupper: false);
1091
1092 return QJSValue(qstrlc);
1093}
1094
1095QJSValue Scriptface::getConfString(const QJSValue &key, const QJSValue &dval)
1096{
1097 if (!key.isString()) {
1098 return throwError(engine: scriptEngine, QStringLiteral("getConfString: expected string as first argument"));
1099 }
1100 if (!(dval.isString() || dval.isNull())) {
1101 return throwError(engine: scriptEngine, SPREF("getConfString: expected string as second argument (when given)"));
1102 }
1103
1104 QString qkey = key.toString();
1105 auto valIt = config.constFind(key: qkey);
1106 if (valIt != config.constEnd()) {
1107 return QJSValue(*valIt);
1108 }
1109
1110 return dval.isNull() ? QJSValue::UndefinedValue : dval;
1111}
1112
1113QJSValue Scriptface::getConfBool(const QJSValue &key, const QJSValue &dval)
1114{
1115 if (!key.isString()) {
1116 return throwError(engine: scriptEngine, SPREF("getConfBool: expected string as first argument"));
1117 }
1118 if (!(dval.isBool() || dval.isNull())) {
1119 return throwError(engine: scriptEngine, SPREF("getConfBool: expected boolean as second argument (when given)"));
1120 }
1121
1122 static QStringList falsities;
1123 if (falsities.isEmpty()) {
1124 falsities.append(t: QString(QLatin1Char('0')));
1125 falsities.append(QStringLiteral("no"));
1126 falsities.append(QStringLiteral("false"));
1127 }
1128
1129 QString qkey = key.toString();
1130 auto valIt = config.constFind(key: qkey);
1131 if (valIt != config.constEnd()) {
1132 QString qval = valIt->toLower();
1133 return QJSValue(!falsities.contains(str: qval));
1134 }
1135
1136 return dval.isNull() ? QJSValue::UndefinedValue : dval;
1137}
1138
1139QJSValue Scriptface::getConfNumber(const QJSValue &key, const QJSValue &dval)
1140{
1141 if (!key.isString()) {
1142 return throwError(engine: scriptEngine,
1143 SPREF("getConfNumber: expected string "
1144 "as first argument"));
1145 }
1146 if (!(dval.isNumber() || dval.isNull())) {
1147 return throwError(engine: scriptEngine,
1148 SPREF("getConfNumber: expected number "
1149 "as second argument (when given)"));
1150 }
1151
1152 QString qkey = key.toString();
1153 auto valIt = config.constFind(key: qkey);
1154 if (valIt != config.constEnd()) {
1155 const QString &qval = *valIt;
1156 bool convOk;
1157 double qnum = qval.toDouble(ok: &convOk);
1158 if (convOk) {
1159 return QJSValue(qnum);
1160 }
1161 }
1162
1163 return dval.isNull() ? QJSValue::UndefinedValue : dval;
1164}
1165
1166// ----------------------------------------------------------------------
1167// Scriptface helpers to interface functions.
1168
1169QJSValue Scriptface::load(const QJSValueList &fnames)
1170{
1171 if (globalKTI()->currentModulePath.isEmpty()) {
1172 return throwError(engine: scriptEngine, SPREF("load: no current module path, aiiie..."));
1173 }
1174
1175 for (int i = 0; i < fnames.size(); ++i) {
1176 if (!fnames[i].isString()) {
1177 return throwError(engine: scriptEngine, SPREF("load: expected string as file name"));
1178 }
1179 }
1180
1181 for (int i = 0; i < fnames.size(); ++i) {
1182 QString qfname = fnames[i].toString();
1183 QString qfpath = globalKTI()->currentModulePath + QLatin1Char('/') + qfname + QLatin1String(".js");
1184
1185 QFile file(qfpath);
1186 if (!file.open(flags: QIODevice::ReadOnly)) {
1187 return throwError(engine: scriptEngine, SPREF("load: cannot read file '%1'").arg(a: qfpath));
1188 }
1189
1190 QTextStream stream(&file);
1191 QString source = stream.readAll();
1192 file.close();
1193
1194 QJSValue comp = scriptEngine->evaluate(program: source, fileName: qfpath, lineNumber: 0);
1195
1196 if (comp.isError()) {
1197 QString msg = comp.toString();
1198
1199 QString line;
1200 if (comp.isObject()) {
1201 QJSValue lval = comp.property(QStringLiteral("line"));
1202 if (lval.isNumber()) {
1203 line = QString::number(lval.toInt());
1204 }
1205 }
1206
1207 return throwError(engine: scriptEngine, QStringLiteral("at %1:%2: %3").arg(args&: qfpath, args&: line, args&: msg));
1208 }
1209 dbgout(str: "Loaded module: %1", a1: qfpath);
1210 }
1211 return QJSValue::UndefinedValue;
1212}
1213
1214QString Scriptface::loadProps_text(const QString &fpath)
1215{
1216 QFile file(fpath);
1217 if (!file.open(flags: QIODevice::ReadOnly)) {
1218 return SPREF("loadProps_text: cannot read file '%1'").arg(a: fpath);
1219 }
1220 QTextStream stream(&file);
1221 QString s = stream.readAll();
1222 file.close();
1223
1224 // Parse the map.
1225 // Should care about performance: possibly executed on each KDE
1226 // app startup and reading houndreds of thousands of characters.
1227 enum { s_nextEntry, s_nextKey, s_nextValue };
1228 QList<QByteArray> ekeys; // holds keys for current entry
1229 QHash<QByteArray, QByteArray> props; // holds properties for current entry
1230 int slen = s.length();
1231 int state = s_nextEntry;
1232 QByteArray pkey;
1233 QChar prop_sep;
1234 QChar key_sep;
1235 int i = 0;
1236 while (1) {
1237 int i_checkpoint = i;
1238
1239 if (state == s_nextEntry) {
1240 while (s[i].isSpace()) {
1241 ++i;
1242 if (i >= slen) {
1243 goto END_PROP_PARSE;
1244 }
1245 }
1246 if (i + 1 >= slen) {
1247 return SPREF("loadProps_text: unexpected end of file in %1").arg(a: fpath);
1248 }
1249 if (s[i] != QLatin1Char('#')) {
1250 // Separator characters for this entry.
1251 key_sep = s[i];
1252 prop_sep = s[i + 1];
1253 if (key_sep.isLetter() || prop_sep.isLetter()) {
1254 return SPREF("loadProps_text: separator characters must not be letters at %1:%2").arg(a: fpath).arg(a: countLines(s, p: i));
1255 }
1256
1257 // Reset all data for current entry.
1258 ekeys.clear();
1259 props.clear();
1260 pkey.clear();
1261
1262 i += 2;
1263 state = s_nextKey;
1264 } else {
1265 // This is a comment, skip to EOL, don't change state.
1266 while (s[i] != QLatin1Char('\n')) {
1267 ++i;
1268 if (i >= slen) {
1269 goto END_PROP_PARSE;
1270 }
1271 }
1272 }
1273 } else if (state == s_nextKey) {
1274 int ip = i;
1275 // Proceed up to next key or property separator.
1276 while (s[i] != key_sep && s[i] != prop_sep) {
1277 ++i;
1278 if (i >= slen) {
1279 goto END_PROP_PARSE;
1280 }
1281 }
1282 if (s[i] == key_sep) {
1283 // This is a property key,
1284 // record for when the value gets parsed.
1285 pkey = normKeystr(raw: s.mid(position: ip, n: i - ip), mayHaveAcc: false);
1286
1287 i += 1;
1288 state = s_nextValue;
1289 } else { // if (s[i] == prop_sep) {
1290 // This is an entry key, or end of entry.
1291 QByteArray ekey = normKeystr(raw: s.mid(position: ip, n: i - ip), mayHaveAcc: false);
1292 if (!ekey.isEmpty()) {
1293 // An entry key.
1294 ekeys.append(t: ekey);
1295
1296 i += 1;
1297 state = s_nextKey;
1298 } else {
1299 // End of entry.
1300 if (ekeys.size() < 1) {
1301 return SPREF("loadProps_text: no entry key for entry ending at %1:%2").arg(a: fpath).arg(a: countLines(s, p: i));
1302 }
1303
1304 // Add collected entry into global store,
1305 // once for each entry key (QHash implicitly shared).
1306 for (const QByteArray &ekey : std::as_const(t&: ekeys)) {
1307 phraseProps[ekey] = props;
1308 }
1309
1310 i += 1;
1311 state = s_nextEntry;
1312 }
1313 }
1314 } else if (state == s_nextValue) {
1315 int ip = i;
1316 // Proceed up to next property separator.
1317 while (s[i] != prop_sep) {
1318 ++i;
1319 if (i >= slen) {
1320 goto END_PROP_PARSE;
1321 }
1322 if (s[i] == key_sep) {
1323 return SPREF("loadProps_text: property separator inside property value at %1:%2").arg(a: fpath).arg(a: countLines(s, p: i));
1324 }
1325 }
1326 // Extract the property value and store the property.
1327 QByteArray pval = trimSmart(raw: s.mid(position: ip, n: i - ip)).toUtf8();
1328 props[pkey] = pval;
1329
1330 i += 1;
1331 state = s_nextKey;
1332 } else {
1333 return SPREF("loadProps: internal error 10 at %1:%2").arg(a: fpath).arg(a: countLines(s, p: i));
1334 }
1335
1336 // To avoid infinite looping and stepping out.
1337 if (i == i_checkpoint || i >= slen) {
1338 return SPREF("loadProps: internal error 20 at %1:%2").arg(a: fpath).arg(a: countLines(s, p: i));
1339 }
1340 }
1341
1342END_PROP_PARSE:
1343
1344 if (state != s_nextEntry) {
1345 return SPREF("loadProps: unexpected end of file in %1").arg(a: fpath);
1346 }
1347
1348 return QString();
1349}
1350
1351// Read big-endian integer of nbytes length at position pos
1352// in character array fc of length len.
1353// Update position to point after the number.
1354// In case of error, pos is set to -1.
1355template<typename T>
1356static int bin_read_int_nbytes(const char *fc, qlonglong len, qlonglong &pos, int nbytes)
1357{
1358 if (pos + nbytes > len) {
1359 pos = -1;
1360 return 0;
1361 }
1362 T num = qFromBigEndian<T>((uchar *)fc + pos);
1363 pos += nbytes;
1364 return num;
1365}
1366
1367// Read 64-bit big-endian integer.
1368static quint64 bin_read_int64(const char *fc, qlonglong len, qlonglong &pos)
1369{
1370 return bin_read_int_nbytes<quint64>(fc, len, pos, nbytes: 8);
1371}
1372
1373// Read 32-bit big-endian integer.
1374static quint32 bin_read_int(const char *fc, qlonglong len, qlonglong &pos)
1375{
1376 return bin_read_int_nbytes<quint32>(fc, len, pos, nbytes: 4);
1377}
1378
1379// Read string at position pos of character array fc of length n.
1380// String is represented as 32-bit big-endian byte length followed by bytes.
1381// Update position to point after the string.
1382// In case of error, pos is set to -1.
1383static QByteArray bin_read_string(const char *fc, qlonglong len, qlonglong &pos)
1384{
1385 // Binary format stores strings as length followed by byte sequence.
1386 // No null-termination.
1387 int nbytes = bin_read_int(fc, len, pos);
1388 if (pos < 0) {
1389 return QByteArray();
1390 }
1391 if (nbytes < 0 || pos + nbytes > len) {
1392 pos = -1;
1393 return QByteArray();
1394 }
1395 QByteArray s(fc + pos, nbytes);
1396 pos += nbytes;
1397 return s;
1398}
1399
1400QString Scriptface::loadProps_bin(const QString &fpath)
1401{
1402 QFile file(fpath);
1403 if (!file.open(flags: QIODevice::ReadOnly)) {
1404 return SPREF("loadProps: cannot read file '%1'").arg(a: fpath);
1405 }
1406 // Collect header.
1407 QByteArray head(8, '0');
1408 file.read(data: head.data(), maxlen: head.size());
1409 file.close();
1410
1411 // Choose pmap loader based on header.
1412 if (head == "TSPMAP00") {
1413 return loadProps_bin_00(fpath);
1414 } else if (head == "TSPMAP01") {
1415 return loadProps_bin_01(fpath);
1416 } else {
1417 return SPREF("loadProps: unknown version of compiled map '%1'").arg(a: fpath);
1418 }
1419}
1420
1421QString Scriptface::loadProps_bin_00(const QString &fpath)
1422{
1423 QFile file(fpath);
1424 if (!file.open(flags: QIODevice::ReadOnly)) {
1425 return SPREF("loadProps: cannot read file '%1'").arg(a: fpath);
1426 }
1427 QByteArray fctmp = file.readAll();
1428 file.close();
1429 const char *fc = fctmp.data();
1430 const int fclen = fctmp.size();
1431
1432 // Indicates stream state.
1433 qlonglong pos = 0;
1434
1435 // Match header.
1436 QByteArray head(fc, 8);
1437 pos += 8;
1438 if (head != "TSPMAP00") {
1439 goto END_PROP_PARSE;
1440 }
1441
1442 // Read total number of entries.
1443 int nentries;
1444 nentries = bin_read_int(fc, len: fclen, pos);
1445 if (pos < 0) {
1446 goto END_PROP_PARSE;
1447 }
1448
1449 // Read all entries.
1450 for (int i = 0; i < nentries; ++i) {
1451 // Read number of entry keys and all entry keys.
1452 QList<QByteArray> ekeys;
1453 int nekeys = bin_read_int(fc, len: fclen, pos);
1454 if (pos < 0) {
1455 goto END_PROP_PARSE;
1456 }
1457 ekeys.reserve(asize: nekeys); // nekeys are appended if data is not corrupted
1458 for (int j = 0; j < nekeys; ++j) {
1459 QByteArray ekey = bin_read_string(fc, len: fclen, pos);
1460 if (pos < 0) {
1461 goto END_PROP_PARSE;
1462 }
1463 ekeys.append(t: ekey);
1464 }
1465 // dbgout("--------> ekey[0]={%1}", QString::fromUtf8(ekeys[0]));
1466
1467 // Read number of properties and all properties.
1468 QHash<QByteArray, QByteArray> props;
1469 int nprops = bin_read_int(fc, len: fclen, pos);
1470 if (pos < 0) {
1471 goto END_PROP_PARSE;
1472 }
1473 for (int j = 0; j < nprops; ++j) {
1474 QByteArray pkey = bin_read_string(fc, len: fclen, pos);
1475 if (pos < 0) {
1476 goto END_PROP_PARSE;
1477 }
1478 QByteArray pval = bin_read_string(fc, len: fclen, pos);
1479 if (pos < 0) {
1480 goto END_PROP_PARSE;
1481 }
1482 props[pkey] = pval;
1483 }
1484
1485 // Add collected entry into global store,
1486 // once for each entry key (QHash implicitly shared).
1487 for (const QByteArray &ekey : std::as_const(t&: ekeys)) {
1488 phraseProps[ekey] = props;
1489 }
1490 }
1491
1492END_PROP_PARSE:
1493
1494 if (pos < 0) {
1495 return SPREF("loadProps: corrupt compiled map '%1'").arg(a: fpath);
1496 }
1497
1498 return QString();
1499}
1500
1501QString Scriptface::loadProps_bin_01(const QString &fpath)
1502{
1503 QFile *file = new QFile(fpath);
1504 if (!file->open(flags: QIODevice::ReadOnly)) {
1505 return SPREF("loadProps: cannot read file '%1'").arg(a: fpath);
1506 }
1507
1508 QByteArray fstr;
1509 qlonglong pos;
1510
1511 // Read the header and number and length of entry keys.
1512 fstr = file->read(maxlen: 8 + 4 + 8);
1513 pos = 0;
1514 QByteArray head = fstr.left(len: 8);
1515 pos += 8;
1516 if (head != "TSPMAP01") {
1517 return SPREF("loadProps: corrupt compiled map '%1'").arg(a: fpath);
1518 }
1519 quint32 numekeys = bin_read_int(fc: fstr, len: fstr.size(), pos);
1520 quint64 lenekeys = bin_read_int64(fc: fstr, len: fstr.size(), pos);
1521
1522 // Read entry keys.
1523 fstr = file->read(maxlen: lenekeys);
1524 pos = 0;
1525 for (quint32 i = 0; i < numekeys; ++i) {
1526 QByteArray ekey = bin_read_string(fc: fstr, len: lenekeys, pos);
1527 quint64 offset = bin_read_int64(fc: fstr, len: lenekeys, pos);
1528 phraseUnparsedProps[ekey] = {.pmapFile: file, .offset: offset};
1529 }
1530
1531 // // Read property keys.
1532 // ...when it becomes necessary
1533
1534 loadedPmapHandles.insert(value: file);
1535 return QString();
1536}
1537
1538QHash<QByteArray, QByteArray> Scriptface::resolveUnparsedProps(const QByteArray &phrase)
1539{
1540 auto [file, offset] = phraseUnparsedProps.value(key: phrase);
1541 QHash<QByteArray, QByteArray> props;
1542 if (file && file->seek(offset)) {
1543 QByteArray fstr = file->read(maxlen: 4 + 4);
1544 qlonglong pos = 0;
1545 quint32 numpkeys = bin_read_int(fc: fstr, len: fstr.size(), pos);
1546 quint32 lenpkeys = bin_read_int(fc: fstr, len: fstr.size(), pos);
1547 fstr = file->read(maxlen: lenpkeys);
1548 pos = 0;
1549 for (quint32 i = 0; i < numpkeys; ++i) {
1550 QByteArray pkey = bin_read_string(fc: fstr, len: lenpkeys, pos);
1551 QByteArray pval = bin_read_string(fc: fstr, len: lenpkeys, pos);
1552 props[pkey] = pval;
1553 }
1554 phraseProps[phrase] = props;
1555 phraseUnparsedProps.remove(key: phrase);
1556 }
1557 return props;
1558}
1559
1560#include "ktranscript.moc"
1561

source code of ki18n/src/i18n/ktranscript.cpp