File indexing completed on 2024-04-28 15:25:21

0001 /*  This file is part of the KDE libraries
0002     SPDX-FileCopyrightText: 2007 Chusslove Illich <caslav.ilic@gmx.net>
0003     SPDX-FileCopyrightText: 2014 Kevin Krammer <krammer@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include <common_helpers_p.h>
0009 #include <ktranscript_p.h>
0010 
0011 #include <ktranscript_export.h>
0012 
0013 //#include <unistd.h>
0014 
0015 #include <QJSEngine>
0016 
0017 #include <QDebug>
0018 #include <QDir>
0019 #include <QFile>
0020 #include <QHash>
0021 #include <QIODevice>
0022 #include <QJSValueIterator>
0023 #include <QList>
0024 #include <QSet>
0025 #include <QStandardPaths>
0026 #include <QStringList>
0027 #include <QTextStream>
0028 #include <QVariant>
0029 #include <qendian.h>
0030 
0031 class KTranscriptImp;
0032 class Scriptface;
0033 
0034 typedef QHash<QString, QString> TsConfigGroup;
0035 typedef QHash<QString, TsConfigGroup> TsConfig;
0036 
0037 // Transcript implementation (used as singleton).
0038 class KTranscriptImp : public KTranscript
0039 {
0040 public:
0041     KTranscriptImp();
0042     ~KTranscriptImp() override;
0043 
0044     QString eval(const QList<QVariant> &argv,
0045                  const QString &lang,
0046                  const QString &ctry,
0047                  const QString &msgctxt,
0048                  const QHash<QString, QString> &dynctxt,
0049                  const QString &msgid,
0050                  const QStringList &subs,
0051                  const QList<QVariant> &vals,
0052                  const QString &ftrans,
0053                  QList<QStringList> &mods,
0054                  QString &error,
0055                  bool &fallback) override;
0056 
0057     QStringList postCalls(const QString &lang) override;
0058 
0059     // Lexical path of the module for the executing code.
0060     QString currentModulePath;
0061 
0062 private:
0063     void loadModules(const QList<QStringList> &mods, QString &error);
0064     void setupInterpreter(const QString &lang);
0065 
0066     TsConfig config;
0067 
0068     QHash<QString, Scriptface *> m_sface;
0069 };
0070 
0071 // Script-side transcript interface.
0072 class Scriptface : public QObject
0073 {
0074     Q_OBJECT
0075 public:
0076     explicit Scriptface(const TsConfigGroup &config, QObject *parent = nullptr);
0077     ~Scriptface();
0078 
0079     // Interface functions.
0080     Q_INVOKABLE QJSValue load(const QString &name);
0081     Q_INVOKABLE QJSValue setcall(const QJSValue &name, const QJSValue &func, const QJSValue &fval = QJSValue::NullValue);
0082     Q_INVOKABLE QJSValue hascall(const QString &name);
0083     Q_INVOKABLE QJSValue acallInternal(const QJSValue &args);
0084     Q_INVOKABLE QJSValue setcallForall(const QJSValue &name, const QJSValue &func, const QJSValue &fval = QJSValue::NullValue);
0085     Q_INVOKABLE QJSValue fallback();
0086     Q_INVOKABLE QJSValue nsubs();
0087     Q_INVOKABLE QJSValue subs(const QJSValue &index);
0088     Q_INVOKABLE QJSValue vals(const QJSValue &index);
0089     Q_INVOKABLE QJSValue msgctxt();
0090     Q_INVOKABLE QJSValue dynctxt(const QString &key);
0091     Q_INVOKABLE QJSValue msgid();
0092     Q_INVOKABLE QJSValue msgkey();
0093     Q_INVOKABLE QJSValue msgstrf();
0094     Q_INVOKABLE void dbgputs(const QString &str);
0095     Q_INVOKABLE void warnputs(const QString &str);
0096     Q_INVOKABLE QJSValue localeCountry();
0097     Q_INVOKABLE QJSValue normKey(const QJSValue &phrase);
0098     Q_INVOKABLE QJSValue loadProps(const QString &name);
0099     Q_INVOKABLE QJSValue getProp(const QJSValue &phrase, const QJSValue &prop);
0100     Q_INVOKABLE QJSValue setProp(const QJSValue &phrase, const QJSValue &prop, const QJSValue &value);
0101     Q_INVOKABLE QJSValue toUpperFirst(const QJSValue &str, const QJSValue &nalt = QJSValue::NullValue);
0102     Q_INVOKABLE QJSValue toLowerFirst(const QJSValue &str, const QJSValue &nalt = QJSValue::NullValue);
0103     Q_INVOKABLE QJSValue getConfString(const QJSValue &key, const QJSValue &dval = QJSValue::NullValue);
0104     Q_INVOKABLE QJSValue getConfBool(const QJSValue &key, const QJSValue &dval = QJSValue::NullValue);
0105     Q_INVOKABLE QJSValue getConfNumber(const QJSValue &key, const QJSValue &dval = QJSValue::NullValue);
0106 
0107     // Helper methods to interface functions.
0108     QJSValue load(const QJSValueList &names);
0109     QJSValue loadProps(const QJSValueList &names);
0110     QString loadProps_text(const QString &fpath);
0111     QString loadProps_bin(const QString &fpath);
0112     QString loadProps_bin_00(const QString &fpath);
0113     QString loadProps_bin_01(const QString &fpath);
0114 
0115     void put(const QString &propertyName, const QJSValue &value);
0116 
0117     // Link to its script engine
0118     QJSEngine *const scriptEngine;
0119 
0120     // Current message data.
0121     const QString *msgcontext;
0122     const QHash<QString, QString> *dyncontext;
0123     const QString *msgId;
0124     const QStringList *subList;
0125     const QList<QVariant> *valList;
0126     const QString *ftrans;
0127     const QString *ctry;
0128 
0129     // Fallback request handle.
0130     bool *fallbackRequest;
0131 
0132     // Function register.
0133     QHash<QString, QJSValue> funcs;
0134     QHash<QString, QJSValue> fvals;
0135     QHash<QString, QString> fpaths;
0136 
0137     // Ordering of those functions which execute for all messages.
0138     QList<QString> nameForalls;
0139 
0140     // Property values per phrase (used by *Prop interface calls).
0141     // Not QStrings, in order to avoid conversion from UTF-8 when
0142     // loading compiled maps (less latency on startup).
0143     QHash<QByteArray, QHash<QByteArray, QByteArray>> phraseProps;
0144     // Unresolved property values per phrase,
0145     // containing the pointer to compiled pmap file handle and offset in it.
0146     struct UnparsedPropInfo {
0147         QFile *pmapFile = nullptr;
0148         quint64 offset = -1;
0149     };
0150     QHash<QByteArray, UnparsedPropInfo> phraseUnparsedProps;
0151     QHash<QByteArray, QByteArray> resolveUnparsedProps(const QByteArray &phrase);
0152     // Set of loaded pmap files by paths and file handle pointers.
0153     QSet<QString> loadedPmapPaths;
0154     QSet<QFile *> loadedPmapHandles;
0155 
0156     // User config.
0157     TsConfigGroup config;
0158 };
0159 
0160 // ----------------------------------------------------------------------
0161 // Custom debug and warning output (kdebug not available)
0162 #define DBGP "KTranscript: "
0163 void dbgout(const char *str)
0164 {
0165 #ifndef NDEBUG
0166     fprintf(stderr, DBGP "%s\n", str);
0167 #else
0168     Q_UNUSED(str);
0169 #endif
0170 }
0171 template<typename T1>
0172 void dbgout(const char *str, const T1 &a1)
0173 {
0174 #ifndef NDEBUG
0175     fprintf(stderr, DBGP "%s\n", QString::fromUtf8(str).arg(a1).toLocal8Bit().data());
0176 #else
0177     Q_UNUSED(str);
0178     Q_UNUSED(a1);
0179 #endif
0180 }
0181 template<typename T1, typename T2>
0182 void dbgout(const char *str, const T1 &a1, const T2 &a2)
0183 {
0184 #ifndef NDEBUG
0185     fprintf(stderr, DBGP "%s\n", QString::fromUtf8(str).arg(a1).arg(a2).toLocal8Bit().data());
0186 #else
0187     Q_UNUSED(str);
0188     Q_UNUSED(a1);
0189     Q_UNUSED(a2);
0190 #endif
0191 }
0192 template<typename T1, typename T2, typename T3>
0193 void dbgout(const char *str, const T1 &a1, const T2 &a2, const T3 &a3)
0194 {
0195 #ifndef NDEBUG
0196     fprintf(stderr, DBGP "%s\n", QString::fromUtf8(str).arg(a1).arg(a2).arg(a3).toLocal8Bit().data());
0197 #else
0198     Q_UNUSED(str);
0199     Q_UNUSED(a1);
0200     Q_UNUSED(a2);
0201     Q_UNUSED(a3);
0202 #endif
0203 }
0204 
0205 #define WARNP "KTranscript: "
0206 void warnout(const char *str)
0207 {
0208     fprintf(stderr, WARNP "%s\n", str);
0209 }
0210 template<typename T1>
0211 void warnout(const char *str, const T1 &a1)
0212 {
0213     fprintf(stderr, WARNP "%s\n", QString::fromUtf8(str).arg(a1).toLocal8Bit().data());
0214 }
0215 
0216 // ----------------------------------------------------------------------
0217 // Produces a string out of a script exception.
0218 
0219 QString expt2str(const QJSValue &expt)
0220 {
0221     if (expt.isError()) {
0222         const QJSValue message = expt.property(QStringLiteral("message"));
0223         if (!message.isUndefined()) {
0224             return QStringLiteral("Error: %1").arg(message.toString());
0225         }
0226     }
0227 
0228     QString strexpt = expt.toString();
0229     return QStringLiteral("Caught exception: %1").arg(strexpt);
0230 }
0231 
0232 // ----------------------------------------------------------------------
0233 // Count number of lines in the string,
0234 // up to and excluding the requested position.
0235 int countLines(const QString &s, int p)
0236 {
0237     int n = 1;
0238     int len = s.length();
0239     for (int i = 0; i < p && i < len; ++i) {
0240         if (s[i] == QLatin1Char('\n')) {
0241             ++n;
0242         }
0243     }
0244     return n;
0245 }
0246 
0247 // ----------------------------------------------------------------------
0248 // Normalize string key for hash lookups,
0249 QByteArray normKeystr(const QString &raw, bool mayHaveAcc = true)
0250 {
0251     // NOTE: Regexes should not be used here for performance reasons.
0252     // This function may potentially be called thousands of times
0253     // on application startup.
0254 
0255     QString key = raw;
0256 
0257     // Strip all whitespace.
0258     int len = key.length();
0259     QString nkey;
0260     for (int i = 0; i < len; ++i) {
0261         QChar c = key[i];
0262         if (!c.isSpace()) {
0263             nkey.append(c);
0264         }
0265     }
0266     key = nkey;
0267 
0268     // Strip accelerator marker.
0269     if (mayHaveAcc) {
0270         key = removeAcceleratorMarker(key);
0271     }
0272 
0273     // Convert to lower case.
0274     key = key.toLower();
0275 
0276     return key.toUtf8();
0277 }
0278 
0279 // ----------------------------------------------------------------------
0280 // Trim multiline string in a "smart" way:
0281 // Remove leading and trailing whitespace up to and including first
0282 // newline from that side, if there is one; otherwise, don't touch.
0283 QString trimSmart(const QString &raw)
0284 {
0285     // NOTE: This could be done by a single regex, but is not due to
0286     // performance reasons.
0287     // This function may potentially be called thousands of times
0288     // on application startup.
0289 
0290     int len = raw.length();
0291 
0292     int is = 0;
0293     while (is < len && raw[is].isSpace() && raw[is] != QLatin1Char('\n')) {
0294         ++is;
0295     }
0296     if (is >= len || raw[is] != QLatin1Char('\n')) {
0297         is = -1;
0298     }
0299 
0300     int ie = len - 1;
0301     while (ie >= 0 && raw[ie].isSpace() && raw[ie] != QLatin1Char('\n')) {
0302         --ie;
0303     }
0304     if (ie < 0 || raw[ie] != QLatin1Char('\n')) {
0305         ie = len;
0306     }
0307 
0308     return raw.mid(is + 1, ie - is - 1);
0309 }
0310 
0311 // ----------------------------------------------------------------------
0312 // Produce a JavaScript object out of Qt variant.
0313 
0314 QJSValue variantToJsValue(const QVariant &val)
0315 {
0316     QVariant::Type vtype = val.type();
0317     if (vtype == QVariant::String) {
0318         return QJSValue(val.toString());
0319     } else if (vtype == QVariant::Bool) {
0320         return QJSValue(val.toBool());
0321     } else if (vtype == QVariant::Double //
0322                || vtype == QVariant::Int //
0323                || vtype == QVariant::UInt //
0324                || vtype == QVariant::LongLong //
0325                || vtype == QVariant::ULongLong) {
0326         return QJSValue(val.toDouble());
0327     } else {
0328         return QJSValue::UndefinedValue;
0329     }
0330 }
0331 
0332 // ----------------------------------------------------------------------
0333 // Parse ini-style config file,
0334 // returning content as hash of hashes by group and key.
0335 // Parsing is not fussy, it will read what it can.
0336 TsConfig readConfig(const QString &fname)
0337 {
0338     TsConfig config;
0339     // Add empty group.
0340     TsConfig::iterator configGroup;
0341     configGroup = config.insert(QString(), TsConfigGroup());
0342 
0343     QFile file(fname);
0344     if (!file.open(QIODevice::ReadOnly)) {
0345         return config;
0346     }
0347     QTextStream stream(&file);
0348 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0349     stream.setCodec("UTF-8");
0350 #endif
0351     while (!stream.atEnd()) {
0352         QString line = stream.readLine();
0353         int p1;
0354         int p2;
0355 
0356         // Remove comment from the line.
0357         p1 = line.indexOf(QLatin1Char('#'));
0358         if (p1 >= 0) {
0359             line.truncate(p1);
0360         }
0361         line = line.trimmed();
0362         if (line.isEmpty()) {
0363             continue;
0364         }
0365 
0366         if (line[0] == QLatin1Char('[')) {
0367             // Group switch.
0368             p1 = 0;
0369             p2 = line.indexOf(QLatin1Char(']'), p1 + 1);
0370             if (p2 < 0) {
0371                 continue;
0372             }
0373             QString group = line.mid(p1 + 1, p2 - p1 - 1).trimmed();
0374             configGroup = config.find(group);
0375             if (configGroup == config.end()) {
0376                 // Add new group.
0377                 configGroup = config.insert(group, TsConfigGroup());
0378             }
0379         } else {
0380             // Field.
0381             p1 = line.indexOf(QLatin1Char('='));
0382             if (p1 < 0) {
0383                 continue;
0384             }
0385 
0386             const QStringView lineView(line);
0387             const QStringView field = lineView.left(p1).trimmed();
0388             if (!field.isEmpty()) {
0389                 const QStringView value = lineView.mid(p1 + 1).trimmed();
0390                 (*configGroup)[field.toString()] = value.toString();
0391             }
0392         }
0393     }
0394     file.close();
0395 
0396     return config;
0397 }
0398 
0399 // ----------------------------------------------------------------------
0400 // throw or log error, depending on context availability
0401 static QJSValue throwError(QJSEngine *engine, const QString &message)
0402 {
0403     if (engine) {
0404         return engine->evaluate(QStringLiteral("new Error(%1)").arg(message));
0405     }
0406 
0407     qCritical() << "Script error" << message;
0408     return QJSValue::UndefinedValue;
0409 }
0410 
0411 #ifdef KTRANSCRIPT_TESTBUILD
0412 
0413 // ----------------------------------------------------------------------
0414 // Test build creation/destruction hooks
0415 static KTranscriptImp *s_transcriptInstance = nullptr;
0416 
0417 KTranscriptImp *globalKTI()
0418 {
0419     return s_transcriptInstance;
0420 }
0421 
0422 KTranscript *autotestCreateKTranscriptImp()
0423 {
0424     Q_ASSERT(s_transcriptInstance == nullptr);
0425     s_transcriptInstance = new KTranscriptImp;
0426     return s_transcriptInstance;
0427 }
0428 
0429 void autotestDestroyKTranscriptImp()
0430 {
0431     Q_ASSERT(s_transcriptInstance != nullptr);
0432     delete s_transcriptInstance;
0433     s_transcriptInstance = nullptr;
0434 }
0435 
0436 #else
0437 
0438 // ----------------------------------------------------------------------
0439 // Dynamic loading.
0440 Q_GLOBAL_STATIC(KTranscriptImp, globalKTI)
0441 extern "C" {
0442 KTRANSCRIPT_EXPORT KTranscript *load_transcript()
0443 {
0444     return globalKTI();
0445 }
0446 }
0447 #endif
0448 
0449 // ----------------------------------------------------------------------
0450 // KTranscript definitions.
0451 
0452 KTranscriptImp::KTranscriptImp()
0453 {
0454     // Load user configuration.
0455 
0456     QString tsConfigPath = QStandardPaths::locate(QStandardPaths::ConfigLocation, QStringLiteral("ktranscript.ini"));
0457     if (tsConfigPath.isEmpty()) {
0458         tsConfigPath = QDir::homePath() + QLatin1Char('/') + QLatin1String(".transcriptrc");
0459     }
0460     config = readConfig(tsConfigPath);
0461 }
0462 
0463 KTranscriptImp::~KTranscriptImp()
0464 {
0465     qDeleteAll(m_sface);
0466 }
0467 
0468 QString KTranscriptImp::eval(const QList<QVariant> &argv,
0469                              const QString &lang,
0470                              const QString &ctry,
0471                              const QString &msgctxt,
0472                              const QHash<QString, QString> &dynctxt,
0473                              const QString &msgid,
0474                              const QStringList &subs,
0475                              const QList<QVariant> &vals,
0476                              const QString &ftrans,
0477                              QList<QStringList> &mods,
0478                              QString &error,
0479                              bool &fallback)
0480 {
0481     // error = "debug"; return QString();
0482 
0483     error.clear(); // empty error message means successful evaluation
0484     fallback = false; // fallback not requested
0485 
0486 #if 0
0487     // FIXME: Maybe not needed, as QJSEngine has no native outside access?
0488     // Unportable (needs unistd.h)?
0489 
0490     // If effective user id is root and real user id is not root.
0491     if (geteuid() == 0 && getuid() != 0) {
0492         // Since scripts are user input, and the program is running with
0493         // root permissions while real user is not root, do not invoke
0494         // scripting at all, to prevent exploits.
0495         error = "Security block: trying to execute a script in suid environment.";
0496         return QString();
0497     }
0498 #endif
0499 
0500     // Load any new modules and clear the list.
0501     if (!mods.isEmpty()) {
0502         loadModules(mods, error);
0503         mods.clear();
0504         if (!error.isEmpty()) {
0505             return QString();
0506         }
0507     }
0508 
0509     // Add interpreters for new languages.
0510     // (though it should never happen here, but earlier when loading modules;
0511     // this also means there are no calls set, so the unregistered call error
0512     // below will be reported).
0513     if (!m_sface.contains(lang)) {
0514         setupInterpreter(lang);
0515     }
0516 
0517     // Shortcuts.
0518     Scriptface *sface = m_sface[lang];
0519 
0520     QJSEngine *engine = sface->scriptEngine;
0521     QJSValue gobj = engine->globalObject();
0522 
0523     // Link current message data for script-side interface.
0524     sface->msgcontext = &msgctxt;
0525     sface->dyncontext = &dynctxt;
0526     sface->msgId = &msgid;
0527     sface->subList = &subs;
0528     sface->valList = &vals;
0529     sface->ftrans = &ftrans;
0530     sface->fallbackRequest = &fallback;
0531     sface->ctry = &ctry;
0532 
0533     // Find corresponding JS function.
0534     int argc = argv.size();
0535     if (argc < 1) {
0536         // error = "At least the call name must be supplied.";
0537         // Empty interpolation is OK, possibly used just to initialize
0538         // at a given point (e.g. for Ts.setForall() to start having effect).
0539         return QString();
0540     }
0541     QString funcName = argv[0].toString();
0542     if (!sface->funcs.contains(funcName)) {
0543         error = QStringLiteral("Unregistered call to '%1'.").arg(funcName);
0544         return QString();
0545     }
0546 
0547     QJSValue func = sface->funcs[funcName];
0548     QJSValue fval = sface->fvals[funcName];
0549 
0550     // Recover module path from the time of definition of this call,
0551     // for possible load calls.
0552     currentModulePath = sface->fpaths[funcName];
0553 
0554     // Execute function.
0555     QJSValueList arglist;
0556     arglist.reserve(argc - 1);
0557     for (int i = 1; i < argc; ++i) {
0558         arglist.append(engine->toScriptValue(argv[i]));
0559     }
0560 
0561     QJSValue val;
0562     if (fval.isObject()) {
0563         val = func.callWithInstance(fval, arglist);
0564     } else { // no object associated to this function, use global
0565         val = func.callWithInstance(gobj, arglist);
0566     }
0567 
0568     if (fallback) {
0569         // Fallback to ordinary translation requested.
0570         return QString();
0571     } else if (!val.isError()) {
0572         // Evaluation successful.
0573 
0574         if (val.isString()) {
0575             // Good to go.
0576 
0577             return val.toString();
0578         } else {
0579             // Accept only strings.
0580 
0581             QString strval = val.toString();
0582             error = QStringLiteral("Non-string return value: %1").arg(strval);
0583             return QString();
0584         }
0585     } else {
0586         // Exception raised.
0587 
0588         error = expt2str(val);
0589 
0590         return QString();
0591     }
0592 }
0593 
0594 QStringList KTranscriptImp::postCalls(const QString &lang)
0595 {
0596     // Return no calls if scripting was not already set up for this language.
0597     // NOTE: This shouldn't happen, as postCalls cannot be called in such case.
0598     if (!m_sface.contains(lang)) {
0599         return QStringList();
0600     }
0601 
0602     // Shortcuts.
0603     Scriptface *sface = m_sface[lang];
0604 
0605     return sface->nameForalls;
0606 }
0607 
0608 void KTranscriptImp::loadModules(const QList<QStringList> &mods, QString &error)
0609 {
0610     QList<QString> modErrors;
0611 
0612     for (const QStringList &mod : mods) {
0613         QString mpath = mod[0];
0614         QString mlang = mod[1];
0615 
0616         // Add interpreters for new languages.
0617         if (!m_sface.contains(mlang)) {
0618             setupInterpreter(mlang);
0619         }
0620 
0621         // Setup current module path for loading submodules.
0622         // (sort of closure over invocations of loadf)
0623         int posls = mpath.lastIndexOf(QLatin1Char('/'));
0624         if (posls < 1) {
0625             modErrors.append(QStringLiteral("Funny module path '%1', skipping.").arg(mpath));
0626             continue;
0627         }
0628         currentModulePath = mpath.left(posls);
0629         QString fname = mpath.mid(posls + 1);
0630         // Scriptface::loadf() wants no extension on the filename
0631         fname = fname.left(fname.lastIndexOf(QLatin1Char('.')));
0632 
0633         // Load the module.
0634         QJSValueList alist;
0635         alist.append(QJSValue(fname));
0636 
0637         m_sface[mlang]->load(alist);
0638     }
0639 
0640     // Unset module path.
0641     currentModulePath.clear();
0642 
0643     for (const QString &merr : std::as_const(modErrors)) {
0644         error.append(merr + QLatin1Char('\n'));
0645     }
0646 }
0647 
0648 #define SFNAME "Ts"
0649 void KTranscriptImp::setupInterpreter(const QString &lang)
0650 {
0651     // Add scripting interface
0652     // Creates its own script engine and registers with it
0653     // NOTE: Config may not contain an entry for the language, in which case
0654     // it is automatically constructed as an empty hash. This is intended.
0655     Scriptface *sface = new Scriptface(config[lang]);
0656 
0657     // Store scriptface
0658     m_sface[lang] = sface;
0659 
0660     // dbgout("=====> Created interpreter for '%1'", lang);
0661 }
0662 
0663 Scriptface::Scriptface(const TsConfigGroup &config_, QObject *parent)
0664     : QObject(parent)
0665     , scriptEngine(new QJSEngine)
0666     , fallbackRequest(nullptr)
0667     , config(config_)
0668 {
0669     QJSValue object = scriptEngine->newQObject(this);
0670     scriptEngine->globalObject().setProperty(QStringLiteral(SFNAME), object);
0671     scriptEngine->evaluate(QStringLiteral("Ts.acall = function() { return Ts.acallInternal(Array.prototype.slice.call(arguments)); };"));
0672 }
0673 
0674 Scriptface::~Scriptface()
0675 {
0676     qDeleteAll(loadedPmapHandles);
0677     scriptEngine->deleteLater();
0678 }
0679 
0680 void Scriptface::put(const QString &propertyName, const QJSValue &value)
0681 {
0682     QJSValue internalObject = scriptEngine->globalObject().property(QStringLiteral("ScriptfaceInternal"));
0683     if (internalObject.isUndefined()) {
0684         internalObject = scriptEngine->newObject();
0685         scriptEngine->globalObject().setProperty(QStringLiteral("ScriptfaceInternal"), internalObject);
0686     }
0687 
0688     internalObject.setProperty(propertyName, value);
0689 }
0690 
0691 // ----------------------------------------------------------------------
0692 // Scriptface interface functions.
0693 
0694 #ifdef _MSC_VER
0695 // Work around bizarre MSVC (2013) bug preventing use of QStringLiteral for concatenated string literals
0696 #define SPREF(X) QString::fromLatin1(SFNAME "." X)
0697 #else
0698 #define SPREF(X) QStringLiteral(SFNAME "." X)
0699 #endif
0700 
0701 QJSValue Scriptface::load(const QString &name)
0702 {
0703     QJSValueList fnames;
0704     fnames << name;
0705     return load(fnames);
0706 }
0707 
0708 QJSValue Scriptface::setcall(const QJSValue &name, const QJSValue &func, const QJSValue &fval)
0709 {
0710     if (!name.isString()) {
0711         return throwError(scriptEngine, SPREF("setcall: expected string as first argument"));
0712     }
0713     if (!func.isCallable()) {
0714         return throwError(scriptEngine, SPREF("setcall: expected function as second argument"));
0715     }
0716     if (!(fval.isObject() || fval.isNull())) {
0717         return throwError(scriptEngine, SPREF("setcall: expected object or null as third argument"));
0718     }
0719 
0720     QString qname = name.toString();
0721     funcs[qname] = func;
0722     fvals[qname] = fval;
0723 
0724     // Register values to keep GC from collecting them. Is this needed?
0725     put(QStringLiteral("#:f<%1>").arg(qname), func);
0726     put(QStringLiteral("#:o<%1>").arg(qname), fval);
0727 
0728     // Set current module path as module path for this call,
0729     // in case it contains load subcalls.
0730     fpaths[qname] = globalKTI()->currentModulePath;
0731 
0732     return QJSValue::UndefinedValue;
0733 }
0734 
0735 QJSValue Scriptface::hascall(const QString &qname)
0736 {
0737     return QJSValue(funcs.contains(qname));
0738 }
0739 
0740 QJSValue Scriptface::acallInternal(const QJSValue &args)
0741 {
0742     QJSValueIterator it(args);
0743 
0744     if (!it.next()) {
0745         return throwError(scriptEngine, SPREF("acall: expected at least one argument (call name)"));
0746     }
0747     if (!it.value().isString()) {
0748         return throwError(scriptEngine, SPREF("acall: expected string as first argument (call name)"));
0749     }
0750     // Get the function and its context object.
0751     QString callname = it.value().toString();
0752     if (!funcs.contains(callname)) {
0753         return throwError(scriptEngine, SPREF("acall: unregistered call to '%1'").arg(callname));
0754     }
0755     QJSValue func = funcs[callname];
0756     QJSValue fval = fvals[callname];
0757 
0758     // Recover module path from the time of definition of this call,
0759     // for possible load calls.
0760     globalKTI()->currentModulePath = fpaths[callname];
0761 
0762     // Execute function.
0763     QJSValueList arglist;
0764     while (it.next()) {
0765         arglist.append(it.value());
0766     }
0767 
0768     QJSValue val;
0769     if (fval.isObject()) {
0770         // Call function with the context object.
0771         val = func.callWithInstance(fval, arglist);
0772     } else {
0773         // No context object associated to this function, use global.
0774         val = func.callWithInstance(scriptEngine->globalObject(), arglist);
0775     }
0776     return val;
0777 }
0778 
0779 QJSValue Scriptface::setcallForall(const QJSValue &name, const QJSValue &func, const QJSValue &fval)
0780 {
0781     if (!name.isString()) {
0782         return throwError(scriptEngine, SPREF("setcallForall: expected string as first argument"));
0783     }
0784     if (!func.isCallable()) {
0785         return throwError(scriptEngine, SPREF("setcallForall: expected function as second argument"));
0786     }
0787     if (!(fval.isObject() || fval.isNull())) {
0788         return throwError(scriptEngine, SPREF("setcallForall: expected object or null as third argument"));
0789     }
0790 
0791     QString qname = name.toString();
0792     funcs[qname] = func;
0793     fvals[qname] = fval;
0794 
0795     // Register values to keep GC from collecting them. Is this needed?
0796     put(QStringLiteral("#:fall<%1>").arg(qname), func);
0797     put(QStringLiteral("#:oall<%1>").arg(qname), fval);
0798 
0799     // Set current module path as module path for this call,
0800     // in case it contains load subcalls.
0801     fpaths[qname] = globalKTI()->currentModulePath;
0802 
0803     // Put in the queue order for execution on all messages.
0804     nameForalls.append(qname);
0805 
0806     return QJSValue::UndefinedValue;
0807 }
0808 
0809 QJSValue Scriptface::fallback()
0810 {
0811     if (fallbackRequest) {
0812         *fallbackRequest = true;
0813     }
0814     return QJSValue::UndefinedValue;
0815 }
0816 
0817 QJSValue Scriptface::nsubs()
0818 {
0819     return QJSValue(static_cast<int>(subList->size()));
0820 }
0821 
0822 QJSValue Scriptface::subs(const QJSValue &index)
0823 {
0824     if (!index.isNumber()) {
0825         return throwError(scriptEngine, SPREF("subs: expected number as first argument"));
0826     }
0827 
0828     int i = qRound(index.toNumber());
0829     if (i < 0 || i >= subList->size()) {
0830         return throwError(scriptEngine, SPREF("subs: index out of range"));
0831     }
0832 
0833     return QJSValue(subList->at(i));
0834 }
0835 
0836 QJSValue Scriptface::vals(const QJSValue &index)
0837 {
0838     if (!index.isNumber()) {
0839         return throwError(scriptEngine, SPREF("vals: expected number as first argument"));
0840     }
0841 
0842     int i = qRound(index.toNumber());
0843     if (i < 0 || i >= valList->size()) {
0844         return throwError(scriptEngine, SPREF("vals: index out of range"));
0845     }
0846 
0847     return scriptEngine->toScriptValue(valList->at(i));
0848     //     return variantToJsValue(valList->at(i));
0849 }
0850 
0851 QJSValue Scriptface::msgctxt()
0852 {
0853     return QJSValue(*msgcontext);
0854 }
0855 
0856 QJSValue Scriptface::dynctxt(const QString &qkey)
0857 {
0858     auto valIt = dyncontext->constFind(qkey);
0859     if (valIt != dyncontext->constEnd()) {
0860         return QJSValue(*valIt);
0861     }
0862     return QJSValue::UndefinedValue;
0863 }
0864 
0865 QJSValue Scriptface::msgid()
0866 {
0867     return QJSValue(*msgId);
0868 }
0869 
0870 QJSValue Scriptface::msgkey()
0871 {
0872     return QJSValue(QString(*msgcontext + QLatin1Char('|') + *msgId));
0873 }
0874 
0875 QJSValue Scriptface::msgstrf()
0876 {
0877     return QJSValue(*ftrans);
0878 }
0879 
0880 void Scriptface::dbgputs(const QString &qstr)
0881 {
0882     dbgout("[JS-debug] %1", qstr);
0883 }
0884 
0885 void Scriptface::warnputs(const QString &qstr)
0886 {
0887     warnout("[JS-warning] %1", qstr);
0888 }
0889 
0890 QJSValue Scriptface::localeCountry()
0891 {
0892     return QJSValue(*ctry);
0893 }
0894 
0895 QJSValue Scriptface::normKey(const QJSValue &phrase)
0896 {
0897     if (!phrase.isString()) {
0898         return throwError(scriptEngine, SPREF("normKey: expected string as argument"));
0899     }
0900 
0901     QByteArray nqphrase = normKeystr(phrase.toString());
0902     return QJSValue(QString::fromUtf8(nqphrase));
0903 }
0904 
0905 QJSValue Scriptface::loadProps(const QString &name)
0906 {
0907     QJSValueList fnames;
0908     fnames << name;
0909     return loadProps(fnames);
0910 }
0911 
0912 QJSValue Scriptface::loadProps(const QJSValueList &fnames)
0913 {
0914     if (globalKTI()->currentModulePath.isEmpty()) {
0915         return throwError(scriptEngine, SPREF("loadProps: no current module path, aiiie..."));
0916     }
0917 
0918     for (int i = 0; i < fnames.size(); ++i) {
0919         if (!fnames[i].isString()) {
0920             return throwError(scriptEngine, SPREF("loadProps: expected string as file name"));
0921         }
0922     }
0923 
0924     for (int i = 0; i < fnames.size(); ++i) {
0925         QString qfname = fnames[i].toString();
0926         QString qfpath_base = globalKTI()->currentModulePath + QLatin1Char('/') + qfname;
0927 
0928         // Determine which kind of map is available.
0929         // Give preference to compiled map.
0930         QString qfpath = qfpath_base + QLatin1String(".pmapc");
0931         bool haveCompiled = true;
0932         QFile file_check(qfpath);
0933         if (!file_check.open(QIODevice::ReadOnly)) {
0934             haveCompiled = false;
0935             qfpath = qfpath_base + QLatin1String(".pmap");
0936             QFile file_check(qfpath);
0937             if (!file_check.open(QIODevice::ReadOnly)) {
0938                 return throwError(scriptEngine, SPREF("loadProps: cannot read map '%1'").arg(qfpath));
0939             }
0940         }
0941         file_check.close();
0942 
0943         // Load from appropriate type of map.
0944         if (!loadedPmapPaths.contains(qfpath)) {
0945             QString errorString;
0946             if (haveCompiled) {
0947                 errorString = loadProps_bin(qfpath);
0948             } else {
0949                 errorString = loadProps_text(qfpath);
0950             }
0951             if (!errorString.isEmpty()) {
0952                 return throwError(scriptEngine, errorString);
0953             }
0954             dbgout("Loaded property map: %1", qfpath);
0955             loadedPmapPaths.insert(qfpath);
0956         }
0957     }
0958 
0959     return QJSValue::UndefinedValue;
0960 }
0961 
0962 QJSValue Scriptface::getProp(const QJSValue &phrase, const QJSValue &prop)
0963 {
0964     if (!phrase.isString()) {
0965         return throwError(scriptEngine, SPREF("getProp: expected string as first argument"));
0966     }
0967     if (!prop.isString()) {
0968         return throwError(scriptEngine, SPREF("getProp: expected string as second argument"));
0969     }
0970 
0971     QByteArray qphrase = normKeystr(phrase.toString());
0972     QHash<QByteArray, QByteArray> props = phraseProps.value(qphrase);
0973     if (props.isEmpty()) {
0974         props = resolveUnparsedProps(qphrase);
0975     }
0976     if (!props.isEmpty()) {
0977         QByteArray qprop = normKeystr(prop.toString());
0978         QByteArray qval = props.value(qprop);
0979         if (!qval.isEmpty()) {
0980             return QJSValue(QString::fromUtf8(qval));
0981         }
0982     }
0983     return QJSValue::UndefinedValue;
0984 }
0985 
0986 QJSValue Scriptface::setProp(const QJSValue &phrase, const QJSValue &prop, const QJSValue &value)
0987 {
0988     if (!phrase.isString()) {
0989         return throwError(scriptEngine, SPREF("setProp: expected string as first argument"));
0990     }
0991     if (!prop.isString()) {
0992         return throwError(scriptEngine, SPREF("setProp: expected string as second argument"));
0993     }
0994     if (!value.isString()) {
0995         return throwError(scriptEngine, SPREF("setProp: expected string as third argument"));
0996     }
0997 
0998     QByteArray qphrase = normKeystr(phrase.toString());
0999     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("~@");
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.
1477         QHash<QByteArray, QByteArray> props;
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);
1550     QHash<QByteArray, QByteArray> props;
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"