From 1832c9aced0ce0ced405502c0f6be07616b7f5bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Fri, 6 Nov 2026 08:10:07 +0100 Subject: [PATCH] MemStickScreen: Refuse folders inside a memory stick's PSP folder Picking the new memory stick folder is checked by CheckNewMemstickFolder(), which compares locations across content URIs and Android storage paths, case-insensitively and on whole folder names: - Inside the current memory stick's PSP folder: refused. This used to be a warning only, compared by string prefix (so "Games2" was "inside" "Games"), and missed URIs from a different volume or a native path. - Inside some other memory stick's PSP folder (like after a reinstall): refused. Found on disk among the parents of native paths, and from the names for picked trees, whose parents we can't look at. - Same PSP folder as the current one (a PSP folder and its parent), or the current one inside the new one: allowed, without moving data. GetPSPFolderOf() is the PSP folder rule GetSysDirectory() uses, now shared with the move, which compared "PSP" case-sensitively and so could move data into psp/PSP. FolderSeemsToBeUsed() looks in that folder too, and "Move Data" starts unchecked when the new folder already has data, since merging overwrites it. Switching to a folder without a config no longer resets settings to the defaults: the current ones are written there instead. Typing a manual path without the leading slash works again (the fixup was unreachable). Co-Authored-By: Claude Opus 5.5 --- Core/Config.h | 1 + Core/Util/MemStick.cpp | 170 ++++++++++++++++++++++++++++++++++++++--- Core/Util/MemStick.h | 15 ++++ Core/Util/PathUtil.cpp | 14 ++-- Core/Util/PathUtil.h | 3 + UI/MemStickScreen.cpp | 74 +++++++++++------- UI/MemStickScreen.h | 9 ++- unittest/UnitTest.cpp | 54 +++++++++++++ 8 files changed, 294 insertions(+), 46 deletions(-) diff --git a/Core/Config.h b/Core/Config.h index ea5f82caa9..3f33db4837 100644 --- a/Core/Config.h +++ b/Core/Config.h @@ -798,6 +798,7 @@ public: void SetSearchPath(const Path &path); void UpdateIniLocation(const char *iniFileName = nullptr, const char *controllerIniFilename = nullptr); + const Path &IniFilename() const { return iniFilename_; } bool SupportsUpgradeCheck() const; void CheckForUpdate(); diff --git a/Core/Util/MemStick.cpp b/Core/Util/MemStick.cpp index 47b61f4a02..5e0c9d3e83 100644 --- a/Core/Util/MemStick.cpp +++ b/Core/Util/MemStick.cpp @@ -1,6 +1,8 @@ #include +#include #include +#include "Common/File/AndroidContentURI.h" #include "Common/File/Path.h" #include "Common/File/FileUtil.h" #include "Common/File/DirListing.h" @@ -12,18 +14,162 @@ #include "Common/Data/Text/Parsers.h" #include "Core/Util/MemStick.h" +#include "Core/Util/PathUtil.h" #include "Core/Config.h" #include "Core/System.h" #include "Core/Reporting.h" bool FolderSeemsToBeUsed(const Path &newMemstickFolder) { - // Inspect the potential new folder, quickly. - if (File::Exists(newMemstickFolder / "PSP/SAVEDATA") || File::Exists(newMemstickFolder / "SAVEDATA")) { - // Does seem likely. We could add more criteria like checking for actual savegames or something. - return true; + // Inspect the potential new folder, quickly. We could add more criteria like checking for actual savegames. + const Path pspFolder = GetPSPFolderOf(newMemstickFolder); + return File::Exists(pspFolder / "SAVEDATA") || File::Exists(pspFolder / "SYSTEM"); +} + +// Checked by string rather than Type(), which only says CONTENT_URI on Android, so the checks below can +// be unit tested anywhere. +static bool IsContentURI(const Path &path) { + return startsWith(path.ToString(), "content://"); +} + +// A form of a folder's location that compares between content URIs and native paths: "volume:path" for +// Android shared storage, the plain path otherwise. Empty if the location can't be told. +static std::string LocationKey(const Path &path) { + std::string key; + if (IsContentURI(path)) { + AndroidContentURI uri(path.ToString()); + // Other providers (like Downloads) use opaque document IDs. + if (uri.Provider() != "com.android.externalstorage.documents") { + return std::string(); + } + key = uri.FilePath().empty() ? uri.RootPath() : uri.FilePath(); + } else if (path.Type() == PathType::NATIVE) { + key = path.ToString(); + // Map Android storage paths to the volume IDs that content URIs use. + static const std::string_view primaryPrefixes[] = { "/storage/emulated/0", "/sdcard", "/storage/self/primary", "/mnt/sdcard" }; + bool mapped = false; + for (std::string_view prefix : primaryPrefixes) { + if (key == prefix || startsWith(key, std::string(prefix) + "/")) { + key = "primary:" + key.substr(std::min(key.size(), prefix.size() + 1)); + mapped = true; + break; + } + } + if (!mapped && startsWith(key, "/storage/")) { + // SD cards and USB drives: /storage/1234-ABCD/... + const size_t volStart = strlen("/storage/"); + const size_t slash = key.find('/', volStart); + const std::string volume = key.substr(volStart, slash == std::string::npos ? std::string::npos : slash - volStart); + if (!volume.empty() && volume != "emulated" && volume != "self") { + key = volume + ":" + (slash == std::string::npos ? std::string() : key.substr(slash + 1)); + } + } } else { + return std::string(); + } + while (key.size() > 1 && key.back() == '/') { + key.pop_back(); + } + return key; +} + +// True if child is strictly inside parent. Case-insensitive, like Android's shared storage. +static bool KeyIsInside(std::string_view child, std::string_view parent) { + if (parent.empty() || child.size() <= parent.size() || !startsWithNoCase(child, parent)) { return false; } + const char last = parent.back(); + return last == '/' || last == ':' || child[parent.size()] == '/'; +} + +// LocationKey() of GetPSPFolderOf(folder), by the same rule. +static std::string PSPFolderKey(const Path &folder) { + const std::string key = LocationKey(folder); + if (key.empty()) { + return key; + } + // Path::GetFilename() is GetLastPart() for content URIs. + const std::string name = IsContentURI(folder) ? AndroidContentURI(folder.ToString()).GetLastPart() : folder.GetFilename(); + if (equalsNoCase(name, "PSP")) { + return key; + } + return key + (key.back() == ':' || key.back() == '/' ? "PSP" : "/PSP"); +} + +static bool IsPSPSubfolderName(std::string_view name) { + static const std::string_view names[] = { + "SAVEDATA", "GAME", "SYSTEM", "PPSSPP_STATE", "TEXTURES", "PLUGINS", "CHEATS", "SCREENSHOT", + "MUSIC", "PHOTO", "VIDEO", "COMMON", + }; + for (std::string_view n : names) { + if (equalsNoCase(name, n)) { + return true; + } + } + return false; +} + +// Works from the name alone, for folders whose parents we can't look at (a picked content URI tree): +// a PSP folder with one of its usual subfolders below it. +static bool KeyLooksInsidePSPFolder(std::string_view key) { + const size_t colon = key.find(':'); + std::vector parts; + SplitString(key.substr(colon == std::string_view::npos ? 0 : colon + 1), '/', parts); + parts.erase(std::remove(parts.begin(), parts.end(), std::string_view()), parts.end()); + for (size_t i = 0; i + 1 < parts.size(); i++) { + if (equalsNoCase(parts[i], "PSP") && IsPSPSubfolderName(parts[i + 1])) { + return true; + } + } + return false; +} + +static bool IsInsideOtherMemstick(const Path &folder, const std::string &key) { + if (KeyLooksInsidePSPFolder(key)) { + return true; + } + if (folder.Type() != PathType::NATIVE || IsContentURI(folder)) { + return false; + } + // Native paths: look for a memory stick in use among the parents. + Path dir = folder; + for (int depth = 0; depth < 64 && dir.CanNavigateUp(); depth++) { + const Path up = dir.NavigateUp(); + if (up == dir || up.empty()) { + break; + } + dir = up; + if (KeyIsInside(key, PSPFolderKey(dir)) && FolderSeemsToBeUsed(dir)) { + return true; + } + } + return false; +} + +MemstickFolderCheck CheckNewMemstickFolder(const Path &newFolder, const Path ¤tFolder) { + const std::string newKey = LocationKey(newFolder); + if (newKey.empty()) { + return MemstickFolderCheck::OK; + } + const std::string newPSPKey = PSPFolderKey(newFolder); + std::string curPSPKey; + if (!currentFolder.empty()) { + curPSPKey = PSPFolderKey(currentFolder); + if (KeyIsInside(newPSPKey, curPSPKey)) { + return MemstickFolderCheck::INSIDE_CURRENT; + } + } + if (IsInsideOtherMemstick(newFolder, newKey)) { + return MemstickFolderCheck::INSIDE_OTHER; + } + if (!curPSPKey.empty()) { + if (equalsNoCase(newPSPKey, curPSPKey)) { + return MemstickFolderCheck::SAME_PSP_FOLDER; + } + if (KeyIsInside(curPSPKey, newPSPKey)) { + return MemstickFolderCheck::CONTAINS_CURRENT; + } + } + return MemstickFolderCheck::OK; } bool SwitchMemstickFolderTo(Path newMemstickFolder) { @@ -145,13 +291,17 @@ std::string MoveProgressReporter::Format() { MoveResult *MoveDirectoryContentsSafe(Path moveSrc, Path moveDest, MoveProgressReporter &progressReporter) { auto ms = GetI18NCategory(I18NCat::MEMSTICK); - if (moveSrc.GetFilename() != "PSP") { - moveSrc = moveSrc / "PSP"; - } - if (moveDest.GetFilename() != "PSP") { - moveDest = moveDest / "PSP"; - File::CreateDir(moveDest); + moveSrc = GetPSPFolderOf(moveSrc); + moveDest = GetPSPFolderOf(moveDest); + + // The UI doesn't offer these (see CheckNewMemstickFolder), but moving a folder into itself would be a mess. + const std::string srcKey = LocationKey(moveSrc); + const std::string destKey = LocationKey(moveDest); + if (!srcKey.empty() && !destKey.empty() && (equalsNoCase(srcKey, destKey) || KeyIsInside(srcKey, destKey) || KeyIsInside(destKey, srcKey))) { + ERROR_LOG(Log::System, "Refusing to move PSP data from '%s' to '%s'", moveSrc.c_str(), moveDest.c_str()); + return new MoveResult{ false, "Source and destination overlap" }; } + File::CreateDir(moveDest); INFO_LOG(Log::System, "About to move PSP data from '%s' to '%s'", moveSrc.c_str(), moveDest.c_str()); diff --git a/Core/Util/MemStick.h b/Core/Util/MemStick.h index 65c7384726..119f78f387 100644 --- a/Core/Util/MemStick.h +++ b/Core/Util/MemStick.h @@ -32,6 +32,21 @@ struct MoveResult { size_t skippedFiles; }; +enum class MemstickFolderCheck { + OK, + // A different folder with the same PSP folder as the current one (a PSP folder and its parent). Nothing to move. + SAME_PSP_FOLDER, + // The current memory stick is inside the new one's PSP folder, so its data can't be moved there. + CONTAINS_CURRENT, + // Not allowed: inside the current memory stick's PSP folder. + INSIDE_CURRENT, + // Not allowed: inside the PSP folder of some other memory stick. + INSIDE_OTHER, +}; + +// Checks a folder the user picked as the new memory stick. currentFolder may be empty. +MemstickFolderCheck CheckNewMemstickFolder(const Path &newFolder, const Path ¤tFolder); + bool FolderSeemsToBeUsed(const Path &newMemstickFolder); bool SwitchMemstickFolderTo(Path newMemstickFolder); MoveResult *MoveDirectoryContentsSafe(Path moveSrc, Path moveDest, MoveProgressReporter &progressReporter); diff --git a/Core/Util/PathUtil.cpp b/Core/Util/PathUtil.cpp index 5a84d761bc..caa91ba8ba 100644 --- a/Core/Util/PathUtil.cpp +++ b/Core/Util/PathUtil.cpp @@ -94,15 +94,17 @@ static Path PreferUpperCaseDir(const Path &parent, const char *upperCase, const return upper; } -Path GetSysDirectory(PSPDirectories directoryType) { - const Path &memStickDirectory = g_Config.memStickDirectory; - Path pspDirectory; +Path GetPSPFolderOf(const Path &memStickDirectory) { if (!strcasecmp(memStickDirectory.GetFilename().c_str(), "PSP")) { // Let's strip this off, to easily allow choosing a root directory named "PSP" on Android. - pspDirectory = memStickDirectory; - } else { - pspDirectory = memStickDirectory / "PSP"; + return memStickDirectory; } + return memStickDirectory / "PSP"; +} + +Path GetSysDirectory(PSPDirectories directoryType) { + const Path &memStickDirectory = g_Config.memStickDirectory; + const Path pspDirectory = GetPSPFolderOf(memStickDirectory); switch (directoryType) { case DIRECTORY_PSP: diff --git a/Core/Util/PathUtil.h b/Core/Util/PathUtil.h index 0c969b8d4e..19f21ce03c 100644 --- a/Core/Util/PathUtil.h +++ b/Core/Util/PathUtil.h @@ -48,6 +48,9 @@ bool HasPathTraversal(std::string_view path); bool IsSafePathComponent(std::string_view name); Path FindConfigFile(const Path &searchPath, std::string_view baseFilename, bool *exists); +// The PSP folder of a memory stick. A folder named PSP is used as it is (the legacy layout, where its +// parent acts as the root), anything else gets a PSP folder inside. +Path GetPSPFolderOf(const Path &memStickDirectory); Path GetSysDirectory(PSPDirectories directoryType); bool CreateSysDirectories(); Path GetGameConfigFilePath(const Path &searchPath, std::string_view gameId, bool *exists); diff --git a/UI/MemStickScreen.cpp b/UI/MemStickScreen.cpp index e23667e756..696c4fb293 100644 --- a/UI/MemStickScreen.cpp +++ b/UI/MemStickScreen.cpp @@ -311,9 +311,13 @@ void MemStickScreen::SetFolderManually(UI::EventParams ¶ms) { auto di = GetI18NCategory(I18NCat::DIALOG); std::string newPath(value); + // Apparently some users got used to leaving out the leading /. + if (!newPath.empty() && newPath[0] != '/') { + newPath = "/" + newPath; + } size_t pos = newPath.find_last_not_of('/'); - // Gotta have at least something but a /, and also needs to start with a /. - if (newPath.empty() || pos == std::string::npos || newPath[0] != '/') { + // Gotta have at least something but a /. + if (pos == std::string::npos) { g_OSD.Show(OSDType::MESSAGE_WARNING, sy->T("ChangingMemstickPathInvalid", "That path couldn't be used to save Memory Stick files."), 5.0f); return; } @@ -329,15 +333,6 @@ void MemStickScreen::SetFolderManually(UI::EventParams ¶ms) { Path pendingMemStickFolder(newPath); - if (!File::Exists(pendingMemStickFolder)) { - // Try to fix the path string, apparently some users got used to leaving out the /. - if (newPath[0] != '/') { - newPath = "/" + newPath; - } - - pendingMemStickFolder = Path(newPath); - } - if (!File::Exists(pendingMemStickFolder) && pendingMemStickFolder.Type() == PathType::NATIVE) { // Still no path? Try to automatically fix the case. std::string oldNewPath = newPath; @@ -352,7 +347,7 @@ void MemStickScreen::SetFolderManually(UI::EventParams ¶ms) { if (pendingMemStickFolder == g_Config.memStickDirectory) { // Same directory as before - all good. Nothing to do. - TriggerFinish(DialogResult::DR_OK); + done_ = true; return; } @@ -419,6 +414,9 @@ void MemStickScreen::Browse(UI::EventParams ¶ms) { System_BrowseForFolder(GetRequesterToken(), mm->T("Choose folder"), g_Config.memStickDirectory, [this](std::string_view value, int) { Path pendingMemStickFolder(value); INFO_LOG(Log::System, "Got folder: '%s' (old: %s)", pendingMemStickFolder.c_str(), g_Config.memStickDirectory.c_str()); + if (pendingMemStickFolder.empty()) { + return; + } // Browse finished. Let's pop up the confirmation dialog. if (!pendingMemStickFolder.empty() && pendingMemStickFolder == g_Config.memStickDirectory && File::IsDirectory(pendingMemStickFolder)) { auto di = GetI18NCategory(I18NCat::DIALOG); @@ -473,18 +471,12 @@ ConfirmMemstickMoveScreen::ConfirmMemstickMoveScreen(const Path &newMemstickFold : newMemstickFolder_(newMemstickFolder), initialSetup_(initialSetup), progressReporter_() { const Path &oldMemstickFolder = g_Config.memStickDirectory; existingFilesInNewFolder_ = FolderSeemsToBeUsed(newMemstickFolder); - if (!oldMemstickFolder.empty()) { - folderConflict_ = newMemstickFolder != oldMemstickFolder && newMemstickFolder.StartsWithGlobalAndNotEqual(oldMemstickFolder); - } else { - folderConflict_ = false; - } + check_ = CheckNewMemstickFolder(newMemstickFolder, oldMemstickFolder); INFO_LOG(Log::System, "Old: '%s'", oldMemstickFolder.c_str()); - INFO_LOG(Log::System, "New: '%s'", newMemstickFolder.c_str()); + INFO_LOG(Log::System, "New: '%s' (check: %d)", newMemstickFolder.c_str(), (int)check_); - // TODO: If you reinstall the app and start by selecting a subfolder of the PSP folder, we don't detect and warn about that - - // we can only warn if there's a known previous folder :( - - if (initialSetup_) { + // Moving into a folder that already has data would overwrite it, so that has to be asked for. + if (initialSetup_ || existingFilesInNewFolder_ || !CanMoveData()) { moveData_ = false; } @@ -542,8 +534,21 @@ void ConfirmMemstickMoveScreen::CreateViews() { } } - if (folderConflict_) { - leftColumn->Add(new NoticeView(NoticeLevel::WARN, ms->T("The new folder is inside the previous one"), "")); + switch (check_) { + case MemstickFolderCheck::INSIDE_CURRENT: + leftColumn->Add(new NoticeView(NoticeLevel::ERROR, ms->T("The new folder is inside the previous one"), "")); + break; + case MemstickFolderCheck::INSIDE_OTHER: + leftColumn->Add(new NoticeView(NoticeLevel::ERROR, ms->T("InsideOtherMemstick", "This folder is inside the PSP folder of another memory stick. Choose that memory stick's folder instead."), "")); + break; + case MemstickFolderCheck::SAME_PSP_FOLDER: + leftColumn->Add(new NoticeView(NoticeLevel::INFO, ms->T("SamePSPFolder", "Contains the same PSP folder as the current one. Nothing will be moved."), "")); + break; + case MemstickFolderCheck::CONTAINS_CURRENT: + leftColumn->Add(new NoticeView(NoticeLevel::WARN, ms->T("ContainsCurrentMemstick", "The current folder is inside this one, so its data can't be moved here"), "")); + break; + case MemstickFolderCheck::OK: + break; } if (!error_.empty()) { @@ -562,12 +567,14 @@ void ConfirmMemstickMoveScreen::CreateViews() { } if (!moveDataTask_) { - if (!initialSetup_) { + if (!initialSetup_ && CanMoveData()) { leftColumn->Add(new CheckBox(&moveData_, ms->T("Move Data")))->OnClick.Handle(this, &ConfirmMemstickMoveScreen::OnMoveDataClick); } auto di = GetI18NCategory(I18NCat::DIALOG); - leftColumn->Add(new Choice(di->T("OK")))->OnClick.Handle(this, &ConfirmMemstickMoveScreen::OnConfirm); + if (!IsBlocked()) { + leftColumn->Add(new Choice(di->T("OK")))->OnClick.Handle(this, &ConfirmMemstickMoveScreen::OnConfirm); + } leftColumn->Add(new Choice(di->T("Back")))->OnClick.Add([this](UI::EventParams ¶ms) { if (moveDataTask_ && !moveDataTask_->Poll()) { return; @@ -633,8 +640,11 @@ void ConfirmMemstickMoveScreen::OnConfirm(UI::EventParams ¶ms) { // Should probably be done on a background thread so we can show some UI. // So we probably need another screen for this with a progress bar.. // If the directory itself is called PSP, don't go below. + if (IsBlocked()) { + return; + } - if (moveData_) { + if (moveData_ && CanMoveData()) { progressReporter_.SetProgress(T(I18NCat::MEMSTICK, "Starting move...")); moveDataTask_ = Promise::Spawn(&g_threadManager, [&]() -> MoveResult * { @@ -666,8 +676,14 @@ void ConfirmMemstickMoveScreen::FinishFolderMove() { INFO_LOG(Log::System, "Move from '%s' to '%s' complete. Updating config.", oldMemstickFolder.c_str(), newMemstickFolder_.c_str()); - // If the chosen folder already had a config, reload it! - g_Config.Load(); + // If the chosen folder already had a config, reload it! Otherwise keep the current settings, and + // write them there. + if (File::Exists(g_Config.IniFilename())) { + g_Config.Load(); + } else { + INFO_LOG(Log::System, "No config at '%s', keeping the current settings", g_Config.IniFilename().c_str()); + g_Config.Save("MemstickPathChanged"); + } // If the current browser directory is the old memstick folder, drop it. if (g_Config.currentDirectory == oldMemstickFolder) { diff --git a/UI/MemStickScreen.h b/UI/MemStickScreen.h index 86a4264fa7..6fc3a3e7ae 100644 --- a/UI/MemStickScreen.h +++ b/UI/MemStickScreen.h @@ -117,9 +117,16 @@ private: void OnConfirm(UI::EventParams ¶ms); + bool IsBlocked() const { + return check_ == MemstickFolderCheck::INSIDE_CURRENT || check_ == MemstickFolderCheck::INSIDE_OTHER; + } + bool CanMoveData() const { + return check_ == MemstickFolderCheck::OK; + } + Path newMemstickFolder_; bool existingFilesInNewFolder_; - bool folderConflict_; + MemstickFolderCheck check_ = MemstickFolderCheck::OK; #if PPSSPP_PLATFORM(UWP) && !defined(__LIBRETRO__) bool moveData_ = false; diff --git a/unittest/UnitTest.cpp b/unittest/UnitTest.cpp index 4c19d1cfc3..3e8cb845b6 100644 --- a/unittest/UnitTest.cpp +++ b/unittest/UnitTest.cpp @@ -127,6 +127,7 @@ #include "Core/ControlMapper.h" #include "Core/HLE/sceCtrl.h" #include "Core/HLE/sceUsbMic.h" +#include "Core/Util/MemStick.h" #include "Core/Util/StorageTest.h" #include "Core/Util/PathUtil.h" #include "Core/Util/PSARUnpack.h" @@ -2954,6 +2955,58 @@ static bool TestStorage() { return true; } +// A picked folder, the way the folder browser hands it back. docPath is like "primary:Games/PSP". +static Path PickedTreeURI(std::string_view docPath) { + const std::string enc = UriEncode(docPath); + return Path("content://com.android.externalstorage.documents/tree/" + enc + "/document/" + enc); +} + +static bool TestMemstickFolderCheck() { + using C = MemstickFolderCheck; + const Path none; + + // Inside the current memory stick's PSP folder, case-insensitively. + EXPECT_EQ_INT((int)CheckNewMemstickFolder(PickedTreeURI("primary:Games/PSP/SAVEDATA"), PickedTreeURI("primary:Games")), (int)C::INSIDE_CURRENT); + EXPECT_EQ_INT((int)CheckNewMemstickFolder(PickedTreeURI("primary:games/psp/stuff"), PickedTreeURI("primary:Games")), (int)C::INSIDE_CURRENT); + // A folder named PSP is the PSP folder itself. + EXPECT_EQ_INT((int)CheckNewMemstickFolder(PickedTreeURI("primary:PSP/stuff"), PickedTreeURI("primary:PSP")), (int)C::INSIDE_CURRENT); + // Next to the PSP folder, or a name that only starts the same, is fine. + EXPECT_EQ_INT((int)CheckNewMemstickFolder(PickedTreeURI("primary:Games/ISOs"), PickedTreeURI("primary:Games")), (int)C::OK); + EXPECT_EQ_INT((int)CheckNewMemstickFolder(PickedTreeURI("primary:GamesX"), PickedTreeURI("primary:Games")), (int)C::OK); + EXPECT_EQ_INT((int)CheckNewMemstickFolder(PickedTreeURI("1234-ABCD:PSP"), PickedTreeURI("primary:PSP")), (int)C::OK); + + // Inside some other memory stick, with no current one (a reinstall). + EXPECT_EQ_INT((int)CheckNewMemstickFolder(PickedTreeURI("primary:Stuff/PSP/SAVEDATA"), none), (int)C::INSIDE_OTHER); + EXPECT_EQ_INT((int)CheckNewMemstickFolder(PickedTreeURI("1234-ABCD:PSP/GAME/x"), none), (int)C::INSIDE_OTHER); + EXPECT_EQ_INT((int)CheckNewMemstickFolder(PickedTreeURI("primary:Stuff/PSP/Mine"), none), (int)C::OK); + EXPECT_EQ_INT((int)CheckNewMemstickFolder(PickedTreeURI("primary:PSP"), none), (int)C::OK); + + // Native storage paths and content URIs are compared by location. + EXPECT_EQ_INT((int)CheckNewMemstickFolder(PickedTreeURI("primary:PSP/GAME"), Path("/storage/emulated/0")), (int)C::INSIDE_CURRENT); + EXPECT_EQ_INT((int)CheckNewMemstickFolder(Path("/storage/emulated/0"), PickedTreeURI("primary:PSP")), (int)C::SAME_PSP_FOLDER); + // Other names for internal storage, typed in on old versions. + EXPECT_EQ_INT((int)CheckNewMemstickFolder(PickedTreeURI("primary:"), Path("/mnt/sdcard")), (int)C::SAME_PSP_FOLDER); + EXPECT_EQ_INT((int)CheckNewMemstickFolder(PickedTreeURI("primary:PSP"), Path("/storage/self/primary/PSP")), (int)C::SAME_PSP_FOLDER); + EXPECT_EQ_INT((int)CheckNewMemstickFolder(PickedTreeURI("1234-ABCD:Mine/PSP/TEXTURES"), Path("/storage/1234-ABCD/Mine")), (int)C::INSIDE_CURRENT); + + // The current memory stick is inside the new one's PSP folder: switching is fine, moving isn't. + EXPECT_EQ_INT((int)CheckNewMemstickFolder(PickedTreeURI("primary:Games"), PickedTreeURI("primary:Games/PSP/Weird")), (int)C::CONTAINS_CURRENT); + + // Native paths: a memory stick in use among the parents is found on disk, whatever the folder names. + const Path root = UnitTestTempPath("unittest_memstick_check"); + File::DeleteDirRecursively(root); + EXPECT_TRUE(File::CreateFullPath(root / "ms/PSP/SAVEDATA")); + EXPECT_TRUE(File::CreateFullPath(root / "ms/PSP/Mine/deeper")); + EXPECT_EQ_INT((int)CheckNewMemstickFolder(root / "ms/PSP/Mine/deeper", none), (int)C::INSIDE_OTHER); + EXPECT_EQ_INT((int)CheckNewMemstickFolder(root / "ms/PSP", none), (int)C::OK); + EXPECT_EQ_INT((int)CheckNewMemstickFolder(root / "ms", none), (int)C::OK); + EXPECT_TRUE(FolderSeemsToBeUsed(root / "ms")); + EXPECT_TRUE(FolderSeemsToBeUsed(root / "ms/PSP")); + EXPECT_FALSE(FolderSeemsToBeUsed(root / "ms/PSP/Mine")); + File::DeleteDirRecursively(root); + return true; +} + class UnitTestWordWrapper : public WordWrapper { public: UnitTestWordWrapper(std::string_view str, float maxW, int flags) @@ -4341,6 +4394,7 @@ TestItem availableTests[] = { TEST_ITEM(OverlayHole), TEST_ITEM(AndroidContentURI), TEST_ITEM(Storage), + TEST_ITEM(MemstickFolderCheck), TEST_ITEM(ThreadManager), TEST_ITEM(WrapText), TEST_ITEM(TinySet),