Index: source/lib/file/vfs/tests/test_vfs_populate.h =================================================================== --- /dev/null +++ source/lib/file/vfs/tests/test_vfs_populate.h @@ -0,0 +1,86 @@ +/* Copyright (C) 2020 Wildfire Games. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include "lib/self_test.h" + +#include "lib/file/vfs/vfs_populate.h" +#include "lib/os_path.h" + +static OsPath CACHE_PATH_HIGH(DataDir()/"_testcache1"); +static OsPath CACHE_PATH_LOW(DataDir()/"_testcache2"); + +extern PIVFS g_VFS; + +class TestVfsPopulate : public CxxTest::TestSuite +{ + void initVfs() + { + // Initialise VFS: + + // Make sure the required directories doesn't exist when we start, + // in case the previous test aborted and left them full of junk + if(DirectoryExists(CACHE_PATH_HIGH)) + DeleteDirectory(CACHE_PATH_HIGH); + if(DirectoryExists(CACHE_PATH_LOW)) + DeleteDirectory(CACHE_PATH_LOW); + + g_VFS = CreateVfs(); + } + + void deinitVfs() + { + g_VFS.reset(); + DeleteDirectory(CACHE_PATH_LOW); + DeleteDirectory(CACHE_PATH_HIGH); + } + +public: + void setUp() + { + initVfs(); + } + + void tearDown() + { + deinitVfs(); + } + + void test_RealPriority() + { + g_VFS->Mount(L"cache", CACHE_PATH_LOW, 0, 0); + OsPath realPath; + g_VFS->GetDirectoryRealPath(L"cache", realPath); + TS_ASSERT_EQUALS(realPath, CACHE_PATH_LOW); + // Higher priority: overwritten. + g_VFS->Mount(L"cache", CACHE_PATH_HIGH, 0, 1); + g_VFS->GetDirectoryRealPath(L"cache", realPath); + TS_ASSERT_EQUALS(realPath, CACHE_PATH_HIGH); + // Same priority: nothing happens + g_VFS->Mount(L"cache", CACHE_PATH_LOW, 0, 1); + g_VFS->GetDirectoryRealPath(L"cache", realPath); + TS_ASSERT_EQUALS(realPath, CACHE_PATH_HIGH); + // Lower priority: nothing happens + g_VFS->Mount(L"cache", CACHE_PATH_LOW, 0, 0); + g_VFS->GetDirectoryRealPath(L"cache", realPath); + TS_ASSERT_EQUALS(realPath, CACHE_PATH_HIGH); + }; +}; Index: source/lib/file/vfs/vfs.h =================================================================== --- source/lib/file/vfs/vfs.h +++ source/lib/file/vfs/vfs.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2017 Wildfire Games. +/* Copyright (C) 2020 Wildfire Games. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the @@ -31,6 +31,9 @@ #include "lib/file/file_system.h" // CFileInfo #include "lib/file/vfs/vfs_path.h" +constexpr size_t VFS_MIN_PRIORITY = 0; +constexpr size_t VFS_MAX_PRIORITY = std::numeric_limits::max(); + namespace ERR { const Status VFS_DIR_NOT_FOUND = -110100; Index: source/lib/file/vfs/vfs_populate.cpp =================================================================== --- source/lib/file/vfs/vfs_populate.cpp +++ source/lib/file/vfs/vfs_populate.cpp @@ -168,7 +168,29 @@ Status vfs_Attach(VfsDirectory* directory, const PRealDirectory& realDirectory) { - RETURN_STATUS_IF_ERR(vfs_Populate(directory)); + PRealDirectory existingRealDir = directory->AssociatedDirectory(); + + // Don't allow replacing the real directory by a lower-priority one. + if (!existingRealDir || existingRealDir->Priority() < realDirectory->Priority()) + { + // This ordering is peculiar but useful, as it "defers" the population call. + // If there is already a real directory, we will replace it (and lose track of it), + // so we'll populate it right away, but the 'new' real directory can wait until we access it. + RETURN_STATUS_IF_ERR(vfs_Populate(directory)); + directory->SetAssociatedDirectory(realDirectory); + return INFO::OK; + } + // We are attaching a lower-priority real directory. + // Because of deferred population, we need to immediately populate this new directory. + bool shouldPop = directory->ShouldPopulate(); + // This sets "should populate" to true, so the vfs_Populate call below immediately populates. directory->SetAssociatedDirectory(realDirectory); + RETURN_STATUS_IF_ERR(vfs_Populate(directory)); + // Reset to the higher priority realDirectory, which resets ShouldPopulate to true. + directory->SetAssociatedDirectory(existingRealDir); + // Avoir un-necessary repopulation by clearing the flag. + if (!shouldPop) + directory->ShouldPopulate(); + return INFO::OK; } Index: source/main.cpp =================================================================== --- source/main.cpp +++ source/main.cpp @@ -619,7 +619,8 @@ Paths paths(args); g_VFS = CreateVfs(); - g_VFS->Mount(L"cache/", paths.Cache(), VFS_MOUNT_ARCHIVABLE); + // Mount with highest priority, we don't want mods overwriting this. + g_VFS->Mount(L"cache/", paths.Cache(), VFS_MOUNT_ARCHIVABLE, VFS_MAX_PRIORITY); MountMods(paths, GetMods(args, INIT_MODS)); { Index: source/ps/ArchiveBuilder.cpp =================================================================== --- source/ps/ArchiveBuilder.cpp +++ source/ps/ArchiveBuilder.cpp @@ -35,10 +35,10 @@ DeleteDirectory(m_TempDir/"_archivecache"); // clean up in case the last run failed - m_VFS->Mount(L"cache/", m_TempDir/"_archivecache"/""); + m_VFS->Mount(L"cache/", m_TempDir/"_archivecache"/"", 0, VFS_MAX_PRIORITY); // Mount with highest priority so base mods do not overwrite files in this mod - m_VFS->Mount(L"", mod/"", VFS_MOUNT_MUST_EXIST | VFS_MOUNT_KEEP_DELETED, (size_t)-1); + m_VFS->Mount(L"", mod/"", VFS_MOUNT_MUST_EXIST | VFS_MOUNT_KEEP_DELETED, VFS_MAX_PRIORITY); // Collect the list of files before loading any base mods vfs::ForEachFile(m_VFS, L"", &CollectFileCB, (uintptr_t)static_cast(this), 0, vfs::DIR_RECURSIVE); Index: source/ps/GameSetup/GameSetup.cpp =================================================================== --- source/ps/GameSetup/GameSetup.cpp +++ source/ps/GameSetup/GameSetup.cpp @@ -451,23 +451,20 @@ g_VFS = CreateVfs(); const OsPath readonlyConfig = paths.RData()/"config"/""; - g_VFS->Mount(L"config/", readonlyConfig); - // Engine localization files. + // Mount these dirs with highest priority so that mods can't overwrite them. + g_VFS->Mount(L"cache/", paths.Cache(), VFS_MOUNT_ARCHIVABLE, VFS_MAX_PRIORITY); // (adding XMBs to archive speeds up subsequent reads) + if(readonlyConfig != paths.Config()) + g_VFS->Mount(L"config/", readonlyConfig, 0, VFS_MAX_PRIORITY-1); + g_VFS->Mount(L"config/", paths.Config(), 0, VFS_MAX_PRIORITY); + g_VFS->Mount(L"screenshots/", paths.UserData()/"screenshots"/"", 0, VFS_MAX_PRIORITY); + g_VFS->Mount(L"saves/", paths.UserData()/"saves"/"", VFS_MOUNT_WATCH, VFS_MAX_PRIORITY); + + // Engine localization files (regular priority, these can be overwritten). g_VFS->Mount(L"l10n/", paths.RData()/"l10n"/""); MountMods(paths, GetMods(args, flags)); - // We mount these dirs last as otherwise writing could result in files being placed in a mod's dir. - g_VFS->Mount(L"screenshots/", paths.UserData()/"screenshots"/""); - g_VFS->Mount(L"saves/", paths.UserData()/"saves"/"", VFS_MOUNT_WATCH); - // Mounting with highest priority, so that a mod supplied user.cfg is harmless - g_VFS->Mount(L"config/", readonlyConfig, 0, (size_t)-1); - if(readonlyConfig != paths.Config()) - g_VFS->Mount(L"config/", paths.Config(), 0, (size_t)-1); - - g_VFS->Mount(L"cache/", paths.Cache(), VFS_MOUNT_ARCHIVABLE); // (adding XMBs to archive speeds up subsequent reads) - // note: don't bother with g_VFS->TextRepresentation - directories // haven't yet been populated and are empty. } Index: source/ps/Mod.cpp =================================================================== --- source/ps/Mod.cpp +++ source/ps/Mod.cpp @@ -59,7 +59,8 @@ for (DirectoryNames::iterator iter = modDirs.begin(); iter != modDirs.end(); ++iter) { vfs->Clear(); - if (vfs->Mount(L"", modPath / *iter, VFS_MOUNT_MUST_EXIST) < 0) + // Mount with lowest priority, we don't want to overwrite anything + if (vfs->Mount(L"", modPath / *iter, VFS_MOUNT_MUST_EXIST, VFS_MIN_PRIORITY) < 0) continue; CVFSFile modinfo; @@ -85,7 +86,8 @@ continue; vfs->Clear(); - if (vfs->Mount(L"", modUserPath / *iter, VFS_MOUNT_MUST_EXIST) < 0) + // Mount with lowest priority, we don't want to overwrite anything + if (vfs->Mount(L"", modUserPath / *iter, VFS_MOUNT_MUST_EXIST, VFS_MIN_PRIORITY) < 0) continue; CVFSFile modinfo;