File indexing completed on 2024-04-28 07:43:04

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     const auto vtype = val.userType();
0317     if (vtype == QMetaType::QString) {
0318         return QJSValue(val.toString());
0319     } else if (vtype == QMetaType::Bool) {
0320         return QJSValue(val.toBool());
0321     } else if (vtype == QMetaType::Double //
0322                || vtype == QMetaType::Int //
0323                || vtype == QMetaType::UInt //
0324                || vtype == QMetaType::LongLong //
0325                || vtype == QMetaType::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     while (!stream.atEnd()) {
0349         QString line = stream.readLine();
0350         int p1;
0351         int p2;
0352 
0353         // Remove comment from the line.
0354         p1 = line.indexOf(QLatin1Char('#'));
0355         if (p1 >= 0) {
0356             line.truncate(p1);
0357         }
0358         line = line.trimmed();
0359         if (line.isEmpty()) {
0360             continue;
0361         }
0362 
0363         if (line[0] == QLatin1Char('[')) {
0364             // Group switch.
0365             p1 = 0;
0366             p2 = line.indexOf(QLatin1Char(']'), p1 + 1);
0367             if (p2 < 0) {
0368                 continue;
0369             }
0370             QString group = line.mid(p1 + 1, p2 - p1 - 1).trimmed();
0371             configGroup = config.find(group);
0372             if (configGroup == config.end()) {
0373                 // Add new group.
0374                 configGroup = config.insert(group, TsConfigGroup());
0375             }
0376         } else {
0377             // Field.
0378             p1 = line.indexOf(QLatin1Char('='));
0379             if (p1 < 0) {
0380                 continue;
0381             }
0382 
0383             const QStringView lineView(line);
0384             const QStringView field = lineView.left(p1).trimmed();
0385             if (!field.isEmpty()) {
0386                 const QStringView value = lineView.mid(p1 + 1).trimmed();
0387                 (*configGroup)[field.toString()] = value.toString();
0388             }
0389         }
0390     }
0391     file.close();
0392 
0393     return config;
0394 }
0395 
0396 // ----------------------------------------------------------------------
0397 // throw or log error, depending on context availability
0398 static QJSValue throwError(QJSEngine *engine, const QString &message)
0399 {
0400     if (engine) {
0401         return engine->evaluate(QStringLiteral("new Error(%1)").arg(message));
0402     }
0403 
0404     qCritical() << "Script error" << message;
0405     return QJSValue::UndefinedValue;
0406 }
0407 
0408 #ifdef KTRANSCRIPT_TESTBUILD
0409 
0410 // ----------------------------------------------------------------------
0411 // Test build creation/destruction hooks
0412 static KTranscriptImp *s_transcriptInstance = nullptr;
0413 
0414 KTranscriptImp *globalKTI()
0415 {
0416     return s_transcriptInstance;
0417 }
0418 
0419 KTranscript *autotestCreateKTranscriptImp()
0420 {
0421     Q_ASSERT(s_transcriptInstance == nullptr);
0422     s_transcriptInstance = new KTranscriptImp;
0423     return s_transcriptInstance;
0424 }
0425 
0426 void autotestDestroyKTranscriptImp()
0427 {
0428     Q_ASSERT(s_transcriptInstance != nullptr);
0429     delete s_transcriptInstance;
0430     s_transcriptInstance = nullptr;
0431 }
0432 
0433 #else
0434 
0435 // ----------------------------------------------------------------------
0436 // Dynamic loading.
0437 Q_GLOBAL_STATIC(KTranscriptImp, globalKTI)
0438 extern "C" {
0439 KTRANSCRIPT_EXPORT KTranscript *load_transcript()
0440 {
0441     return globalKTI();
0442 }
0443 }
0444 #endif
0445 
0446 // ----------------------------------------------------------------------
0447 // KTranscript definitions.
0448 
0449 KTranscriptImp::KTranscriptImp()
0450 {
0451     // Load user configuration.
0452 
0453     QString tsConfigPath = QStandardPaths::locate(QStandardPaths::ConfigLocation, QStringLiteral("ktranscript.ini"));
0454     if (tsConfigPath.isEmpty()) {
0455         tsConfigPath = QDir::homePath() + QLatin1Char('/') + QLatin1String(".transcriptrc");
0456     }
0457     config = readConfig(tsConfigPath);
0458 }
0459 
0460 KTranscriptImp::~KTranscriptImp()
0461 {
0462     qDeleteAll(m_sface);
0463 }
0464 
0465 QString KTranscriptImp::eval(const QList<QVariant> &argv,
0466                              const QString &lang,
0467                              const QString &ctry,
0468                              const QString &msgctxt,
0469                              const QHash<QString, QString> &dynctxt,
0470                              const QString &msgid,
0471                              const QStringList &subs,
0472                              const QList<QVariant> &vals,
0473                              const QString &ftrans,
0474                              QList<QStringList> &mods,
0475                              QString &error,
0476                              bool &fallback)
0477 {
0478     // error = "debug"; return QString();
0479 
0480     error.clear(); // empty error message means successful evaluation
0481     fallback = false; // fallback not requested
0482 
0483 #if 0
0484     // FIXME: Maybe not needed, as QJSEngine has no native outside access?
0485     // Unportable (needs unistd.h)?
0486 
0487     // If effective user id is root and real user id is not root.
0488     if (geteuid() == 0 && getuid() != 0) {
0489         // Since scripts are user input, and the program is running with
0490         // root permissions while real user is not root, do not invoke
0491         // scripting at all, to prevent exploits.
0492         error = "Security block: trying to execute a script in suid environment.";
0493         return QString();
0494     }
0495 #endif
0496 
0497     // Load any new modules and clear the list.
0498     if (!mods.isEmpty()) {
0499         loadModules(mods, error);
0500         mods.clear();
0501         if (!error.isEmpty()) {
0502             return QString();
0503         }
0504     }
0505 
0506     // Add interpreters for new languages.
0507     // (though it should never happen here, but earlier when loading modules;
0508     // this also means there are no calls set, so the unregistered call error
0509     // below will be reported).
0510     if (!m_sface.contains(lang)) {
0511         setupInterpreter(lang);
0512     }
0513 
0514     // Shortcuts.
0515     Scriptface *sface = m_sface[lang];
0516 
0517     QJSEngine *engine = sface->scriptEngine;
0518     QJSValue gobj = engine->globalObject();
0519 
0520     // Link current message data for script-side interface.
0521     sface->msgcontext = &msgctxt;
0522     sface->dyncontext = &dynctxt;
0523     sface->msgId = &msgid;
0524     sface->subList = &subs;
0525     sface->valList = &vals;
0526     sface->ftrans = &ftrans;
0527     sface->fallbackRequest = &fallback;
0528     sface->ctry = &ctry;
0529 
0530     // Find corresponding JS function.
0531     int argc = argv.size();
0532     if (argc < 1) {
0533         // error = "At least the call name must be supplied.";
0534         // Empty interpolation is OK, possibly used just to initialize
0535         // at a given point (e.g. for Ts.setForall() to start having effect).
0536         return QString();
0537     }
0538     QString funcName = argv[0].toString();
0539     if (!sface->funcs.contains(funcName)) {
0540         error = QStringLiteral("Unregistered call to '%1'.").arg(funcName);
0541         return QString();
0542     }
0543 
0544     QJSValue func = sface->funcs[funcName];
0545     QJSValue fval = sface->fvals[funcName];
0546 
0547     // Recover module path from the time of definition of this call,
0548     // for possible load calls.
0549     currentModulePath = sface->fpaths[funcName];
0550 
0551     // Execute function.
0552     QJSValueList arglist;
0553     arglist.reserve(argc - 1);
0554     for (int i = 1; i < argc; ++i) {
0555         arglist.append(engine->toScriptValue(argv[i]));
0556     }
0557 
0558     QJSValue val;
0559     if (fval.isObject()) {
0560         val = func.callWithInstance(fval, arglist);
0561     } else { // no object associated to this function, use global
0562         val = func.callWithInstance(gobj, arglist);
0563     }
0564 
0565     if (fallback) {
0566         // Fallback to ordinary translation requested.
0567         return QString();
0568     } else if (!val.isError()) {
0569         // Evaluation successful.
0570 
0571         if (val.isString()) {
0572             // Good to go.
0573 
0574             return val.toString();
0575         } else {
0576             // Accept only strings.
0577 
0578             QString strval = val.toString();
0579             error = QStringLiteral("Non-string return value: %1").arg(strval);
0580             return QString();
0581         }
0582     } else {
0583         // Exception raised.
0584 
0585         error = expt2str(val);
0586 
0587         return QString();
0588     }
0589 }
0590 
0591 QStringList KTranscriptImp::postCalls(const QString &lang)
0592 {
0593     // Return no calls if scripting was not already set up for this language.
0594     // NOTE: This shouldn't happen, as postCalls cannot be called in such case.
0595     if (!m_sface.contains(lang)) {
0596         return QStringList();
0597     }
0598 
0599     // Shortcuts.
0600     Scriptface *sface = m_sface[lang];
0601 
0602     return sface->nameForalls;
0603 }
0604 
0605 void KTranscriptImp::loadModules(const QList<QStringList> &mods, QString &error)
0606 {
0607     QList<QString> modErrors;
0608 
0609     for (const QStringList &mod : mods) {
0610         QString mpath = mod[0];
0611         QString mlang = mod[1];
0612 
0613         // Add interpreters for new languages.
0614         if (!m_sface.contains(mlang)) {
0615             setupInterpreter(mlang);
0616         }
0617 
0618         // Setup current module path for loading submodules.
0619         // (sort of closure over invocations of loadf)
0620         int posls = mpath.lastIndexOf(QLatin1Char('/'));
0621         if (posls < 1) {
0622             modErrors.append(QStringLiteral("Funny module path '%1', skipping.").arg(mpath));
0623             continue;
0624         }
0625         currentModulePath = mpath.left(posls);
0626         QString fname = mpath.mid(posls + 1);
0627         // Scriptface::loadf() wants no extension on the filename
0628         fname = fname.left(fname.lastIndexOf(QLatin1Char('.')));
0629 
0630         // Load the module.
0631         QJSValueList alist;
0632         alist.append(QJSValue(fname));
0633 
0634         m_sface[mlang]->load(alist);
0635     }
0636 
0637     // Unset module path.
0638     currentModulePath.clear();
0639 
0640     for (const QString &merr : std::as_const(modErrors)) {
0641         error.append(merr + QLatin1Char('\n'));
0642     }
0643 }
0644 
0645 #define SFNAME "Ts"
0646 void KTranscriptImp::setupInterpreter(const QString &lang)
0647 {
0648     // Add scripting interface
0649     // Creates its own script engine and registers with it
0650     // NOTE: Config may not contain an entry for the language, in which case
0651     // it is automatically constructed as an empty hash. This is intended.
0652     Scriptface *sface = new Scriptface(config[lang]);
0653 
0654     // Store scriptface
0655     m_sface[lang] = sface;
0656 
0657     // dbgout("=====> Created interpreter for '%1'", lang);
0658 }
0659 
0660 Scriptface::Scriptface(const TsConfigGroup &config_, QObject *parent)
0661     : QObject(parent)
0662     , scriptEngine(new QJSEngine)
0663     , fallbackRequest(nullptr)
0664     , config(config_)
0665 {
0666     QJSValue object = scriptEngine->newQObject(this);
0667     scriptEngine->globalObject().setProperty(QStringLiteral(SFNAME), object);
0668     scriptEngine->evaluate(QStringLiteral("Ts.acall = function() { return Ts.acallInternal(Array.prototype.slice.call(arguments)); };"));
0669 }
0670 
0671 Scriptface::~Scriptface()
0672 {
0673     qDeleteAll(loadedPmapHandles);
0674     scriptEngine->deleteLater();
0675 }
0676 
0677 void Scriptface::put(const QString &propertyName, const QJSValue &value)
0678 {
0679     QJSValue internalObject = scriptEngine->globalObject().property(QStringLiteral("ScriptfaceInternal"));
0680     if (internalObject.isUndefined()) {
0681         internalObject = scriptEngine->newObject();
0682         scriptEngine->globalObject().setProperty(QStringLiteral("ScriptfaceInternal"), internalObject);
0683     }
0684 
0685     internalObject.setProperty(propertyName, value);
0686 }
0687 
0688 // ----------------------------------------------------------------------
0689 // Scriptface interface functions.
0690 
0691 #ifdef _MSC_VER
0692 // Work around bizarre MSVC (2013) bug preventing use of QStringLiteral for concatenated string literals
0693 #define SPREF(X) QString::fromLatin1(SFNAME "." X)
0694 #else
0695 #define SPREF(X) QStringLiteral(SFNAME "." X)
0696 #endif
0697 
0698 QJSValue Scriptface::load(const QString &name)
0699 {
0700     QJSValueList fnames;
0701     fnames << name;
0702     return load(fnames);
0703 }
0704 
0705 QJSValue Scriptface::setcall(const QJSValue &name, const QJSValue &func, const QJSValue &fval)
0706 {
0707     if (!name.isString()) {
0708         return throwError(scriptEngine, SPREF("setcall: expected string as first argument"));
0709     }
0710     if (!func.isCallable()) {
0711         return throwError(scriptEngine, SPREF("setcall: expected function as second argument"));
0712     }
0713     if (!(fval.isObject() || fval.isNull())) {
0714         return throwError(scriptEngine, SPREF("setcall: expected object or null as third argument"));
0715     }
0716 
0717     QString qname = name.toString();
0718     funcs[qname] = func;
0719     fvals[qname] = fval;
0720 
0721     // Register values to keep GC from collecting them. Is this needed?
0722     put(QStringLiteral("#:f<%1>").arg(qname), func);
0723     put(QStringLiteral("#:o<%1>").arg(qname), fval);
0724 
0725     // Set current module path as module path for this call,
0726     // in case it contains load subcalls.
0727     fpaths[qname] = globalKTI()->currentModulePath;
0728 
0729     return QJSValue::UndefinedValue;
0730 }
0731 
0732 QJSValue Scriptface::hascall(const QString &qname)
0733 {
0734     return QJSValue(funcs.contains(qname));
0735 }
0736 
0737 QJSValue Scriptface::acallInternal(const QJSValue &args)
0738 {
0739     QJSValueIterator it(args);
0740 
0741     if (!it.next()) {
0742         return throwError(scriptEngine, SPREF("acall: expected at least one argument (call name)"));
0743     }
0744     if (!it.value().isString()) {
0745         return throwError(scriptEngine, SPREF("acall: expected string as first argument (call name)"));
0746     }
0747     // Get the function and its context object.
0748     QString callname = it.value().toString();
0749     if (!funcs.contains(callname)) {
0750         return throwError(scriptEngine, SPREF("acall: unregistered call to '%1'").arg(callname));
0751     }
0752     QJSValue func = funcs[callname];
0753     QJSValue fval = fvals[callname];
0754 
0755     // Recover module path from the time of definition of this call,
0756     // for possible load calls.
0757     globalKTI()->currentModulePath = fpaths[callname];
0758 
0759     // Execute function.
0760     QJSValueList arglist;
0761     while (it.next()) {
0762         arglist.append(it.value());
0763     }
0764 
0765     QJSValue val;
0766     if (fval.isObject()) {
0767         // Call function with the context object.
0768         val = func.callWithInstance(fval, arglist);
0769     } else {
0770         // No context object associated to this function, use global.
0771         val = func.callWithInstance(scriptEngine->globalObject(), arglist);
0772     }
0773     return val;
0774 }
0775 
0776 QJSValue Scriptface::setcallForall(const QJSValue &name, const QJSValue &func, const QJSValue &fval)
0777 {
0778     if (!name.isString()) {
0779         return throwError(scriptEngine, SPREF("setcallForall: expected string as first argument"));
0780     }
0781     if (!func.isCallable()) {
0782         return throwError(scriptEngine, SPREF("setcallForall: expected function as second argument"));
0783     }
0784     if (!(fval.isObject() || fval.isNull())) {
0785         return throwError(scriptEngine, SPREF("setcallForall: expected object or null as third argument"));
0786     }
0787 
0788     QString qname = name.toString();
0789     funcs[qname] = func;
0790     fvals[qname] = fval;
0791 
0792     // Register values to keep GC from collecting them. Is this needed?
0793     put(QStringLiteral("#:fall<%1>").arg(qname), func);
0794     put(QStringLiteral("#:oall<%1>").arg(qname), fval);
0795 
0796     // Set current module path as module path for this call,
0797     // in case it contains load subcalls.
0798     fpaths[qname] = globalKTI()->currentModulePath;
0799 
0800     // Put in the queue order for execution on all messages.
0801     nameForalls.append(qname);
0802 
0803     return QJSValue::UndefinedValue;
0804 }
0805 
0806 QJSValue Scriptface::fallback()
0807 {
0808     if (fallbackRequest) {
0809         *fallbackRequest = true;
0810     }
0811     return QJSValue::UndefinedValue;
0812 }
0813 
0814 QJSValue Scriptface::nsubs()
0815 {
0816     return QJSValue(static_cast<int>(subList->size()));
0817 }
0818 
0819 QJSValue Scriptface::subs(const QJSValue &index)
0820 {
0821     if (!index.isNumber()) {
0822         return throwError(scriptEngine, SPREF("subs: expected number as first argument"));
0823     }
0824 
0825     int i = qRound(index.toNumber());
0826     if (i < 0 || i >= subList->size()) {
0827         return throwError(scriptEngine, SPREF("subs: index out of range"));
0828     }
0829 
0830     return QJSValue(subList->at(i));
0831 }
0832 
0833 QJSValue Scriptface::vals(const QJSValue &index)
0834 {
0835     if (!index.isNumber()) {
0836         return throwError(scriptEngine, SPREF("vals: expected number as first argument"));
0837     }
0838 
0839     int i = qRound(index.toNumber());
0840     if (i < 0 || i >= valList->size()) {
0841         return throwError(scriptEngine, SPREF("vals: index out of range"));
0842     }
0843 
0844     return scriptEngine->toScriptValue(valList->at(i));
0845     //     return variantToJsValue(valList->at(i));
0846 }
0847 
0848 QJSValue Scriptface::msgctxt()
0849 {
0850     return QJSValue(*msgcontext);
0851 }
0852 
0853 QJSValue Scriptface::dynctxt(const QString &qkey)
0854 {
0855     auto valIt = dyncontext->constFind(qkey);
0856     if (valIt != dyncontext->constEnd()) {
0857         return QJSValue(*valIt);
0858     }
0859     return QJSValue::UndefinedValue;
0860 }
0861 
0862 QJSValue Scriptface::msgid()
0863 {
0864     return QJSValue(*msgId);
0865 }
0866 
0867 QJSValue Scriptface::msgkey()
0868 {
0869     return QJSValue(QString(*msgcontext + QLatin1Char('|') + *msgId));
0870 }
0871 
0872 QJSValue Scriptface::msgstrf()
0873 {
0874     return QJSValue(*ftrans);
0875 }
0876 
0877 void Scriptface::dbgputs(const QString &qstr)
0878 {
0879     dbgout("[JS-debug] %1", qstr);
0880 }
0881 
0882 void Scriptface::warnputs(const QString &qstr)
0883 {
0884     warnout("[JS-warning] %1", qstr);
0885 }
0886 
0887 QJSValue Scriptface::localeCountry()
0888 {
0889     return QJSValue(*ctry);
0890 }
0891 
0892 QJSValue Scriptface::normKey(const QJSValue &phrase)
0893 {
0894     if (!phrase.isString()) {
0895         return throwError(scriptEngine, SPREF("normKey: expected string as argument"));
0896     }
0897 
0898     QByteArray nqphrase = normKeystr(phrase.toString());
0899     return QJSValue(QString::fromUtf8(nqphrase));
0900 }
0901 
0902 QJSValue Scriptface::loadProps(const QString &name)
0903 {
0904     QJSValueList fnames;
0905     fnames << name;
0906     return loadProps(fnames);
0907 }
0908 
0909 QJSValue Scriptface::loadProps(const QJSValueList &fnames)
0910 {
0911     if (globalKTI()->currentModulePath.isEmpty()) {
0912         return throwError(scriptEngine, SPREF("loadProps: no current module path, aiiie..."));
0913     }
0914 
0915     for (int i = 0; i < fnames.size(); ++i) {
0916         if (!fnames[i].isString()) {
0917             return throwError(scriptEngine, SPREF("loadProps: expected string as file name"));
0918         }
0919     }
0920 
0921     for (int i = 0; i < fnames.size(); ++i) {
0922         QString qfname = fnames[i].toString();
0923         QString qfpath_base = globalKTI()->currentModulePath + QLatin1Char('/') + qfname;
0924 
0925         // Determine which kind of map is available.
0926         // Give preference to compiled map.
0927         QString qfpath = qfpath_base + QLatin1String(".pmapc");
0928         bool haveCompiled = true;
0929         QFile file_check(qfpath);
0930         if (!file_check.open(QIODevice::ReadOnly)) {
0931             haveCompiled = false;
0932             qfpath = qfpath_base + QLatin1String(".pmap");
0933             QFile file_check(qfpath);
0934             if (!file_check.open(QIODevice::ReadOnly)) {
0935                 return throwError(scriptEngine, SPREF("loadProps: cannot read map '%1'").arg(qfpath));
0936             }
0937         }
0938         file_check.close();
0939 
0940         // Load from appropriate type of map.
0941         if (!loadedPmapPaths.contains(qfpath)) {
0942             QString errorString;
0943             if (haveCompiled) {
0944                 errorString = loadProps_bin(qfpath);
0945             } else {
0946                 errorString = loadProps_text(qfpath);
0947             }
0948             if (!errorString.isEmpty()) {
0949                 return throwError(scriptEngine, errorString);
0950             }
0951             dbgout("Loaded property map: %1", qfpath);
0952             loadedPmapPaths.insert(qfpath);
0953         }
0954     }
0955 
0956     return QJSValue::UndefinedValue;
0957 }
0958 
0959 QJSValue Scriptface::getProp(const QJSValue &phrase, const QJSValue &prop)
0960 {
0961     if (!phrase.isString()) {
0962         return throwError(scriptEngine, SPREF("getProp: expected string as first argument"));
0963     }
0964     if (!prop.isString()) {
0965         return throwError(scriptEngine, SPREF("getProp: expected string as second argument"));
0966     }
0967 
0968     QByteArray qphrase = normKeystr(phrase.toString());
0969     QHash<QByteArray, QByteArray> props = phraseProps.value(qphrase);
0970     if (props.isEmpty()) {
0971         props = resolveUnparsedProps(qphrase);
0972     }
0973     if (!props.isEmpty()) {
0974         QByteArray qprop = normKeystr(prop.toString());
0975         QByteArray qval = props.value(qprop);
0976         if (!qval.isEmpty()) {
0977             return QJSValue(QString::fromUtf8(qval));
0978         }
0979     }
0980     return QJSValue::UndefinedValue;
0981 }
0982 
0983 QJSValue Scriptface::setProp(const QJSValue &phrase, const QJSValue &prop, const QJSValue &value)
0984 {
0985     if (!phrase.isString()) {
0986         return throwError(scriptEngine, SPREF("setProp: expected string as first argument"));
0987     }
0988     if (!prop.isString()) {
0989         return throwError(scriptEngine, SPREF("setProp: expected string as second argument"));
0990     }
0991     if (!value.isString()) {
0992         return throwError(scriptEngine, SPREF("setProp: expected string as third argument"));
0993     }
0994 
0995     QByteArray qphrase = normKeystr(phrase.toString());
0996     QByteArray qprop = normKeystr(prop.toString());
0997     QByteArray qvalue = value.toString().toUtf8();
0998     // Any non-existent key in first or second-level hash will be created.
0999     phraseProps[qphrase][qprop] = qvalue;
1000     return QJSValue::UndefinedValue;
1001 }
1002 
1003 static QString toCaseFirst(const QString &qstr, int qnalt, bool toupper)
1004 {
1005     static const QLatin1String head("~@");
1006     static const int hlen = 2; // head.length()
1007 
1008     // If the first letter is found within an alternatives directive,
1009     // change case of the first letter in each of the alternatives.
1010     QString qstrcc = qstr;
1011     const int len = qstr.length();
1012     QChar altSep;
1013     int remainingAlts = 0;
1014     bool checkCase = true;
1015     int numChcased = 0;
1016     int i = 0;
1017     while (i < len) {
1018         QChar c = qstr[i];
1019 
1020         if (qnalt && !remainingAlts && QStringView(qstr).mid(i, hlen) == head) {
1021             // An alternatives directive is just starting.
1022             i += 2;
1023             if (i >= len) {
1024                 break; // malformed directive, bail out
1025             }
1026             // Record alternatives separator, set number of remaining
1027             // alternatives, reactivate case checking.
1028             altSep = qstrcc[i];
1029             remainingAlts = qnalt;
1030             checkCase = true;
1031         } else if (remainingAlts && c == altSep) {
1032             // Alternative separator found, reduce number of remaining
1033             // alternatives and reactivate case checking.
1034             --remainingAlts;
1035             checkCase = true;
1036         } else if (checkCase && c.isLetter()) {
1037             // Case check is active and the character is a letter; change case.
1038             if (toupper) {
1039                 qstrcc[i] = c.toUpper();
1040             } else {
1041                 qstrcc[i] = c.toLower();
1042             }
1043             ++numChcased;
1044             // No more case checks until next alternatives separator.
1045             checkCase = false;
1046         }
1047 
1048         // If any letter has been changed, and there are no more alternatives
1049         // to be processed, we're done.
1050         if (numChcased > 0 && remainingAlts == 0) {
1051             break;
1052         }
1053 
1054         // Go to next character.
1055         ++i;
1056     }
1057 
1058     return qstrcc;
1059 }
1060 
1061 QJSValue Scriptface::toUpperFirst(const QJSValue &str, const QJSValue &nalt)
1062 {
1063     if (!str.isString()) {
1064         return throwError(scriptEngine, SPREF("toUpperFirst: expected string as first argument"));
1065     }
1066     if (!(nalt.isNumber() || nalt.isNull())) {
1067         return throwError(scriptEngine, SPREF("toUpperFirst: expected number as second argument"));
1068     }
1069 
1070     QString qstr = str.toString();
1071     int qnalt = nalt.isNull() ? 0 : nalt.toInt();
1072 
1073     QString qstruc = toCaseFirst(qstr, qnalt, true);
1074 
1075     return QJSValue(qstruc);
1076 }
1077 
1078 QJSValue Scriptface::toLowerFirst(const QJSValue &str, const QJSValue &nalt)
1079 {
1080     if (!str.isString()) {
1081         return throwError(scriptEngine, SPREF("toLowerFirst: expected string as first argument"));
1082     }
1083     if (!(nalt.isNumber() || nalt.isNull())) {
1084         return throwError(scriptEngine, SPREF("toLowerFirst: expected number as second argument"));
1085     }
1086 
1087     QString qstr = str.toString();
1088     int qnalt = nalt.isNull() ? 0 : nalt.toInt();
1089 
1090     QString qstrlc = toCaseFirst(qstr, qnalt, false);
1091 
1092     return QJSValue(qstrlc);
1093 }
1094 
1095 QJSValue Scriptface::getConfString(const QJSValue &key, const QJSValue &dval)
1096 {
1097     if (!key.isString()) {
1098         return throwError(scriptEngine, QStringLiteral("getConfString: expected string as first argument"));
1099     }
1100     if (!(dval.isString() || dval.isNull())) {
1101         return throwError(scriptEngine, SPREF("getConfString: expected string as second argument (when given)"));
1102     }
1103 
1104     QString qkey = key.toString();
1105     auto valIt = config.constFind(qkey);
1106     if (valIt != config.constEnd()) {
1107         return QJSValue(*valIt);
1108     }
1109 
1110     return dval.isNull() ? QJSValue::UndefinedValue : dval;
1111 }
1112 
1113 QJSValue Scriptface::getConfBool(const QJSValue &key, const QJSValue &dval)
1114 {
1115     if (!key.isString()) {
1116         return throwError(scriptEngine, SPREF("getConfBool: expected string as first argument"));
1117     }
1118     if (!(dval.isBool() || dval.isNull())) {
1119         return throwError(scriptEngine, SPREF("getConfBool: expected boolean as second argument (when given)"));
1120     }
1121 
1122     static QStringList falsities;
1123     if (falsities.isEmpty()) {
1124         falsities.append(QString(QLatin1Char('0')));
1125         falsities.append(QStringLiteral("no"));
1126         falsities.append(QStringLiteral("false"));
1127     }
1128 
1129     QString qkey = key.toString();
1130     auto valIt = config.constFind(qkey);
1131     if (valIt != config.constEnd()) {
1132         QString qval = valIt->toLower();
1133         return QJSValue(!falsities.contains(qval));
1134     }
1135 
1136     return dval.isNull() ? QJSValue::UndefinedValue : dval;
1137 }
1138 
1139 QJSValue Scriptface::getConfNumber(const QJSValue &key, const QJSValue &dval)
1140 {
1141     if (!key.isString()) {
1142         return throwError(scriptEngine,
1143                           SPREF("getConfNumber: expected string "
1144                                 "as first argument"));
1145     }
1146     if (!(dval.isNumber() || dval.isNull())) {
1147         return throwError(scriptEngine,
1148                           SPREF("getConfNumber: expected number "
1149                                 "as second argument (when given)"));
1150     }
1151 
1152     QString qkey = key.toString();
1153     auto valIt = config.constFind(qkey);
1154     if (valIt != config.constEnd()) {
1155         const QString &qval = *valIt;
1156         bool convOk;
1157         double qnum = qval.toDouble(&convOk);
1158         if (convOk) {
1159             return QJSValue(qnum);
1160         }
1161     }
1162 
1163     return dval.isNull() ? QJSValue::UndefinedValue : dval;
1164 }
1165 
1166 // ----------------------------------------------------------------------
1167 // Scriptface helpers to interface functions.
1168 
1169 QJSValue Scriptface::load(const QJSValueList &fnames)
1170 {
1171     if (globalKTI()->currentModulePath.isEmpty()) {
1172         return throwError(scriptEngine, SPREF("load: no current module path, aiiie..."));
1173     }
1174 
1175     for (int i = 0; i < fnames.size(); ++i) {
1176         if (!fnames[i].isString()) {
1177             return throwError(scriptEngine, SPREF("load: expected string as file name"));
1178         }
1179     }
1180 
1181     for (int i = 0; i < fnames.size(); ++i) {
1182         QString qfname = fnames[i].toString();
1183         QString qfpath = globalKTI()->currentModulePath + QLatin1Char('/') + qfname + QLatin1String(".js");
1184 
1185         QFile file(qfpath);
1186         if (!file.open(QIODevice::ReadOnly)) {
1187             return throwError(scriptEngine, SPREF("load: cannot read file '%1'").arg(qfpath));
1188         }
1189 
1190         QTextStream stream(&file);
1191         QString source = stream.readAll();
1192         file.close();
1193 
1194         QJSValue comp = scriptEngine->evaluate(source, qfpath, 0);
1195 
1196         if (comp.isError()) {
1197             QString msg = comp.toString();
1198 
1199             QString line;
1200             if (comp.isObject()) {
1201                 QJSValue lval = comp.property(QStringLiteral("line"));
1202                 if (lval.isNumber()) {
1203                     line = QString::number(lval.toInt());
1204                 }
1205             }
1206 
1207             return throwError(scriptEngine, QStringLiteral("at %1:%2: %3").arg(qfpath, line, msg));
1208         }
1209         dbgout("Loaded module: %1", qfpath);
1210     }
1211     return QJSValue::UndefinedValue;
1212 }
1213 
1214 QString Scriptface::loadProps_text(const QString &fpath)
1215 {
1216     QFile file(fpath);
1217     if (!file.open(QIODevice::ReadOnly)) {
1218         return SPREF("loadProps_text: cannot read file '%1'").arg(fpath);
1219     }
1220     QTextStream stream(&file);
1221     QString s = stream.readAll();
1222     file.close();
1223 
1224     // Parse the map.
1225     // Should care about performance: possibly executed on each KDE
1226     // app startup and reading houndreds of thousands of characters.
1227     enum { s_nextEntry, s_nextKey, s_nextValue };
1228     QList<QByteArray> ekeys; // holds keys for current entry
1229     QHash<QByteArray, QByteArray> props; // holds properties for current entry
1230     int slen = s.length();
1231     int state = s_nextEntry;
1232     QByteArray pkey;
1233     QChar prop_sep;
1234     QChar key_sep;
1235     int i = 0;
1236     while (1) {
1237         int i_checkpoint = i;
1238 
1239         if (state == s_nextEntry) {
1240             while (s[i].isSpace()) {
1241                 ++i;
1242                 if (i >= slen) {
1243                     goto END_PROP_PARSE;
1244                 }
1245             }
1246             if (i + 1 >= slen) {
1247                 return SPREF("loadProps_text: unexpected end of file in %1").arg(fpath);
1248             }
1249             if (s[i] != QLatin1Char('#')) {
1250                 // Separator characters for this entry.
1251                 key_sep = s[i];
1252                 prop_sep = s[i + 1];
1253                 if (key_sep.isLetter() || prop_sep.isLetter()) {
1254                     return SPREF("loadProps_text: separator characters must not be letters at %1:%2").arg(fpath).arg(countLines(s, i));
1255                 }
1256 
1257                 // Reset all data for current entry.
1258                 ekeys.clear();
1259                 props.clear();
1260                 pkey.clear();
1261 
1262                 i += 2;
1263                 state = s_nextKey;
1264             } else {
1265                 // This is a comment, skip to EOL, don't change state.
1266                 while (s[i] != QLatin1Char('\n')) {
1267                     ++i;
1268                     if (i >= slen) {
1269                         goto END_PROP_PARSE;
1270                     }
1271                 }
1272             }
1273         } else if (state == s_nextKey) {
1274             int ip = i;
1275             // Proceed up to next key or property separator.
1276             while (s[i] != key_sep && s[i] != prop_sep) {
1277                 ++i;
1278                 if (i >= slen) {
1279                     goto END_PROP_PARSE;
1280                 }
1281             }
1282             if (s[i] == key_sep) {
1283                 // This is a property key,
1284                 // record for when the value gets parsed.
1285                 pkey = normKeystr(s.mid(ip, i - ip), false);
1286 
1287                 i += 1;
1288                 state = s_nextValue;
1289             } else { // if (s[i] == prop_sep) {
1290                 // This is an entry key, or end of entry.
1291                 QByteArray ekey = normKeystr(s.mid(ip, i - ip), false);
1292                 if (!ekey.isEmpty()) {
1293                     // An entry key.
1294                     ekeys.append(ekey);
1295 
1296                     i += 1;
1297                     state = s_nextKey;
1298                 } else {
1299                     // End of entry.
1300                     if (ekeys.size() < 1) {
1301                         return SPREF("loadProps_text: no entry key for entry ending at %1:%2").arg(fpath).arg(countLines(s, i));
1302                     }
1303 
1304                     // Add collected entry into global store,
1305                     // once for each entry key (QHash implicitly shared).
1306                     for (const QByteArray &ekey : std::as_const(ekeys)) {
1307                         phraseProps[ekey] = props;
1308                     }
1309 
1310                     i += 1;
1311                     state = s_nextEntry;
1312                 }
1313             }
1314         } else if (state == s_nextValue) {
1315             int ip = i;
1316             // Proceed up to next property separator.
1317             while (s[i] != prop_sep) {
1318                 ++i;
1319                 if (i >= slen) {
1320                     goto END_PROP_PARSE;
1321                 }
1322                 if (s[i] == key_sep) {
1323                     return SPREF("loadProps_text: property separator inside property value at %1:%2").arg(fpath).arg(countLines(s, i));
1324                 }
1325             }
1326             // Extract the property value and store the property.
1327             QByteArray pval = trimSmart(s.mid(ip, i - ip)).toUtf8();
1328             props[pkey] = pval;
1329 
1330             i += 1;
1331             state = s_nextKey;
1332         } else {
1333             return SPREF("loadProps: internal error 10 at %1:%2").arg(fpath).arg(countLines(s, i));
1334         }
1335 
1336         // To avoid infinite looping and stepping out.
1337         if (i == i_checkpoint || i >= slen) {
1338             return SPREF("loadProps: internal error 20 at %1:%2").arg(fpath).arg(countLines(s, i));
1339         }
1340     }
1341 
1342 END_PROP_PARSE:
1343 
1344     if (state != s_nextEntry) {
1345         return SPREF("loadProps: unexpected end of file in %1").arg(fpath);
1346     }
1347 
1348     return QString();
1349 }
1350 
1351 // Read big-endian integer of nbytes length at position pos
1352 // in character array fc of length len.
1353 // Update position to point after the number.
1354 // In case of error, pos is set to -1.
1355 template<typename T>
1356 static int bin_read_int_nbytes(const char *fc, qlonglong len, qlonglong &pos, int nbytes)
1357 {
1358     if (pos + nbytes > len) {
1359         pos = -1;
1360         return 0;
1361     }
1362     T num = qFromBigEndian<T>((uchar *)fc + pos);
1363     pos += nbytes;
1364     return num;
1365 }
1366 
1367 // Read 64-bit big-endian integer.
1368 static quint64 bin_read_int64(const char *fc, qlonglong len, qlonglong &pos)
1369 {
1370     return bin_read_int_nbytes<quint64>(fc, len, pos, 8);
1371 }
1372 
1373 // Read 32-bit big-endian integer.
1374 static quint32 bin_read_int(const char *fc, qlonglong len, qlonglong &pos)
1375 {
1376     return bin_read_int_nbytes<quint32>(fc, len, pos, 4);
1377 }
1378 
1379 // Read string at position pos of character array fc of length n.
1380 // String is represented as 32-bit big-endian byte length followed by bytes.
1381 // Update position to point after the string.
1382 // In case of error, pos is set to -1.
1383 static QByteArray bin_read_string(const char *fc, qlonglong len, qlonglong &pos)
1384 {
1385     // Binary format stores strings as length followed by byte sequence.
1386     // No null-termination.
1387     int nbytes = bin_read_int(fc, len, pos);
1388     if (pos < 0) {
1389         return QByteArray();
1390     }
1391     if (nbytes < 0 || pos + nbytes > len) {
1392         pos = -1;
1393         return QByteArray();
1394     }
1395     QByteArray s(fc + pos, nbytes);
1396     pos += nbytes;
1397     return s;
1398 }
1399 
1400 QString Scriptface::loadProps_bin(const QString &fpath)
1401 {
1402     QFile file(fpath);
1403     if (!file.open(QIODevice::ReadOnly)) {
1404         return SPREF("loadProps: cannot read file '%1'").arg(fpath);
1405     }
1406     // Collect header.
1407     QByteArray head(8, '0');
1408     file.read(head.data(), head.size());
1409     file.close();
1410 
1411     // Choose pmap loader based on header.
1412     if (head == "TSPMAP00") {
1413         return loadProps_bin_00(fpath);
1414     } else if (head == "TSPMAP01") {
1415         return loadProps_bin_01(fpath);
1416     } else {
1417         return SPREF("loadProps: unknown version of compiled map '%1'").arg(fpath);
1418     }
1419 }
1420 
1421 QString Scriptface::loadProps_bin_00(const QString &fpath)
1422 {
1423     QFile file(fpath);
1424     if (!file.open(QIODevice::ReadOnly)) {
1425         return SPREF("loadProps: cannot read file '%1'").arg(fpath);
1426     }
1427     QByteArray fctmp = file.readAll();
1428     file.close();
1429     const char *fc = fctmp.data();
1430     const int fclen = fctmp.size();
1431 
1432     // Indicates stream state.
1433     qlonglong pos = 0;
1434 
1435     // Match header.
1436     QByteArray head(fc, 8);
1437     pos += 8;
1438     if (head != "TSPMAP00") {
1439         goto END_PROP_PARSE;
1440     }
1441 
1442     // Read total number of entries.
1443     int nentries;
1444     nentries = bin_read_int(fc, fclen, pos);
1445     if (pos < 0) {
1446         goto END_PROP_PARSE;
1447     }
1448 
1449     // Read all entries.
1450     for (int i = 0; i < nentries; ++i) {
1451         // Read number of entry keys and all entry keys.
1452         QList<QByteArray> ekeys;
1453         int nekeys = bin_read_int(fc, fclen, pos);
1454         if (pos < 0) {
1455             goto END_PROP_PARSE;
1456         }
1457         ekeys.reserve(nekeys); // nekeys are appended if data is not corrupted
1458         for (int j = 0; j < nekeys; ++j) {
1459             QByteArray ekey = bin_read_string(fc, fclen, pos);
1460             if (pos < 0) {
1461                 goto END_PROP_PARSE;
1462             }
1463             ekeys.append(ekey);
1464         }
1465         // dbgout("--------> ekey[0]={%1}", QString::fromUtf8(ekeys[0]));
1466 
1467         // Read number of properties and all properties.
1468         QHash<QByteArray, QByteArray> props;
1469         int nprops = bin_read_int(fc, fclen, pos);
1470         if (pos < 0) {
1471             goto END_PROP_PARSE;
1472         }
1473         for (int j = 0; j < nprops; ++j) {
1474             QByteArray pkey = bin_read_string(fc, fclen, pos);
1475             if (pos < 0) {
1476                 goto END_PROP_PARSE;
1477             }
1478             QByteArray pval = bin_read_string(fc, fclen, pos);
1479             if (pos < 0) {
1480                 goto END_PROP_PARSE;
1481             }
1482             props[pkey] = pval;
1483         }
1484 
1485         // Add collected entry into global store,
1486         // once for each entry key (QHash implicitly shared).
1487         for (const QByteArray &ekey : std::as_const(ekeys)) {
1488             phraseProps[ekey] = props;
1489         }
1490     }
1491 
1492 END_PROP_PARSE:
1493 
1494     if (pos < 0) {
1495         return SPREF("loadProps: corrupt compiled map '%1'").arg(fpath);
1496     }
1497 
1498     return QString();
1499 }
1500 
1501 QString Scriptface::loadProps_bin_01(const QString &fpath)
1502 {
1503     QFile *file = new QFile(fpath);
1504     if (!file->open(QIODevice::ReadOnly)) {
1505         return SPREF("loadProps: cannot read file '%1'").arg(fpath);
1506     }
1507 
1508     QByteArray fstr;
1509     qlonglong pos;
1510 
1511     // Read the header and number and length of entry keys.
1512     fstr = file->read(8 + 4 + 8);
1513     pos = 0;
1514     QByteArray head = fstr.left(8);
1515     pos += 8;
1516     if (head != "TSPMAP01") {
1517         return SPREF("loadProps: corrupt compiled map '%1'").arg(fpath);
1518     }
1519     quint32 numekeys = bin_read_int(fstr, fstr.size(), pos);
1520     quint64 lenekeys = bin_read_int64(fstr, fstr.size(), pos);
1521 
1522     // Read entry keys.
1523     fstr = file->read(lenekeys);
1524     pos = 0;
1525     for (quint32 i = 0; i < numekeys; ++i) {
1526         QByteArray ekey = bin_read_string(fstr, lenekeys, pos);
1527         quint64 offset = bin_read_int64(fstr, lenekeys, pos);
1528         phraseUnparsedProps[ekey] = {file, offset};
1529     }
1530 
1531     // // Read property keys.
1532     // ...when it becomes necessary
1533 
1534     loadedPmapHandles.insert(file);
1535     return QString();
1536 }
1537 
1538 QHash<QByteArray, QByteArray> Scriptface::resolveUnparsedProps(const QByteArray &phrase)
1539 {
1540     auto [file, offset] = phraseUnparsedProps.value(phrase);
1541     QHash<QByteArray, QByteArray> props;
1542     if (file && file->seek(offset)) {
1543         QByteArray fstr = file->read(4 + 4);
1544         qlonglong pos = 0;
1545         quint32 numpkeys = bin_read_int(fstr, fstr.size(), pos);
1546         quint32 lenpkeys = bin_read_int(fstr, fstr.size(), pos);
1547         fstr = file->read(lenpkeys);
1548         pos = 0;
1549         for (quint32 i = 0; i < numpkeys; ++i) {
1550             QByteArray pkey = bin_read_string(fstr, lenpkeys, pos);
1551             QByteArray pval = bin_read_string(fstr, lenpkeys, pos);
1552             props[pkey] = pval;
1553         }
1554         phraseProps[phrase] = props;
1555         phraseUnparsedProps.remove(phrase);
1556     }
1557     return props;
1558 }
1559 
1560 #include "ktranscript.moc"