File indexing completed on 2023-09-24 04:08:30
0001 /* 0002 This file is part of the KDE libraries 0003 SPDX-FileCopyrightText: 2000 Torben Weis <weis@kde.org> 0004 SPDX-FileCopyrightText: 2006-2013 David Faure <faure@kde.org> 0005 SPDX-FileCopyrightText: 2009 Michael Pyne <michael.pyne@kdemail.net> 0006 0007 SPDX-License-Identifier: LGPL-2.0-or-later 0008 */ 0009 0010 #include "desktopexecparser.h" 0011 #ifndef Q_OS_ANDROID 0012 #include "kiofuse_interface.h" 0013 #endif 0014 0015 #include <KApplicationTrader> 0016 #include <KConfigGroup> 0017 #include <KDesktopFile> 0018 #include <KLocalizedString> 0019 #include <KMacroExpander> 0020 #include <KService> 0021 #include <KSharedConfig> 0022 #include <KShell> 0023 #include <kprotocolinfo.h> // KF6 TODO remove after moving hasSchemeHandler to OpenUrlJob 0024 0025 #ifndef Q_OS_ANDROID 0026 #include <QDBusConnection> 0027 #include <QDBusReply> 0028 #endif 0029 #include <QDir> 0030 #include <QFile> 0031 #include <QProcessEnvironment> 0032 #include <QStandardPaths> 0033 #include <QUrl> 0034 0035 #include <config-kiocore.h> // KDE_INSTALL_FULL_LIBEXECDIR_KF 0036 0037 #include "kiocoredebug.h" 0038 0039 class KRunMX1 : public KMacroExpanderBase 0040 { 0041 public: 0042 explicit KRunMX1(const KService &_service) 0043 : KMacroExpanderBase(QLatin1Char('%')) 0044 , hasUrls(false) 0045 , hasSpec(false) 0046 , service(_service) 0047 { 0048 } 0049 0050 bool hasUrls; 0051 bool hasSpec; 0052 0053 protected: 0054 int expandEscapedMacro(const QString &str, int pos, QStringList &ret) override; 0055 0056 private: 0057 const KService &service; 0058 }; 0059 0060 int KRunMX1::expandEscapedMacro(const QString &str, int pos, QStringList &ret) 0061 { 0062 uint option = str[pos + 1].unicode(); 0063 switch (option) { 0064 case 'c': 0065 ret << service.name().replace(QLatin1Char('%'), QLatin1String("%%")); 0066 break; 0067 case 'k': 0068 ret << service.entryPath().replace(QLatin1Char('%'), QLatin1String("%%")); 0069 break; 0070 case 'i': 0071 ret << QStringLiteral("--icon") << service.icon().replace(QLatin1Char('%'), QLatin1String("%%")); 0072 break; 0073 case 'm': 0074 // ret << "-miniicon" << service.icon().replace( '%', "%%" ); 0075 qCWarning(KIO_CORE) << "-miniicon isn't supported anymore (service" << service.name() << ')'; 0076 break; 0077 case 'u': 0078 case 'U': 0079 hasUrls = true; 0080 Q_FALLTHROUGH(); 0081 /* fallthrough */ 0082 case 'f': 0083 case 'F': 0084 case 'n': 0085 case 'N': 0086 case 'd': 0087 case 'D': 0088 case 'v': 0089 hasSpec = true; 0090 Q_FALLTHROUGH(); 0091 /* fallthrough */ 0092 default: 0093 return -2; // subst with same and skip 0094 } 0095 return 2; 0096 } 0097 0098 class KRunMX2 : public KMacroExpanderBase 0099 { 0100 public: 0101 explicit KRunMX2(const QList<QUrl> &_urls) 0102 : KMacroExpanderBase(QLatin1Char('%')) 0103 , ignFile(false) 0104 , urls(_urls) 0105 { 0106 } 0107 0108 bool ignFile; 0109 0110 protected: 0111 int expandEscapedMacro(const QString &str, int pos, QStringList &ret) override; 0112 0113 private: 0114 void subst(int option, const QUrl &url, QStringList &ret); 0115 0116 const QList<QUrl> &urls; 0117 }; 0118 0119 void KRunMX2::subst(int option, const QUrl &url, QStringList &ret) 0120 { 0121 switch (option) { 0122 case 'u': 0123 ret << ((url.isLocalFile() && url.fragment().isNull() && url.query().isNull()) ? QDir::toNativeSeparators(url.toLocalFile()) : url.toString()); 0124 break; 0125 case 'd': 0126 ret << url.adjusted(QUrl::RemoveFilename).path(); 0127 break; 0128 case 'f': 0129 ret << QDir::toNativeSeparators(url.toLocalFile()); 0130 break; 0131 case 'n': 0132 ret << url.fileName(); 0133 break; 0134 case 'v': 0135 if (url.isLocalFile() && QFile::exists(url.toLocalFile())) { 0136 ret << KDesktopFile(url.toLocalFile()).desktopGroup().readEntry("Dev"); 0137 } 0138 break; 0139 } 0140 return; 0141 } 0142 0143 int KRunMX2::expandEscapedMacro(const QString &str, int pos, QStringList &ret) 0144 { 0145 uint option = str[pos + 1].unicode(); 0146 switch (option) { 0147 case 'f': 0148 case 'u': 0149 case 'n': 0150 case 'd': 0151 case 'v': 0152 if (urls.isEmpty()) { 0153 if (!ignFile) { 0154 // qCDebug(KIO_CORE) << "No URLs supplied to single-URL service" << str; 0155 } 0156 } else if (urls.count() > 1) { 0157 qCWarning(KIO_CORE) << urls.count() << "URLs supplied to single-URL service" << str; 0158 } else { 0159 subst(option, urls.first(), ret); 0160 } 0161 break; 0162 case 'F': 0163 case 'U': 0164 case 'N': 0165 case 'D': 0166 option += 'a' - 'A'; 0167 for (const QUrl &url : urls) { 0168 subst(option, url, ret); 0169 } 0170 break; 0171 case '%': 0172 ret = QStringList(QStringLiteral("%")); 0173 break; 0174 default: 0175 return -2; // subst with same and skip 0176 } 0177 return 2; 0178 } 0179 0180 QStringList KIO::DesktopExecParser::supportedProtocols(const KService &service) 0181 { 0182 QStringList supportedProtocols = service.property(QStringLiteral("X-KDE-Protocols")).toStringList(); 0183 KRunMX1 mx1(service); 0184 QString exec = service.exec(); 0185 if (mx1.expandMacrosShellQuote(exec) && !mx1.hasUrls) { 0186 if (!supportedProtocols.isEmpty()) { 0187 qCWarning(KIO_CORE) << service.entryPath() << "contains a X-KDE-Protocols line but doesn't use %u or %U in its Exec line! This is inconsistent."; 0188 } 0189 return QStringList(); 0190 } else { 0191 if (supportedProtocols.isEmpty()) { 0192 // compat mode: assume KIO if not set and it's a KDE app (or a KDE service) 0193 const QStringList categories = service.property(QStringLiteral("Categories")).toStringList(); 0194 if (categories.contains(QLatin1String("KDE")) || !service.isApplication() || service.entryPath().isEmpty() /*temp service*/) { 0195 supportedProtocols.append(QStringLiteral("KIO")); 0196 } else { // if no KDE app, be a bit over-generic 0197 supportedProtocols.append(QStringLiteral("http")); 0198 supportedProtocols.append(QStringLiteral("https")); // #253294 0199 supportedProtocols.append(QStringLiteral("ftp")); 0200 } 0201 } 0202 } 0203 0204 // add x-scheme-handler/<protocol> 0205 const QLatin1String xScheme("x-scheme-handler/"); 0206 const auto servicesTypes = service.serviceTypes(); 0207 for (const auto &mimeType : servicesTypes) { 0208 if (mimeType.startsWith(xScheme)) { 0209 supportedProtocols << mimeType.mid(xScheme.size()); 0210 } 0211 } 0212 0213 // qCDebug(KIO_CORE) << "supportedProtocols:" << supportedProtocols; 0214 return supportedProtocols; 0215 } 0216 0217 bool KIO::DesktopExecParser::isProtocolInSupportedList(const QUrl &url, const QStringList &supportedProtocols) 0218 { 0219 return url.isLocalFile() // 0220 || supportedProtocols.contains(QLatin1String("KIO")) // 0221 || supportedProtocols.contains(url.scheme(), Qt::CaseInsensitive); 0222 } 0223 0224 // We have up to two sources of data, for protocols not handled by KIO workers (so called "helper") : 0225 // 1) the exec line of the .protocol file, if there's one 0226 // 2) the application associated with x-scheme-handler/<protocol> if there's one 0227 bool KIO::DesktopExecParser::hasSchemeHandler(const QUrl &url) // KF6 TODO move to OpenUrlJob 0228 { 0229 if (KProtocolInfo::isHelperProtocol(url)) { 0230 return true; 0231 } 0232 const KService::Ptr service = KApplicationTrader::preferredService(QLatin1String("x-scheme-handler/") + url.scheme()); 0233 if (service) { 0234 qCDebug(KIO_CORE) << QLatin1String("preferred service for x-scheme-handler/") + url.scheme() << service->desktopEntryName(); 0235 } 0236 return service; 0237 } 0238 0239 class KIO::DesktopExecParserPrivate 0240 { 0241 public: 0242 DesktopExecParserPrivate(const KService &_service, const QList<QUrl> &_urls) 0243 : service(_service) 0244 , urls(_urls) 0245 , tempFiles(false) 0246 { 0247 } 0248 0249 bool isUrlSupported(const QUrl &url, const QStringList &supportedProtocols); 0250 0251 const KService &service; 0252 QList<QUrl> urls; 0253 bool tempFiles; 0254 QString suggestedFileName; 0255 QString m_errorString; 0256 }; 0257 0258 KIO::DesktopExecParser::DesktopExecParser(const KService &service, const QList<QUrl> &urls) 0259 : d(new DesktopExecParserPrivate(service, urls)) 0260 { 0261 } 0262 0263 KIO::DesktopExecParser::~DesktopExecParser() 0264 { 0265 } 0266 0267 void KIO::DesktopExecParser::setUrlsAreTempFiles(bool tempFiles) 0268 { 0269 d->tempFiles = tempFiles; 0270 } 0271 0272 void KIO::DesktopExecParser::setSuggestedFileName(const QString &suggestedFileName) 0273 { 0274 d->suggestedFileName = suggestedFileName; 0275 } 0276 0277 static const QString kioexecPath() 0278 { 0279 QString kioexec = QCoreApplication::applicationDirPath() + QLatin1String("/kioexec"); 0280 if (!QFileInfo::exists(kioexec)) { 0281 kioexec = QStringLiteral(KDE_INSTALL_FULL_LIBEXECDIR_KF "/kioexec"); 0282 } 0283 Q_ASSERT(QFileInfo::exists(kioexec)); 0284 return kioexec; 0285 } 0286 0287 static QString findNonExecutableProgram(const QString &executable) 0288 { 0289 // Relative to current dir, or absolute path 0290 const QFileInfo fi(executable); 0291 if (fi.exists() && !fi.isExecutable()) { 0292 return executable; 0293 } 0294 0295 #ifdef Q_OS_UNIX 0296 // This is a *very* simplified version of QStandardPaths::findExecutable 0297 const QStringList searchPaths = QString::fromLocal8Bit(qgetenv("PATH")).split(QDir::listSeparator(), Qt::SkipEmptyParts); 0298 for (const QString &searchPath : searchPaths) { 0299 const QString candidate = searchPath + QLatin1Char('/') + executable; 0300 const QFileInfo fileInfo(candidate); 0301 if (fileInfo.exists()) { 0302 if (fileInfo.isExecutable()) { 0303 qWarning() << "Internal program error. QStandardPaths::findExecutable couldn't find" << executable << "but our own logic found it at" 0304 << candidate << ". Please report a bug at https://bugs.kde.org"; 0305 } else { 0306 return candidate; 0307 } 0308 } 0309 } 0310 #endif 0311 return QString(); 0312 } 0313 0314 bool KIO::DesktopExecParserPrivate::isUrlSupported(const QUrl &url, const QStringList &protocols) 0315 { 0316 if (KIO::DesktopExecParser::isProtocolInSupportedList(url, protocols)) { 0317 return true; 0318 } 0319 0320 // supportedProtocols() only checks whether the .desktop file has MimeType=x-scheme-handler/xxx 0321 // We also want to check whether the app has been set as default/associated in mimeapps.list 0322 const auto handlers = KApplicationTrader::queryByMimeType(QLatin1String("x-scheme-handler/") + url.scheme()); 0323 for (const KService::Ptr &handler : handlers) { 0324 if (handler->desktopEntryName() == service.desktopEntryName()) { 0325 return true; 0326 } 0327 } 0328 0329 return false; 0330 } 0331 0332 QStringList KIO::DesktopExecParser::resultingArguments() const 0333 { 0334 QString exec = d->service.exec(); 0335 if (exec.isEmpty()) { 0336 d->m_errorString = i18n("No Exec field in %1", d->service.entryPath()); 0337 qCWarning(KIO_CORE) << "No Exec field in" << d->service.entryPath(); 0338 return QStringList(); 0339 } 0340 0341 // Extract the name of the binary to execute from the full Exec line, to see if it exists 0342 const QString binary = executablePath(exec); 0343 QString executableFullPath; 0344 if (!binary.isEmpty()) { // skip all this if the Exec line is a complex shell command 0345 if (QDir::isRelativePath(binary)) { 0346 // Resolve the executable to ensure that helpers in libexec are found. 0347 // Too bad for commands that need a shell - they must reside in $PATH. 0348 executableFullPath = QStandardPaths::findExecutable(binary); 0349 if (executableFullPath.isEmpty()) { 0350 executableFullPath = QFile::decodeName(KDE_INSTALL_FULL_LIBEXECDIR_KF "/") + binary; 0351 } 0352 } else { 0353 executableFullPath = binary; 0354 } 0355 0356 // Now check that the binary exists and has the executable flag 0357 if (!QFileInfo(executableFullPath).isExecutable()) { 0358 // Does it really not exist, or is it non-executable (on Unix)? (bug #415567) 0359 const QString nonExecutable = findNonExecutableProgram(binary); 0360 if (nonExecutable.isEmpty()) { 0361 d->m_errorString = i18n("Could not find the program '%1'", binary); 0362 } else { 0363 if (QDir::isRelativePath(binary)) { 0364 d->m_errorString = i18n("The program '%1' was found at '%2' but it is missing executable permissions.", binary, nonExecutable); 0365 } else { 0366 d->m_errorString = i18n("The program '%1' is missing executable permissions.", nonExecutable); 0367 } 0368 } 0369 return QStringList(); 0370 } 0371 } 0372 0373 QStringList result; 0374 bool appHasTempFileOption; 0375 0376 KRunMX1 mx1(d->service); 0377 KRunMX2 mx2(d->urls); 0378 0379 if (!mx1.expandMacrosShellQuote(exec)) { // Error in shell syntax 0380 d->m_errorString = i18n("Syntax error in command %1 coming from %2", exec, d->service.entryPath()); 0381 qCWarning(KIO_CORE) << "Syntax error in command" << d->service.exec() << ", service" << d->service.name(); 0382 return QStringList(); 0383 } 0384 0385 // FIXME: the current way of invoking kioexec disables term and su use 0386 0387 // Check if we need "tempexec" (kioexec in fact) 0388 appHasTempFileOption = d->tempFiles && d->service.property(QStringLiteral("X-KDE-HasTempFileOption")).toBool(); 0389 if (d->tempFiles && !appHasTempFileOption && d->urls.size()) { 0390 result << kioexecPath() << QStringLiteral("--tempfiles") << exec; 0391 if (!d->suggestedFileName.isEmpty()) { 0392 result << QStringLiteral("--suggestedfilename"); 0393 result << d->suggestedFileName; 0394 } 0395 result += QUrl::toStringList(d->urls); 0396 return result; 0397 } 0398 0399 // Return true for non-KIO desktop files with explicit X-KDE-Protocols list, like vlc, for the special case below 0400 auto isNonKIO = [this]() { 0401 const QStringList protocols = d->service.property(QStringLiteral("X-KDE-Protocols")).toStringList(); 0402 return !protocols.isEmpty() && !protocols.contains(QLatin1String("KIO")); 0403 }; 0404 0405 // Check if we need kioexec, or KIOFuse 0406 bool useKioexec = false; 0407 #ifndef Q_OS_ANDROID 0408 org::kde::KIOFuse::VFS kiofuse_iface(QStringLiteral("org.kde.KIOFuse"), QStringLiteral("/org/kde/KIOFuse"), QDBusConnection::sessionBus()); 0409 struct MountRequest { 0410 QDBusPendingReply<QString> reply; 0411 int urlIndex; 0412 }; 0413 QVector<MountRequest> requests; 0414 requests.reserve(d->urls.count()); 0415 0416 const QStringList appSupportedProtocols = supportedProtocols(d->service); 0417 for (int i = 0; i < d->urls.count(); ++i) { 0418 const QUrl url = d->urls.at(i); 0419 const bool supported = mx1.hasUrls ? d->isUrlSupported(url, appSupportedProtocols) : url.isLocalFile(); 0420 if (!supported) { 0421 // If FUSE fails, and there is no scheme handler, we'll have to fallback to kioexec 0422 useKioexec = true; 0423 } 0424 0425 // NOTE: Some non-KIO apps may support the URLs (e.g. VLC supports smb://) 0426 // but will not have the password if they are not in the URL itself. 0427 // Hence convert URL to KIOFuse equivalent in case there is a password. 0428 // @see https://pointieststick.com/2018/01/17/videos-on-samba-shares/ 0429 // @see https://bugs.kde.org/show_bug.cgi?id=330192 0430 if (!supported || (!url.userName().isEmpty() && url.password().isEmpty() && isNonKIO())) { 0431 requests.push_back({kiofuse_iface.mountUrl(url.toString()), i}); 0432 } 0433 } 0434 0435 for (auto &request : requests) { 0436 request.reply.waitForFinished(); 0437 } 0438 const bool fuseError = std::any_of(requests.cbegin(), requests.cend(), [](const MountRequest &request) { 0439 return request.reply.isError(); 0440 }); 0441 0442 if (fuseError && useKioexec) { 0443 // We need to run the app through kioexec 0444 result << kioexecPath(); 0445 if (d->tempFiles) { 0446 result << QStringLiteral("--tempfiles"); 0447 } 0448 if (!d->suggestedFileName.isEmpty()) { 0449 result << QStringLiteral("--suggestedfilename"); 0450 result << d->suggestedFileName; 0451 } 0452 result << exec; 0453 result += QUrl::toStringList(d->urls); 0454 return result; 0455 } 0456 0457 // At this point we know we're not using kioexec, so feel free to replace 0458 // KIO URLs with their KIOFuse local path. 0459 for (const auto &request : std::as_const(requests)) { 0460 if (!request.reply.isError()) { 0461 d->urls[request.urlIndex] = QUrl::fromLocalFile(request.reply.value()); 0462 } 0463 } 0464 #endif 0465 0466 if (appHasTempFileOption) { 0467 exec += QLatin1String(" --tempfile"); 0468 } 0469 0470 // Did the user forget to append something like '%f'? 0471 // If so, then assume that '%f' is the right choice => the application 0472 // accepts only local files. 0473 if (!mx1.hasSpec) { 0474 exec += QLatin1String(" %f"); 0475 mx2.ignFile = true; 0476 } 0477 0478 mx2.expandMacrosShellQuote(exec); // syntax was already checked, so don't check return value 0479 0480 /* 0481 1 = need_shell, 2 = terminal, 4 = su 0482 0483 0 << split(cmd) 0484 1 << "sh" << "-c" << cmd 0485 2 << split(term) << "-e" << split(cmd) 0486 3 << split(term) << "-e" << "sh" << "-c" << cmd 0487 0488 4 << "kdesu" << "-u" << user << "-c" << cmd 0489 5 << "kdesu" << "-u" << user << "-c" << ("sh -c " + quote(cmd)) 0490 6 << split(term) << "-e" << "su" << user << "-c" << cmd 0491 7 << split(term) << "-e" << "su" << user << "-c" << ("sh -c " + quote(cmd)) 0492 0493 "sh -c" is needed in the "su" case, too, as su uses the user's login shell, not sh. 0494 this could be optimized with the -s switch of some su versions (e.g., debian linux). 0495 */ 0496 0497 if (d->service.terminal()) { 0498 KConfigGroup cg(KSharedConfig::openConfig(), "General"); 0499 QString terminal = cg.readPathEntry("TerminalApplication", QStringLiteral("konsole")); 0500 0501 const bool isKonsole = (terminal == QLatin1String("konsole")); 0502 QStringList terminalParts = KShell::splitArgs(terminal); 0503 QString terminalPath; 0504 if (!terminalParts.isEmpty()) { 0505 terminalPath = QStandardPaths::findExecutable(terminalParts.at(0)); 0506 } 0507 0508 if (terminalPath.isEmpty()) { 0509 d->m_errorString = i18n("Terminal %1 not found while trying to run %2", terminal, d->service.entryPath()); 0510 qCWarning(KIO_CORE) << "Terminal" << terminal << "not found, service" << d->service.name(); 0511 return QStringList(); 0512 } 0513 terminalParts[0] = terminalPath; 0514 terminal = KShell::joinArgs(terminalParts); 0515 if (isKonsole) { 0516 if (!d->service.workingDirectory().isEmpty()) { 0517 terminal += QLatin1String(" --workdir ") + KShell::quoteArg(d->service.workingDirectory()); 0518 } 0519 terminal += QLatin1String(" -qwindowtitle '%c'"); 0520 if (!d->service.icon().isEmpty()) { 0521 terminal += QLatin1String(" -qwindowicon ") + KShell::quoteArg(d->service.icon().replace(QLatin1Char('%'), QLatin1String("%%"))); 0522 } 0523 } 0524 terminal += QLatin1Char(' ') + d->service.terminalOptions(); 0525 if (!mx1.expandMacrosShellQuote(terminal)) { 0526 d->m_errorString = i18n("Syntax error in command %1 while trying to run %2", terminal, d->service.entryPath()); 0527 qCWarning(KIO_CORE) << "Syntax error in command" << terminal << ", service" << d->service.name(); 0528 return QStringList(); 0529 } 0530 mx2.expandMacrosShellQuote(terminal); 0531 result = KShell::splitArgs(terminal); // assuming that the term spec never needs a shell! 0532 result << QStringLiteral("-e"); 0533 } 0534 0535 KShell::Errors err; 0536 QStringList execlist = KShell::splitArgs(exec, KShell::AbortOnMeta | KShell::TildeExpand, &err); 0537 if (!executableFullPath.isEmpty()) { 0538 execlist[0] = executableFullPath; 0539 } 0540 0541 if (d->service.substituteUid()) { 0542 if (d->service.terminal()) { 0543 result << QStringLiteral("su"); 0544 } else { 0545 QString kdesu = QFile::decodeName(KDE_INSTALL_FULL_LIBEXECDIR_KF "/kdesu"); 0546 if (!QFile::exists(kdesu)) { 0547 kdesu = QStandardPaths::findExecutable(QStringLiteral("kdesu")); 0548 } 0549 if (!QFile::exists(kdesu)) { 0550 // Insert kdesu as string so we show a nice warning: 'Could not launch kdesu' 0551 result << QStringLiteral("kdesu"); 0552 return result; 0553 } else { 0554 result << kdesu << QStringLiteral("-u"); 0555 } 0556 } 0557 0558 result << d->service.username() << QStringLiteral("-c"); 0559 if (err == KShell::FoundMeta) { 0560 exec = QLatin1String("/bin/sh -c ") + KShell::quoteArg(exec); 0561 } else { 0562 exec = KShell::joinArgs(execlist); 0563 } 0564 result << exec; 0565 } else { 0566 if (err == KShell::FoundMeta) { 0567 result << QStringLiteral("/bin/sh") << QStringLiteral("-c") << exec; 0568 } else { 0569 result += execlist; 0570 } 0571 } 0572 0573 return result; 0574 } 0575 0576 QString KIO::DesktopExecParser::errorMessage() const 0577 { 0578 return d->m_errorString; 0579 } 0580 0581 // static 0582 QString KIO::DesktopExecParser::executableName(const QString &execLine) 0583 { 0584 const QString bin = executablePath(execLine); 0585 return bin.mid(bin.lastIndexOf(QLatin1Char('/')) + 1); 0586 } 0587 0588 // static 0589 QString KIO::DesktopExecParser::executablePath(const QString &execLine) 0590 { 0591 // Remove parameters and/or trailing spaces. 0592 const QStringList args = KShell::splitArgs(execLine, KShell::AbortOnMeta | KShell::TildeExpand); 0593 auto it = std::find_if(args.cbegin(), args.cend(), [](const QString &arg) { 0594 return !arg.contains(QLatin1Char('=')); 0595 }); 0596 return it != args.cend() ? *it : QString{}; 0597 }