Merge remote-tracking branch 'origin/master' into loadmodule-deferred

This commit is contained in:
Henrik Rydgård committed 2026-10-11 20:00:41 +02:00
commit 4e2d3ab8d0
14 files changed
+267 -57

No files matched your search

+43
View File
@@ -156,6 +156,49 @@ void RequestManager::Clear() {
callbackMap_.clear();
}
static std::mutex g_textInputLock;
static bool g_textInputWanted[(size_t)TextInputUser::COUNT];
static void SendUIEvent(UIEventNotification notification) {
g_requestManager.MakeSystemRequest(SystemRequestType::NOTIFY_UI_EVENT, NO_REQUESTER_TOKEN, nullptr, nullptr, "", "", (int64_t)notification, 0);
}
void System_SetTextInputWanted(TextInputUser user, bool wanted) {
bool anyWanted = false;
{
std::lock_guard<std::mutex> guard(g_textInputLock);
g_textInputWanted[(size_t)user] = wanted;
for (bool w : g_textInputWanted) {
anyWanted = anyWanted || w;
}
}
// GOTFOCUS always goes through, as platforms may need it to show a keyboard again.
if (wanted) {
SendUIEvent(UIEventNotification::TEXT_GOTFOCUS);
} else if (!anyWanted) {
SendUIEvent(UIEventNotification::TEXT_LOSTFOCUS);
}
}
void System_NotifyUIEvent(UIEventNotification notification) {
switch (notification) {
case UIEventNotification::TEXT_GOTFOCUS:
System_SetTextInputWanted(TextInputUser::UI, true);
break;
case UIEventNotification::TEXT_LOSTFOCUS:
System_SetTextInputWanted(TextInputUser::UI, false);
break;
case UIEventNotification::POPUP_CLOSED:
// Platforms may hide an on-screen keyboard on this, but text input itself goes through the above.
System_SetTextInputWanted(TextInputUser::UI, false);
SendUIEvent(notification);
break;
default:
SendUIEvent(notification);
break;
}
}
void System_CreateGameShortcut(const Path &path, std::string_view title) {
g_requestManager.MakeSystemRequest(SystemRequestType::CREATE_GAME_SHORTCUT, NO_REQUESTER_TOKEN, nullptr, nullptr, path.ToString(), title, 0);
}
+11 -3
View File
@@ -182,9 +182,17 @@ inline void System_ShareFile(const Path &path, std::string_view mimeType = "") {
g_requestManager.MakeSystemRequest(SystemRequestType::SHARE_FILE, NO_REQUESTER_TOKEN, nullptr, nullptr, path.ToString(), mimeType, 0);
}
inline void System_NotifyUIEvent(UIEventNotification notification) {
g_requestManager.MakeSystemRequest(SystemRequestType::NOTIFY_UI_EVENT, NO_REQUESTER_TOKEN, nullptr, nullptr, "", "", (int64_t)notification, 0);
}
// TEXT_GOTFOCUS/TEXT_LOSTFOCUS (and POPUP_CLOSED) sent through here count as the regular UI's use of text input.
void System_NotifyUIEvent(UIEventNotification notification);
// Several users can want text input at the same time (for example type-to-search under the imgui debugger).
// The platform only gets TEXT_LOSTFOCUS once none of them does.
enum class TextInputUser {
UI,
ImDebugger,
COUNT,
};
void System_SetTextInputWanted(TextInputUser user, bool wanted);
inline void System_SetKeepScreenBright(bool keepScreenBright) {
g_requestManager.MakeSystemRequest(SystemRequestType::SET_KEEP_SCREEN_BRIGHT, NO_REQUESTER_TOKEN, nullptr, nullptr, "", "", (int64_t)keepScreenBright);
+7 -5
View File
@@ -151,14 +151,11 @@ static ParseParamResult ParseParameterStr(int argc, const char *argv[], size_t &
}
// Check for boolean/inverse-boolean parameter - these don't take a separate param value.
if (param.type == CmdParamType::Bool && equals(arg, std::string("--") + param.longName)) {
// SetValue() does the inverting, so both get "true".
if ((param.type == CmdParamType::Bool || param.type == CmdParamType::BoolInverse) && equals(arg, std::string("--") + param.longName)) {
ParseParamResult result = SetValue(options, param, "true");
i++; // Skip --param
return result;
} else if (param.type == CmdParamType::BoolInverse && equals(arg, std::string("--") + param.longName)) {
ParseParamResult result = SetValue(options, param, "false");
i++; // Skip --param
return result;
}
// Check for long form with space: --param value
@@ -202,6 +199,7 @@ static const CommandLineParam g_autoParams[] = {
{POFF(compare), CmdParamType::Bool, "compare", 'c', "Enable comparison mode", CmdLineMode::Headless},
{POFF(bench), CmdParamType::Bool, "bench", 'b', "Enable benchmark mode", CmdLineMode::Headless},
{POFF(oldAtrac), CmdParamType::Bool, "old-atrac", '\0', "Use old ATRAC decoder"},
{POFF(memStickInserted), CmdParamType::BoolInverse, "no-memstick", '\0', "Run with the memory stick removed"},
{POFF(log), CmdParamType::String, "log", '\0', "Output log to FILE", CmdLineMode::Application},
{POFF(enableLogging), CmdParamType::Bool, "log", '\0', "Full log output, not just emulated printfs", CmdLineMode::Headless},
{POFF(screenshotFilename), CmdParamType::String, "screenshot", '\0', "Compare rendered output against a reference screenshot FILE", CmdLineMode::Headless},
@@ -554,6 +552,10 @@ void CommandLineOptions::ApplyToConfig() const {
g_Config.bAutoRun = false;
g_Config.bSaveSettings = false;
}
if (memStickInserted.has_value()) {
g_Config.bMemStickInserted = memStickInserted.value();
g_Config.DoNotSaveSetting(&g_Config.bMemStickInserted);
}
if (escapeExit.has_value()) {
g_Config.bPauseExitsEmulator = escapeExit.value();
}
+2
View File
@@ -43,6 +43,8 @@ struct CommandLineOptions {
std::optional<std::string> startScreen;
std::optional<bool> escapeExit;
// Run as if no memory stick were inserted.
std::optional<bool> memStickInserted;
std::optional<bool> pauseMenuExit;
// Enables the WebSocket debugger on startup, on this port (0 = pick automatically).
+42 -10
View File
@@ -1114,10 +1114,48 @@ void PSPSaveDialog::ExecuteIOAction() {
ioThreadStatus = SAVEIO_READY;
}
// What a mode that shows no dialog returns without a memory stick, or 0 for one not tested on
// hardware. They all start fine and fail when they run (pspautotests utility/savedata/nomemstick).
static u32 NoMemstickResult(SceUtilitySavedataType mode) {
switch (mode) {
case SCE_UTILITY_SAVEDATA_TYPE_AUTOLOAD:
return SCE_UTILITY_SAVEDATA_ERROR_LOAD_NO_MS;
case SCE_UTILITY_SAVEDATA_TYPE_AUTOSAVE:
return SCE_UTILITY_SAVEDATA_ERROR_SAVE_NO_MS;
case SCE_UTILITY_SAVEDATA_TYPE_AUTODELETE:
return SCE_UTILITY_SAVEDATA_ERROR_DELETE_NO_MS;
case SCE_UTILITY_SAVEDATA_TYPE_SIZES:
return SCE_UTILITY_SAVEDATA_ERROR_SIZES_NO_MS;
case SCE_UTILITY_SAVEDATA_TYPE_LIST:
case SCE_UTILITY_SAVEDATA_TYPE_FILES:
case SCE_UTILITY_SAVEDATA_TYPE_MAKEDATASECURE:
case SCE_UTILITY_SAVEDATA_TYPE_MAKEDATA:
case SCE_UTILITY_SAVEDATA_TYPE_READDATASECURE:
case SCE_UTILITY_SAVEDATA_TYPE_READDATA:
case SCE_UTILITY_SAVEDATA_TYPE_WRITEDATASECURE:
case SCE_UTILITY_SAVEDATA_TYPE_WRITEDATA:
case SCE_UTILITY_SAVEDATA_TYPE_ERASESECURE:
case SCE_UTILITY_SAVEDATA_TYPE_ERASE:
case SCE_UTILITY_SAVEDATA_TYPE_DELETEDATA:
case SCE_UTILITY_SAVEDATA_TYPE_GETSIZE:
return SCE_UTILITY_SAVEDATA_ERROR_RW_NO_MEMSTICK;
default:
return 0;
}
}
void PSPSaveDialog::ExecuteNotVisibleIOAction() {
auto &result = ioRequest_.common.result;
SceUtilitySavedataType utilityMode = (SceUtilitySavedataType)(u32)ioRequest_.mode;
if (MemoryStick_State() != PSP_MEMORYSTICK_STATE_INSERTED) {
const u32 noMemstickResult = NoMemstickResult(utilityMode);
if (noMemstickResult != 0) {
result = noMemstickResult;
return;
}
}
switch (utilityMode) {
case SCE_UTILITY_SAVEDATA_TYPE_LOAD: // Only load and exit
case SCE_UTILITY_SAVEDATA_TYPE_AUTOLOAD:
@@ -1138,16 +1176,10 @@ void PSPSaveDialog::ExecuteNotVisibleIOAction() {
result = ioParam_.GetFilesList(&ioRequest_, requestAddr, ioListSaveDirName_);
break;
case SCE_UTILITY_SAVEDATA_TYPE_GETSIZE:
{
bool sizeResult = ioParam_.GetSize(&ioRequest_);
// TODO: According to JPCSP, should test/verify this part but seems edge casey.
if (MemoryStick_State() != PSP_MEMORYSTICK_STATE_INSERTED) {
result = SCE_UTILITY_SAVEDATA_ERROR_RW_NO_MEMSTICK;
} else if (sizeResult) {
result = 0;
} else {
result = SCE_UTILITY_SAVEDATA_ERROR_RW_NO_DATA;
}
if (ioParam_.GetSize(&ioRequest_)) {
result = 0;
} else {
result = SCE_UTILITY_SAVEDATA_ERROR_RW_NO_DATA;
}
break;
case SCE_UTILITY_SAVEDATA_TYPE_DELETEDATA:
+20
View File
@@ -946,10 +946,22 @@ static void __IoSchedSync(FileNode *f, int fd, int usec) {
f->hasAsyncResult = false;
}
// With the memory stick out, there's no device behind ms0: (pspautotests utility/savedata/nomemstick).
static bool IsRemovedMemstickPath(const char *path) {
if (MemoryStick_State() == PSP_MEMORYSTICK_STATE_INSERTED) {
return false;
}
return startsWithNoCase(path, "ms0:") || startsWithNoCase(path, "fatms0:");
}
static u32 sceIoGetstat(const char *filename, u32 addr) {
// TODO: Improve timing (although this seems normally slow..)
int usec = 1000;
if (IsRemovedMemstickPath(filename)) {
return hleDelayResult(hleLogWarning(Log::sceIo, SCE_KERNEL_ERROR_NODEV, "memory stick removed"), "io getstat", usec);
}
// A real PSP refuses to stat the root of a volume - io/stat records sceIoGetstat("ms0:/")
// coming back as an invalid argument rather than describing the directory.
const char *colon = strchr(filename, ':');
@@ -1573,6 +1585,10 @@ static FileNode *__IoOpen(int &error, const char *filename, int flags, int mode)
error = SCE_KERNEL_ERROR_ERRNO_FILE_NOT_FOUND;
return nullptr;
}
if (IsRemovedMemstickPath(filename)) {
error = SCE_KERNEL_ERROR_NODEV;
return nullptr;
}
int access = FILEACCESS_NONE;
if (flags & PSP_O_RDONLY)
@@ -2549,6 +2565,10 @@ static u32 sceIoDopen(const char *path) {
return hleLogError(Log::sceIo, SCE_KERNEL_ERROR_ERRNO_FILE_NOT_FOUND, "nullptr file not found");
}
if (IsRemovedMemstickPath(path)) {
return hleLogWarning(Log::sceIo, SCE_KERNEL_ERROR_NODEV, "memory stick removed");
}
double startTime = time_now_d();
bool listingExists = false;
+8 -4
View File
@@ -721,10 +721,14 @@ bool GameManager::ExtractZipContents(struct zip *z, const Path &dest, const ZipF
Path outFilename = dest / zippedName.substr(info.stripChars);
bool isDir = zippedName.empty() || zippedName.back() == '/';
if (!isDir && zippedName.find('/') != std::string::npos) {
outFilename = dest / zippedName.substr(info.stripChars, zippedName.rfind('/') - info.stripChars);
} else if (!isDir) {
outFilename = dest;
if (!isDir) {
// stripChars can reach past the last slash (save states strip their whole directory).
size_t lastSlash = zippedName.rfind('/');
if (lastSlash != std::string::npos && lastSlash > (size_t)info.stripChars) {
outFilename = dest / zippedName.substr(info.stripChars, lastSlash - info.stripChars);
} else {
outFilename = dest;
}
}
Path outPath(outFilename);
+18 -2
View File
@@ -108,6 +108,9 @@ static int g_audioFramesPerBuffer = 0;
static bool g_textFocusChanged;
static bool g_textFocus;
// Set when SDL_StartTextInput fails, so we generate KeyInputFlags::CHAR from key events ourselves.
// SDL 3.4.0 to 3.4.4 refuse text input on Wayland compositors without zwp_text_input_v3 (like WSLg's Weston).
static bool g_textInputFallback;
double g_audioStartTime = 0.0;
// Window state to be transferred to the main SDL thread.
@@ -1016,7 +1019,6 @@ bool System_MakeRequest(SystemRequestType type, int requestId, const std::string
g_textFocus = true;
g_textFocusChanged = true;
break;
case UIEventNotification::POPUP_CLOSED:
case UIEventNotification::TEXT_LOSTFOCUS:
g_textFocus = false;
g_textFocusChanged = true;
@@ -1523,6 +1525,17 @@ static void ProcessSDLEvent(SDL_Window *window, const SDL_Event &event, InputSta
#endif
case SDL_EVENT_KEY_DOWN:
{
if (g_textInputFallback && g_textFocus && !(event.key.mod & (SDL_KMOD_CTRL | SDL_KMOD_ALT | SDL_KMOD_GUI))) {
// Layout-aware, with shift applied. Repeats are wanted here, like real text input.
SDL_Keycode c = SDL_GetKeyFromScancode(event.key.scancode, event.key.mod, false);
if (c >= 0x20 && c != 0x7F && !(c & SDLK_SCANCODE_MASK)) {
KeyInput key;
key.flags = KeyInputFlags::CHAR;
key.unicodeChar = c;
key.deviceId = DEVICE_ID_KEYBOARD;
NativeKey(key);
}
}
if (event.key.repeat > 0) {
break;
}
@@ -1848,7 +1861,10 @@ void UpdateTextFocus(SDL_Window *window) {
if (g_textFocusChanged) {
DEBUG_LOG(Log::System, "Updating text focus: %d", g_textFocus);
if (g_textFocus) {
SDL_StartTextInput(window);
if (!SDL_StartTextInput(window) && !g_textInputFallback) {
WARN_LOG(Log::System, "SDL_StartTextInput failed (%s), generating text from key events instead. Update SDL, or set SDL_VIDEO_DRIVER=x11, for full text input.", SDL_GetError());
g_textInputFallback = true;
}
} else {
SDL_StopTextInput(window);
}
+25 -26
View File
@@ -212,7 +212,7 @@ static std::vector<QueuedEvent> g_inputEventQueue;
static std::unique_ptr<ImDebugger> imDebugger_;
static ImCommand imCmd_{}; // needed to buffer commands in case imgui wasn't created yet.
static bool imguiInited_ = false;
static bool lastImguiEnabled_ = false;
static bool imguiWantTextInput_ = false;
static ImGuiContext *ctx_ = nullptr;
class GlobalListener : public ControlListener {
@@ -297,20 +297,7 @@ static void RunAchievementsOverrideUpdate(std::function<void()> func) {
}
void runImDebugger(Draw::DrawContext *draw) {
bool lastImguiEnabled_ = false; // temp
if (lastImguiEnabled_ && g_Config.bShowImDebugger) {
#if !defined(MOBILE_DEVICE)
// On mobile devices (specifically iOS) we don't want to pop the keyboard
// on activating imgui. Instead, we should do it when a text edit field in imgui gets focus,
// although we'll still have ugly overlap problems.
System_NotifyUIEvent(UIEventNotification::TEXT_GOTFOCUS);
#endif
VERBOSE_LOG(Log::System, "activating keyboard");
} else if (lastImguiEnabled_ && !g_Config.bShowImDebugger) {
System_NotifyUIEvent(UIEventNotification::TEXT_LOSTFOCUS);
VERBOSE_LOG(Log::System, "deactivating keyboard");
}
lastImguiEnabled_ = g_Config.bShowImDebugger;
bool wantTextInput = false;
if (g_Config.bShowImDebugger) {
if (!imguiInited_) {
// TODO: Do this only on demand.
@@ -383,10 +370,20 @@ void runImDebugger(Draw::DrawContext *draw) {
// Convert to drawlists.
ImGui::Render();
wantTextInput = io.WantTextInput;
} else {
uiContext->SetOverrideScreenFrame(nullptr);
SetOverrideScreenFrame(nullptr);
}
// Enable text input (needed for KeyInputFlags::CHAR events) only while an imgui text field is active.
// On mobile devices (specifically iOS) this would pop the keyboard, with ugly overlap problems.
#if !defined(MOBILE_DEVICE)
if (wantTextInput != imguiWantTextInput_) {
System_SetTextInputWanted(TextInputUser::ImDebugger, wantTextInput);
imguiWantTextInput_ = wantTextInput;
}
#endif
}
void renderImDebugger(Draw::DrawContext *draw) {
@@ -1569,9 +1566,11 @@ bool NativeKey(const KeyInput &key) {
// Filtering, detailed rules needed for good imgui behavior without having to ask the screen about what to do.
InputMode inputMode = g_screenManager->PassInputToMapper();
bool imguiHasKeyboard = false;
if (g_Config.bShowImDebugger && imguiInited_) {
if (ImGui::GetIO().WantCaptureKeyboard) {
inputMode &= ~InputMode::Keyboard;
imguiHasKeyboard = true;
}
if (ImGui::GetIO().WantCaptureMouse) {
inputMode &= ~InputMode::Mouse;
@@ -1579,21 +1578,21 @@ bool NativeKey(const KeyInput &key) {
}
bool passKeyThrough = false;
if (inputMode != InputMode::None) {
if ((inputMode & InputMode::ImDebuggerToggle) && (key.flags & (KeyInputFlags::UP | KeyInputFlags::DOWN))) {
InputMapping mapping(key.deviceId, key.keyCode);
std::vector<int> pspButtons;
bool mappingFound = KeyMap::InputMappingToPspButton(mapping, &pspButtons);
if (mappingFound) {
for (auto b : pspButtons) {
if (b == VIRTKEY_TOGGLE_DEBUGGER || b == VIRTKEY_PAUSE) {
// TRUE
passKeyThrough = true;
}
// The debugger toggle also goes through while imgui has the keyboard, so it can always be closed again.
if (((inputMode & InputMode::ImDebuggerToggle) || imguiHasKeyboard) && (key.flags & (KeyInputFlags::UP | KeyInputFlags::DOWN))) {
InputMapping mapping(key.deviceId, key.keyCode);
std::vector<int> pspButtons;
bool mappingFound = KeyMap::InputMappingToPspButton(mapping, &pspButtons);
if (mappingFound) {
for (auto b : pspButtons) {
if (b == VIRTKEY_TOGGLE_DEBUGGER || (b == VIRTKEY_PAUSE && (inputMode & InputMode::ImDebuggerToggle))) {
passKeyThrough = true;
}
}
}
}
if (inputMode != InputMode::None) {
if (key.deviceId == DEVICE_ID_MOUSE) {
if (inputMode & InputMode::Mouse) {
passKeyThrough = true;
+17 -5
View File
@@ -165,6 +165,13 @@ namespace MainWindow {
static bool hasFocus = true;
static bool g_keepScreenBright = false;
// The simulated double-click that toggles fullscreen. Only two clicks on the running game count, so
// focus loss, a click outside the client area, or a UI state change in between resets it.
static double lastMouseDownTime = 0.0;
static float lastMouseDownX = -1.0f;
static float lastMouseDownY = -1.0f;
static GlobalUIState lastMouseDownUIState = UISTATE_MENU;
static bool disasmMapLoadPending = false;
static bool memoryMapLoadPending = false;
static bool g_wasMinimized = false;
@@ -825,6 +832,7 @@ namespace MainWindow {
trapMouse = true;
}
if (wParam == WA_INACTIVE) {
lastMouseDownTime = 0.0;
System_PostUIMessage(UIMessage::LOST_FOCUS);
WindowsRawInput::LoseFocus();
g_InputManager.LoseFocus();
@@ -1208,27 +1216,31 @@ namespace MainWindow {
SetCapture(hWnd);
// Simulate doubleclick, doesn't work with RawInput enabled
static double lastMouseDownTime;
static float lastMouseDownX = -1.0f;
static float lastMouseDownY = -1.0f;
const double now = time_now_d();
if ((now - lastMouseDownTime) < 0.001 * GetDoubleClickTime()) {
const GlobalUIState uiState = GetUIState();
if ((now - lastMouseDownTime) < 0.001 * GetDoubleClickTime() && uiState == lastMouseDownUIState) {
const float dx = lastMouseDownX - x;
const float dy = lastMouseDownY - y;
const float distSq = dx * dx + dy * dy;
if (distSq < 3.0f*3.0f && !g_Config.bShowTouchControls && !g_Config.bShowImDebugger && !g_Config.bMouseControl && GetUIState() == UISTATE_INGAME && g_Config.bFullscreenOnDoubleclick) {
if (distSq < 3.0f*3.0f && !g_Config.bShowTouchControls && !g_Config.bShowImDebugger && !g_Config.bMouseControl && uiState == UISTATE_INGAME && g_Config.bFullscreenOnDoubleclick) {
g_Config.bFullScreen = !g_Config.bFullScreen;
SendApplyFullscreenState();
}
lastMouseDownTime = 0.0;
} else {
lastMouseDownTime = now;
lastMouseDownUIState = uiState;
}
lastMouseDownX = x;
lastMouseDownY = y;
}
break;
case WM_NCLBUTTONDOWN:
// The title bar or the window's buttons, so not part of a double-click on the game.
lastMouseDownTime = 0.0;
return DefWindowProc(hWnd, message, wParam, lParam);
case WM_MOUSEMOVE:
if (!touchHandler.hasTouch() ||
(GetMessageExtraInfo() & MOUSEEVENTF_MASK_PLUS_PENTOUCH) != MOUSEEVENTF_FROMTOUCH_NOPEN)
+13 -1
View File
@@ -512,6 +512,7 @@ tests_good = [
"utility/savedata/filelist",
"utility/savedata/getsize",
"utility/savedata/makedata",
"utility/savedata/nomemstick",
"utility/systemparam/systemparam",
"umd/api/api",
"umd/callbacks/umd",
@@ -656,6 +657,13 @@ tests_next = [
]
# Tests that need extra headless flags, so they run in a headless process of their own. The flags
# have to match how the .expected was recorded.
test_flags = {
"utility/savedata/nomemstick": ["--no-memstick"],
}
# These are the tests we ignore (not important, or impossible to run)
tests_ignored = [
"kirk/kirk",
@@ -773,7 +781,11 @@ def main():
if skipped:
print("Skipping %d known failures on %s: %s" % (len(skipped), skip_arch, ", ".join(skipped)))
returncode = run_tests(tests, args)
plain = [t for t in tests if t not in test_flags]
returncode = run_tests(plain, args)
for test in tests:
if test in test_flags:
returncode = run_tests([test], args + test_flags[test]) or returncode
if teamcity:
return 0
return returncode
+45
View File
@@ -80,6 +80,49 @@ static bool TestZipSlipExtraction() {
return true;
}
// Save state zips are detected with stripChars past the last slash, since the whole
// directory is stripped. The files must land directly in the destination.
static bool TestSaveStateZipExtraction() {
// Absolute, since CreateFullPath does nothing with relative paths.
Path tempRoot = File::GetCurDirectory() / "unittest_zip_savestate_test";
File::DeleteDirRecursively(tempRoot);
EXPECT_TRUE(File::CreateDir(tempRoot));
Path destDir = tempRoot / "dest";
EXPECT_TRUE(File::CreateDir(destDir));
Path zipPath = tempRoot / "states.zip";
int errorp = 0;
zip_t *z = zip_open(zipPath.c_str(), ZIP_CREATE | ZIP_TRUNCATE, &errorp);
EXPECT_TRUE(z != nullptr);
static const char *const contents = "data";
EXPECT_TRUE(zip_dir_add(z, "SAVE/", ZIP_FL_ENC_UTF_8) >= 0);
EXPECT_TRUE(zip_dir_add(z, "SAVE/PPSSPP_STATE/", ZIP_FL_ENC_UTF_8) >= 0);
for (const char *name : { "SAVE/PPSSPP_STATE/UCES01401_1.00_1.jpg", "SAVE/PPSSPP_STATE/UCES01401_1.00_1.ppst" }) {
zip_source_t *source = zip_source_buffer(z, contents, strlen(contents), 0);
EXPECT_TRUE(source != nullptr);
EXPECT_TRUE(zip_file_add(z, name, source, ZIP_FL_ENC_UTF_8) >= 0);
}
EXPECT_TRUE(zip_close(z) == 0);
z = zip_open(zipPath.c_str(), 0, &errorp);
EXPECT_TRUE(z != nullptr);
ZipFileInfo info;
DetectZipFileContents(z, &info);
EXPECT_EQ_INT((int)info.contents, (int)ZipFileContents::SAVE_STATES);
GameManager manager;
EXPECT_TRUE(manager.ExtractZipContents(z, destDir, info, true));
zip_close(z);
EXPECT_TRUE(File::Exists(destDir / "UCES01401_1.00_1.jpg") && !File::IsDirectory(destDir / "UCES01401_1.00_1.jpg"));
EXPECT_TRUE(File::Exists(destDir / "UCES01401_1.00_1.ppst") && !File::IsDirectory(destDir / "UCES01401_1.00_1.ppst"));
File::DeleteDirRecursively(tempRoot);
return true;
}
static bool TestIsSafePathComponent() {
EXPECT_TRUE(IsSafePathComponent("ULUS10041"));
EXPECT_TRUE(IsSafePathComponent("NPJH-50465"));
@@ -106,5 +149,7 @@ bool TestZipSlip() {
return false;
if (!TestZipSlipExtraction())
return false;
if (!TestSaveStateZipExtraction())
return false;
return true;
}
+15
View File
@@ -3029,6 +3029,21 @@ bool TestCmdLine() {
EXPECT_EQ_INT(options.timeoutWall.value_or(0), 4);
EXPECT_EQ_INT(options.timeoutEmulated.value_or(0), 5);
}
// Inverse booleans: --windowed turns fullscreen off, and --no-memstick removes the stick.
{
const char *argv[] = {
"ppsspp",
"--windowed",
"--no-memstick",
};
int argc = ARRAY_SIZE(argv);
CommandLineOptions options;
options.Parse(argc, argv, CmdLineMode::Application);
EXPECT_TRUE(options.fullscreen.has_value());
EXPECT_FALSE(options.fullscreen.value_or(true));
EXPECT_TRUE(options.memStickInserted.has_value());
EXPECT_FALSE(options.memStickInserted.value_or(true));
}
// Test GL version override
{
const char *argv[] = {