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"