/* Copyright (C) 2021 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. */ /* * populate VFS directories with files */ #include "precompiled.h" #include "lib/file/vfs/vfs_populate.h" #include "lib/file/archive/archive_zip.h" #include "lib/file/vfs/vfs_tree.h" #include "lib/file/vfs/vfs_lookup.h" #include "lib/file/vfs/vfs.h" // error codes struct CompareFileInfoByName { bool operator()(const CFileInfo& a, const CFileInfo& b) { return a.Name() < b.Name(); } }; // helper class that allows breaking up the logic into sub-functions without // always having to pass directory/realDirectory as parameters. class PopulateHelper { NONCOPYABLE(PopulateHelper); public: PopulateHelper(VfsDirectory* directory, const PRealDirectory& realDirectory) : m_directory(directory), m_realDirectory(realDirectory) { } Status AddEntries() const { CFileInfos files; files.reserve(500); DirectoryNames subdirectoryNames; subdirectoryNames.reserve(50); RETURN_STATUS_IF_ERR(GetDirectoryEntries(m_realDirectory->Path(), &files, &subdirectoryNames)); // Since .DELETED files only remove files in lower priority mods // loose files and archive files have no conflicts so we do not need // to sort them. // We add directories after they might have been removed by .DELETED // files (as they did not contain any files at that point). The order // of GetDirectoryEntries is undefined, but that does not really matter (TODO really?) // so we do not need to sort its output. RETURN_STATUS_IF_ERR(AddFiles(files)); AddSubdirectories(subdirectoryNames); return INFO::OK; } private: void AddFile(const CFileInfo& fileInfo) const { const VfsPath name = fileInfo.Name(); const VfsFile file(name, (size_t)fileInfo.Size(), fileInfo.MTime(), m_realDirectory->Priority(), m_realDirectory); if(name.Extension() == L".DELETED") { m_directory->DeleteSubtree(file); if(!(m_realDirectory->Flags() & VFS_MOUNT_KEEP_DELETED)) return; } m_directory->AddFile(file); } static void AddArchiveFile(const VfsPath& pathname, const CFileInfo& fileInfo, PIArchiveFile archiveFile, uintptr_t cbData) { PopulateHelper* this_ = (PopulateHelper*)cbData; // (we have to create missing subdirectoryNames because archivers // don't always place directory entries before their files) const size_t flags = VFS_LOOKUP_ADD|VFS_LOOKUP_SKIP_POPULATE; VfsDirectory* directory; WARN_IF_ERR(vfs_Lookup(pathname, this_->m_directory, directory, 0, flags)); const VfsPath name = fileInfo.Name(); const VfsFile file(name, (size_t)fileInfo.Size(), fileInfo.MTime(), this_->m_realDirectory->Priority(), archiveFile); if(name.Extension() == L".DELETED") { directory->DeleteSubtree(file); if(!(this_->m_realDirectory->Flags() & VFS_MOUNT_KEEP_DELETED)) return; } directory->AddFile(file); } Status AddFiles(const CFileInfos& files) const { const OsPath path(m_realDirectory->Path()); for(size_t i = 0; i < files.size(); i++) { const OsPath pathname = path / files[i].Name(); if(pathname.Extension() == L".zip") { PIArchiveReader archiveReader = CreateArchiveReader_Zip(pathname); // archiveReader == nullptr if file could not be opened (e.g. because // archive is currently open in another program) if(archiveReader) RETURN_STATUS_IF_ERR(archiveReader->ReadEntries(AddArchiveFile, (uintptr_t)this)); } else // regular (non-archive) file AddFile(files[i]); } return INFO::OK; } void AddSubdirectories(const DirectoryNames& subdirectoryNames) const { for(size_t i = 0; i < subdirectoryNames.size(); i++) { // skip version control directories - this avoids cluttering the // VFS with hundreds of irrelevant files. if(subdirectoryNames[i] == L".svn" || subdirectoryNames[i] == L".git") continue; VfsDirectory* subdirectory = m_directory->AddSubdirectory(subdirectoryNames[i]); PRealDirectory realDirectory = CreateRealSubdirectory(m_realDirectory, subdirectoryNames[i]); vfs_Attach(subdirectory, realDirectory); } } VfsDirectory* const m_directory; PRealDirectory m_realDirectory; }; Status vfs_Populate(VfsDirectory* directory) { if(!directory->ShouldPopulate()) return INFO::OK; const PRealDirectory& realDirectory = directory->AssociatedDirectory(); if(realDirectory->Flags() & VFS_MOUNT_WATCH) realDirectory->Watch(); PopulateHelper helper(directory, realDirectory); RETURN_STATUS_IF_ERR(helper.AddEntries()); return INFO::OK; } Status vfs_Attach(VfsDirectory* directory, const PRealDirectory& realDirectory) { 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); // Avoid un-necessary repopulation by clearing the flag. if (!shouldPop) directory->ShouldPopulate(); return INFO::OK; }