/* * Copyright (C) 2020-2022 Roy Qu (royqh1979@gmail.com) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "api.h" #include #include #include #include #include #include #include #include #ifdef Q_OS_WINDOWS # include #endif #include #include "utils.h" #include "settings.h" #include "thememanager.h" #include "runtime.h" #ifdef Q_OS_WINDOWS // added in Windows 11 21H2, declare our version to support old SDKs. constexpr int win32_flag_UserEnabled = 0x00000001; // added in Windows 11 21H2, use GetProcAddress to detect availability. using pGetMachineTypeAttributes_t = HRESULT (WINAPI *)( USHORT Machine, int *MachineTypeAttributes ); // added in Windows 10 1709, use GetProcAddress to detect availability. using pIsWow64GuestMachineSupported_t = HRESULT (WINAPI *)( USHORT WowGuestMachine, BOOL *MachineIsSupported ); using pIsWow64Process2_t = BOOL (WINAPI *)( HANDLE hProcess, USHORT *pProcessMachine, USHORT *pNativeMachine ); #endif static QString luaDump(const QJsonValue &value) { if (value.isNull()) return "nil"; if (value.isBool()) return value.toBool() ? "true" : "false"; if (value.isDouble()) return QString::number(value.toDouble()); if (value.isString()) { QString s = value.toString(); s.replace('\\', "\\\\"); s.replace('"', "\\\""); s.replace('\n', "\\n"); s.replace('\r', "\\r"); s.replace('\t', "\\t"); return '"' + s + '"'; } if (value.isArray()) { QString s = "{"; for (const QJsonValue &v : value.toArray()) s += luaDump(v) + ','; s += '}'; return s; } if (value.isObject()) { QJsonObject o = value.toObject(); QString s = "{"; for (const QString &k : o.keys()) s += '[' + luaDump(k) + "]=" + luaDump(o[k]) + ','; s += '}'; return s; } return "nil"; } static QString stringify(const QJsonValue &value) { if (value.isString()) return value.toString(); if (value.isNull()) return "[nil]"; if (value.isBool()) return value.toBool() ? "[true]" : "[false]"; if (value.isDouble()) return QString("[number %1]").arg(value.toDouble()); if (value.isArray() || value.isObject()) return QString("[table %1]").arg(luaDump(value)); return "[unknown]"; } /* https://stackoverflow.com/questions/15687223/explicit-call-to-destructor-before-longjmp-croak * * C++11 18.10/4: A setjmp/longjmp call pair has undefined behavior if * replacing the setjmp and longjmp by catch and throw would invoke any * non-trivial destructors for any automatic objects. * * Use impl function so the API boundary contains only POD types. */ void luaApiImpl_Debug_debug(lua_State *L) { QString s = AddOn::RaiiLuaState::fetchString(L, 1); int nArgs = lua_gettop(L); for (int i = 2; i <= nArgs; ++i) { QJsonValue arg; try { arg = AddOn::RaiiLuaState::fetch(L, i); } catch (const AddOn::LuaError &e) { QString reason = e.reason() + QString(" ('C_Debug.debug' argument #%1)").arg(i); throw AddOn::LuaError(reason); } s = s.arg(stringify(arg)); } qDebug().noquote() << s; } // C_Debug.debug(string, ...) -> () extern "C" int luaApi_Debug_debug(lua_State *L) noexcept { bool error = false; try { luaApiImpl_Debug_debug(L); } catch (const AddOn::LuaError &e) { error = true; AddOn::RaiiLuaState::push(L, e.reason()); } if (error) { lua_error(L); // longjmp, never returns __builtin_unreachable(); } else { return 0; } } void luaApiImpl_Debug_messageBox(lua_State *L) { QString s = AddOn::RaiiLuaState::fetchString(L, 1); int nArgs = lua_gettop(L); for (int i = 2; i <= nArgs; ++i) { QJsonValue arg; try { arg = AddOn::RaiiLuaState::fetch(L, i); } catch (const AddOn::LuaError &e) { QString reason = e.reason() + QString(" ('C_Debug.messageBox' argument #%1)").arg(i); throw AddOn::LuaError(reason); } s = s.arg(stringify(arg)); } QMessageBox::information(nullptr, "Debug", s); } // C_Debug.messageBox(string, ...) -> () extern "C" int luaApi_Debug_messageBox(lua_State *L) noexcept { bool error = false; try { luaApiImpl_Debug_messageBox(L); } catch (const AddOn::LuaError &e) { error = true; AddOn::RaiiLuaState::push(L, e.reason()); } if (error) { lua_error(L); // longjmp, never returns __builtin_unreachable(); } else { return 0; } } // C_Desktop.desktopEnvironment() -> string extern "C" int luaApi_Desktop_desktopEnvironment(lua_State *L) noexcept { #if defined(Q_OS_WIN32) // exclude WinRT intentionally lua_pushliteral(L, "windows"); #elif defined(Q_OS_MACOS) lua_pushliteral(L, "macos"); #elif (defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)) || defined(Q_OS_HURD) || defined(Q_OS_FREEBSD) || defined(Q_OS_NETBSD) || defined(Q_OS_OPENBSD) || defined(Q_OS_SOLARIS) // desktops that follows to freedesktop.org specs, i.e. GNU/Linux, GNU/Hurd, BSD, Solaris (illumos) lua_pushliteral(L, "xdg"); #else lua_pushliteral(L, "unknown"); #endif return 1; } // C_Desktop.language() -> string extern "C" int luaApi_Desktop_language(lua_State *L) noexcept { QString lang = pSettings->environment().language(); AddOn::RaiiLuaState::push(L, lang); return 1; } // C_Desktop.qtStyleList() -> [string] extern "C" int luaApi_Desktop_qtStyleList(lua_State *L) noexcept { QStringList styles = QStyleFactory::keys(); AddOn::RaiiLuaState::push(L, styles); return 1; } // C_Desktop.systemAppMode() -> string extern "C" int luaApi_Desktop_systemAppMode(lua_State *L) noexcept { if (AppTheme::isSystemInDarkMode()) lua_pushliteral(L, "dark"); else lua_pushliteral(L, "light"); return 1; } // C_Desktop.systemStyle() -> string extern "C" int luaApi_Desktop_systemStyle(lua_State *L) noexcept { AddOn::RaiiLuaState::push(L, AppTheme::initialStyle()); return 1; } // C_FileSystem.exists(string) -> bool extern "C" int luaApi_FileSystem_exists(lua_State *L) noexcept { QString path = AddOn::RaiiLuaState::fetchString(L, 1); QFileInfo fileInfo(path); AddOn::RaiiLuaState::push(L, fileInfo.exists()); return 1; } // C_FileSystem.isExecutable(string) -> bool extern "C" int luaApi_FileSystem_isExecutable(lua_State *L) noexcept { QString path = AddOn::RaiiLuaState::fetchString(L, 1); QFileInfo fileInfo(path); bool isExecutable = fileInfo.isFile() && fileInfo.isReadable() && fileInfo.isExecutable(); AddOn::RaiiLuaState::push(L, isExecutable); return 1; } // C_FileSystem.matchFiles(string, string) -> {string} extern "C" int luaApi_FileSystem_matchFiles(lua_State *L) noexcept { QString dir = AddOn::RaiiLuaState::fetchString(L, 1); QString pattern = AddOn::RaiiLuaState::fetchString(L, 2); QRegularExpression re(pattern); QStringList result; QDirIterator it(dir, QDir::Files); while (it.hasNext()) { it.next(); if (re.match(it.fileName()).hasMatch()) result.append(it.fileName()); } std::sort(result.begin(), result.end()); AddOn::RaiiLuaState::push(L, result); return 1; } // C_System.appArch() -> string extern "C" int luaApi_System_appArch(lua_State *L) noexcept { AddOn::RaiiLuaState::push(L, appArch()); return 1; } // C_System.appDir() -> string extern "C" int luaApi_System_appDir(lua_State *L) noexcept { QString appDir = pSettings->dirs().appDir(); AddOn::RaiiLuaState::push(L, appDir); return 1; } // C_System.appLibexecDir() -> string extern "C" int luaApi_System_appLibexecDir(lua_State *L) noexcept { QString appLibexecDir = pSettings->dirs().appLibexecDir(); AddOn::RaiiLuaState::push(L, appLibexecDir); return 1; } // C_System.appResourceDir() -> string extern "C" int luaApi_System_appResourceDir(lua_State *L) noexcept { QString appResourceDir = pSettings->dirs().appResourceDir(); AddOn::RaiiLuaState::push(L, appResourceDir); return 1; } // C_System.osArch() -> string extern "C" int luaApi_System_osArch(lua_State *L) noexcept { AddOn::RaiiLuaState::push(L, osArch()); return 1; } void luaApiImpl_System_popen(lua_State *L) { using namespace std::chrono; QMetaEnum exitStatusEnum = QMetaEnum::fromType(); QMetaEnum processErrorEnum = QMetaEnum::fromType(); QString prog = AddOn::RaiiLuaState::fetchString(L, 1); QStringList args; try { QJsonArray argsArray = AddOn::RaiiLuaState::fetchArray(L, 2); for (const QJsonValue &arg : argsArray) args.append(arg.toString()); } catch (const AddOn::LuaError &e) { QString reason = e.reason() + " ('C_System.popen' argument #2)"; throw AddOn::LuaError(reason); } // fetch and normalize timeout microseconds timeout; AddOn::LuaExtraState &extraState = AddOn::RaiiLuaState::extraState(L); try { QJsonObject option; if (lua_type(L, 3) == LUA_TTABLE) option = AddOn::RaiiLuaState::fetchObject(L, 3); if (option.contains("timeout")) timeout = milliseconds(option["timeout"].toInt()); else timeout = AddOn::RaiiLuaState::extraState(L).timeLimit; } catch (const AddOn::LuaError &e) { QString reason = e.reason() + " ('C_System.popen' argument #3)"; throw AddOn::LuaError(reason); } time_point deadline = system_clock::now() + timeout; if (deadline > extraState.timeStart + timeout) { deadline = extraState.timeStart + timeout; timeout = duration_cast(deadline - system_clock::now()); } QProcess process; process.setProgram(prog); process.setArguments(args); process.start(QIODevice::ReadOnly); if (!process.waitForStarted(duration_cast(timeout).count())) { QJsonObject result{ {"exitStatus", exitStatusEnum.valueToKey(QProcess::CrashExit)}, {"error", processErrorEnum.valueToKey(process.error())}, }; AddOn::RaiiLuaState::push(L, ""); AddOn::RaiiLuaState::push(L, ""); AddOn::RaiiLuaState::push(L, result); return; } process.closeWriteChannel(); timeout = duration_cast(deadline - system_clock::now()); process.waitForFinished(duration_cast(timeout).count()); QByteArray stdoutData = process.readAllStandardOutput(); QByteArray stderrData = process.readAllStandardError(); if ( QProcess::ExitStatus exitStatus = process.exitStatus(); exitStatus == QProcess::NormalExit ) { QJsonObject result{ {"exitStatus", exitStatusEnum.valueToKey(exitStatus)}, {"exitCode", process.exitCode()}, }; AddOn::RaiiLuaState::push(L, QString::fromUtf8(stdoutData)); AddOn::RaiiLuaState::push(L, QString::fromUtf8(stderrData)); AddOn::RaiiLuaState::push(L, result); } else { QJsonObject result{ {"exitStatus", exitStatusEnum.valueToKey(exitStatus)}, {"error", processErrorEnum.valueToKey(process.error())}, }; AddOn::RaiiLuaState::push(L, ""); AddOn::RaiiLuaState::push(L, QString::fromUtf8(stderrData)); AddOn::RaiiLuaState::push(L, result); } } // C_System.popen(string, {string}, PopenOption) -> string, string, PopenResult extern "C" int luaApi_System_popen(lua_State *L) noexcept { bool error = false; try { luaApiImpl_System_popen(L); } catch (const AddOn::LuaError &e) { error = true; AddOn::RaiiLuaState::push(L, e.reason()); } if (error) { lua_error(L); // longjmp, never returns __builtin_unreachable(); } else { return 3; } } static QString qtArchitectureNormalization(const QString &arch) { // adjust QEMU user emulation arches to match QSysInfo::currentCpuArchitecture if (arch == "aarch64") return "arm64"; if (arch.startsWith("ppc")) return "power" + arch.mid(3); if (arch == "sparc64") return "sparcv9"; return arch; } // C_System.supportedAppArchList() -> [string] extern "C" int luaApi_System_supportedAppArchList(lua_State *L) noexcept { #ifdef Q_OS_WINDOWS QSet arches; pGetMachineTypeAttributes_t pGetMachineTypeAttributes = reinterpret_cast(GetProcAddress( GetModuleHandleW(L"kernel32"), "GetMachineTypeAttributes")); if (pGetMachineTypeAttributes) { // the direct way int result; if (pGetMachineTypeAttributes(IMAGE_FILE_MACHINE_I386, &result) == 0 && result & win32_flag_UserEnabled) arches.insert("i386"); if (pGetMachineTypeAttributes(IMAGE_FILE_MACHINE_AMD64, &result) == 0 && result & win32_flag_UserEnabled) arches.insert("x86_64"); if (pGetMachineTypeAttributes(IMAGE_FILE_MACHINE_ARMNT, &result) == 0 && result & win32_flag_UserEnabled) arches.insert("arm"); if (pGetMachineTypeAttributes(IMAGE_FILE_MACHINE_ARM64, &result) == 0 && result & win32_flag_UserEnabled) arches.insert("arm64"); } else { pIsWow64GuestMachineSupported_t pIsWow64GuestMachineSupported = reinterpret_cast(GetProcAddress( GetModuleHandleW(L"kernel32"), "IsWow64GuestMachineSupported")); pIsWow64Process2_t pIsWow64Process2 = reinterpret_cast(GetProcAddress( GetModuleHandleW(L"kernel32"), "IsWow64Process2")); if (pIsWow64GuestMachineSupported && pIsWow64Process2) { // recent Windows 10, native + WoW64 arches // IsWow64Process2 returns real native architecture under xtajit, while GetNativeSystemInfo does not. USHORT processMachineResult, nativeMachineResult; if (pIsWow64Process2(GetCurrentProcess(), &processMachineResult, &nativeMachineResult)) switch (nativeMachineResult) { case IMAGE_FILE_MACHINE_I386: arches.insert("i386"); break; case IMAGE_FILE_MACHINE_AMD64: arches.insert("x86_64"); break; case IMAGE_FILE_MACHINE_ARM64: arches.insert("arm64"); break; } BOOL wow64SupportResult; if (pIsWow64GuestMachineSupported(IMAGE_FILE_MACHINE_I386, &wow64SupportResult) == S_OK && wow64SupportResult) arches.insert("i386"); if (pIsWow64GuestMachineSupported(IMAGE_FILE_MACHINE_ARMNT, &wow64SupportResult) == S_OK && wow64SupportResult) arches.insert("arm"); } else { // legacy Windows, hardcode SYSTEM_INFO si; GetNativeSystemInfo(&si); switch (si.wProcessorArchitecture) { case PROCESSOR_ARCHITECTURE_INTEL: arches.insert("i386"); break; case PROCESSOR_ARCHITECTURE_AMD64: arches.insert("i386"); arches.insert("x86_64"); break; case PROCESSOR_ARCHITECTURE_IA32_ON_WIN64: // undocumented but found in Qt source case PROCESSOR_ARCHITECTURE_IA64: // does Red Panda C++ run on IA-64? arches.insert("i386"); arches.insert("ia64"); break; } } } QStringList result{arches.begin(), arches.end()}; AddOn::RaiiLuaState::push(L, result); return 1; #elif defined(Q_OS_MACOS) QStringList result; if (QSysInfo::currentCpuArchitecture() == "arm64") result = QStringList{"arm64", "x86_64"}; else if (QVersionNumber::fromString(QSysInfo::kernelVersion()) >= QVersionNumber(19, 0)) // macOS 10.15+ result = QStringList{"x86_64"}; else result = QStringList{"i386", "x86_64"}; AddOn::RaiiLuaState::push(L, result); return 1; #else QSet arches; arches.insert(QSysInfo::currentCpuArchitecture()); arches.insert(QSysInfo::buildCpuArchitecture()); // read binfmt_misc registry QDir binfmt_misc("/proc/sys/fs/binfmt_misc"); if (binfmt_misc.exists()) { QFileInfoList entries = binfmt_misc.entryInfoList(QDir::Files); for (const QFileInfo &entry : entries) { if (entry.fileName().startsWith("qemu-")) { QString arch = entry.fileName().mid(5); arches.insert(qtArchitectureNormalization(arch)); } } } QStringList result = arches.values(); AddOn::RaiiLuaState::push(L, result); return 1; #endif } #ifdef Q_OS_WINDOWS // C_System.readRegistry(string, string) -> string|nil extern "C" int luaApi_System_readRegistry(lua_State *L) noexcept { QString subKey = AddOn::RaiiLuaState::fetchString(L, 1); QString name = AddOn::RaiiLuaState::fetchString(L, 2); QString value; bool success = readRegistry(HKEY_CURRENT_USER, subKey, name, value); if (success) AddOn::RaiiLuaState::push(L, value); else { success = readRegistry(HKEY_LOCAL_MACHINE, subKey, name, value); if (success) AddOn::RaiiLuaState::push(L, value); else AddOn::RaiiLuaState::push(L, nullptr); } return 1; } #endif void luaApiImpl_Util_format(lua_State *L) { QString s = AddOn::RaiiLuaState::fetchString(L, 1); int nArgs = lua_gettop(L); for (int i = 2; i <= nArgs; ++i) { QJsonValue arg; try { arg = AddOn::RaiiLuaState::fetch(L, i); } catch (const AddOn::LuaError &e) { QString reason = e.reason() + QString(" ('C_Util.format' argument #%1)").arg(i); throw AddOn::LuaError(reason); } s = s.arg(stringify(arg)); } AddOn::RaiiLuaState::push(L, s); } // C_Util.format(string, ...) -> string extern "C" int luaApi_Util_format(lua_State *L) noexcept { bool error = false; try { luaApiImpl_Util_format(L); } catch (const AddOn::LuaError &e) { error = true; AddOn::RaiiLuaState::push(L, e.reason()); } if (error) { lua_error(L); // longjmp, never returns __builtin_unreachable(); } else { return 1; } }