mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-21 10:19:04 +02:00
Replacement: Find pack files whose names differ in case
- Fixes #18746 Packs are mostly made on Windows, where "portraits/x.png" in textures.ini finds Portraits/x.png. On a case sensitive file system (Steam Deck, Linux) it didn't, while zipped packs already ignored case. DirectoryReader gets an opt-in ignoreCase mode, used for texture packs. On the first miss it indexes the directory by lowercased path and retries there, so packs where every name matches never pay for it, and the walk happens on the IO thread that loads the texture. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
034901ee87
commit
5cc5a2d802
4 files changed
+111
-7
No files matched your search
@@ -1,16 +1,73 @@
|
||||
#include "ppsspp_config.h"
|
||||
|
||||
#include <cctype>
|
||||
#include <cstdio>
|
||||
|
||||
#include "Common/Common.h"
|
||||
#include "Common/Log.h"
|
||||
#include "Common/File/DirListing.h"
|
||||
#include "Common/File/VFS/DirectoryReader.h"
|
||||
|
||||
DirectoryReader::DirectoryReader(const Path &path) {
|
||||
DirectoryReader::DirectoryReader(const Path &path, bool ignoreCase) {
|
||||
path_ = path;
|
||||
#if !PPSSPP_PLATFORM(WINDOWS)
|
||||
// Windows file systems already ignore case.
|
||||
ignoreCase_ = ignoreCase;
|
||||
#endif
|
||||
}
|
||||
|
||||
static std::string ToLowerASCII(std::string_view str) {
|
||||
std::string lower(str);
|
||||
for (char &c : lower) {
|
||||
c = (char)tolower((unsigned char)c);
|
||||
}
|
||||
return lower;
|
||||
}
|
||||
|
||||
void DirectoryReader::IndexDirectory(const Path &dir, const std::string &prefix) {
|
||||
std::vector<File::FileInfo> files;
|
||||
File::GetFilesInDir(dir, &files);
|
||||
for (const File::FileInfo &file : files) {
|
||||
const std::string relative = prefix + file.name;
|
||||
if (file.isDirectory) {
|
||||
IndexDirectory(file.fullName, relative + "/");
|
||||
} else {
|
||||
lowerIndex_[ToLowerASCII(relative)] = relative;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool DirectoryReader::FindIgnoringCase(std::string_view path, std::string *actualPath) {
|
||||
if (!ignoreCase_) {
|
||||
return false;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(indexLock_);
|
||||
if (!indexBuilt_) {
|
||||
// Only done on the first miss, so a directory where every name matches never pays for it.
|
||||
IndexDirectory(path_, "");
|
||||
indexBuilt_ = true;
|
||||
INFO_LOG(Log::IO, "Indexed %d files in %s to find names that differ in case", (int)lowerIndex_.size(), path_.c_str());
|
||||
}
|
||||
auto it = lowerIndex_.find(ToLowerASCII(path));
|
||||
if (it == lowerIndex_.end()) {
|
||||
return false;
|
||||
}
|
||||
*actualPath = it->second;
|
||||
if (caseFixCount_++ < 5) {
|
||||
WARN_LOG(Log::IO, "'%.*s' differs in case from the file '%s' in %s, which only works on some file systems", STR_VIEW(path), actualPath->c_str(), path_.c_str());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
uint8_t *DirectoryReader::ReadFile(std::string_view path, size_t *size) {
|
||||
Path new_path = Path(path).StartsWith(path_) ? Path(path) : path_ / path;
|
||||
return File::ReadLocalFile(new_path, size);
|
||||
const bool absolute = Path(path).StartsWith(path_);
|
||||
Path new_path = absolute ? Path(path) : path_ / path;
|
||||
uint8_t *data = File::ReadLocalFile(new_path, size);
|
||||
std::string actualPath;
|
||||
if (!data && !absolute && FindIgnoringCase(path, &actualPath)) {
|
||||
data = File::ReadLocalFile(path_ / actualPath, size);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
bool DirectoryReader::GetFileListing(std::string_view path, std::vector<File::FileInfo> *listing, const char *filter = nullptr) {
|
||||
@@ -53,7 +110,11 @@ public:
|
||||
VFSFileReference *DirectoryReader::GetFile(std::string_view path) {
|
||||
Path filePath = path_ / path;
|
||||
if (!File::Exists(filePath)) {
|
||||
return nullptr;
|
||||
std::string actualPath;
|
||||
if (!FindIgnoringCase(path, &actualPath)) {
|
||||
return nullptr;
|
||||
}
|
||||
filePath = path_ / actualPath;
|
||||
}
|
||||
|
||||
DirectoryReaderFileReference *reference = new DirectoryReaderFileReference();
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "Common/File/VFS/VFS.h"
|
||||
#include "Common/File/FileUtil.h"
|
||||
#include "Common/File/Path.h"
|
||||
|
||||
class DirectoryReader : public VFSBackend {
|
||||
public:
|
||||
explicit DirectoryReader(const Path &path);
|
||||
// With ignoreCase, GetFile and ReadFile also find files whose names only differ in case, like the
|
||||
// zip reader does. Meant for directories that people fill by hand on Windows, like texture packs,
|
||||
// which then break on case sensitive file systems.
|
||||
explicit DirectoryReader(const Path &path, bool ignoreCase = false);
|
||||
// use delete[] on the returned value.
|
||||
uint8_t *ReadFile(std::string_view path, size_t *size) override;
|
||||
|
||||
@@ -27,5 +34,16 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
// Returns the path (relative to path_) of a file whose name matches path except for case.
|
||||
bool FindIgnoringCase(std::string_view path, std::string *actualPath);
|
||||
void IndexDirectory(const Path &dir, const std::string &prefix);
|
||||
|
||||
Path path_;
|
||||
bool ignoreCase_ = false;
|
||||
|
||||
// Lowercase relative path -> actual relative path of every file, built on the first miss.
|
||||
std::mutex indexLock_;
|
||||
bool indexBuilt_ = false;
|
||||
std::unordered_map<std::string, std::string> lowerIndex_;
|
||||
int caseFixCount_ = 0;
|
||||
};
|
||||
@@ -169,7 +169,7 @@ bool TextureReplacer::LoadIni(std::string *error, bool notify) {
|
||||
if (!dir) {
|
||||
INFO_LOG(Log::TexReplacement, "%s wasn't a zip file - opening the directory %s instead.", zipPath.c_str(), basePath_.c_str());
|
||||
vfsIsZip_ = false;
|
||||
dir = new DirectoryReader(basePath_);
|
||||
dir = new DirectoryReader(basePath_, true);
|
||||
} else {
|
||||
if (!replaceEnabled_ && saveEnabled_) {
|
||||
WARN_LOG(Log::TexReplacement, "Found zip file even though only saving is enabled! This is weird.");
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "Common/Data/Format/PNGLoad.h"
|
||||
#include "Common/File/FileUtil.h"
|
||||
#include "Common/File/Path.h"
|
||||
#include "Common/File/VFS/DirectoryReader.h"
|
||||
#include "Common/Thread/ThreadManager.h"
|
||||
#include "GPU/Common/TextureReplacer.h"
|
||||
|
||||
@@ -711,6 +712,30 @@ static bool TestSaveIndexedOption(const Path &packDir) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Packs made on Windows often name their folders differently in textures.ini (#18746). On a case
|
||||
// insensitive file system this passes without the index, Linux CI is where it counts.
|
||||
static bool TestDirectoryIgnoreCase(const Path &packDir) {
|
||||
if (!File::CreateDir(packDir / "Portraits")) {
|
||||
return false;
|
||||
}
|
||||
if (!CreateTestPNG(packDir / "Portraits" / "Face.png", 4, 4, 0xFF0000FF)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
DirectoryReader reader(packDir, true);
|
||||
VFSFileReference *ref = reader.GetFile("portraits/face.png");
|
||||
EXPECT_TRUE(ref != nullptr);
|
||||
if (ref) {
|
||||
reader.ReleaseFile(ref);
|
||||
}
|
||||
size_t size = 0;
|
||||
uint8_t *data = reader.ReadFile("PORTRAITS/FACE.PNG", &size);
|
||||
EXPECT_TRUE(data != nullptr && size != 0);
|
||||
delete[] data;
|
||||
EXPECT_TRUE(reader.GetFile("portraits/missing.png") == nullptr);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TestTextureReplacer() {
|
||||
Path packDir = UnitTestTempPath("unittest_texture_pack");
|
||||
if (!CreateTestPack(packDir)) {
|
||||
@@ -741,7 +766,7 @@ bool TestTextureReplacer() {
|
||||
}
|
||||
|
||||
if (!TestIndexedPNG(packDir) || !TestIndexedReplacement(packDir) || !TestIndexedKTX2(packDir) ||
|
||||
!TestSaveIndexedPNG(packDir) || !TestSaveIndexedOption(packDir)) {
|
||||
!TestSaveIndexedPNG(packDir) || !TestSaveIndexedOption(packDir) || !TestDirectoryIgnoreCase(packDir)) {
|
||||
File::DeleteDirRecursively(packDir);
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user