KI18n

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

KDE's Doxygen guidelines are available online.