Add a unit test, fix listing zip directories

This commit is contained in:
Henrik Rydgård 2023-05-02 11:35:45 +02:00
parent d10fae7274
commit ee7e8d7c06
11 changed files with 175 additions and 49 deletions

View File

@ -2481,6 +2481,7 @@ if(UNITTEST)
unittest/TestIRPassSimplify.cpp
unittest/TestX64Emitter.cpp
unittest/TestVertexJit.cpp
unittest/TestVFS.cpp
unittest/TestRiscVEmitter.cpp
unittest/TestSoftwareGPUJit.cpp
unittest/TestThreadManager.cpp

View File

@ -37,6 +37,7 @@ class VFSInterface {
public:
virtual ~VFSInterface() {}
virtual uint8_t *ReadFile(const char *path, size_t *size) = 0;
// If listing already contains files, it'll be cleared.
virtual bool GetFileListing(const char *path, std::vector<File::FileInfo> *listing, const char *filter = nullptr) = 0;
};

View File

@ -38,10 +38,13 @@ ZipFileReader *ZipFileReader::Create(const Path &zipFile, const char *inZipPath,
return nullptr;
}
ZipFileReader *reader = new ZipFileReader();
reader->zip_file_ = zip_file;
truncate_cpy(reader->inZipPath_, inZipPath);
return reader;
// The inZipPath is supposed to be a folder, and internally in this class, we suffix
// folder paths with '/', matching how the zip library works.
std::string path = inZipPath;
if (!path.empty() && path.back() != '/') {
path.push_back('/');
}
return new ZipFileReader(zip_file, path);
}
ZipFileReader::~ZipFileReader() {
@ -50,16 +53,15 @@ ZipFileReader::~ZipFileReader() {
}
uint8_t *ZipFileReader::ReadFile(const char *path, size_t *size) {
char temp_path[2048];
snprintf(temp_path, sizeof(temp_path), "%s%s", inZipPath_, path);
std::string temp_path = inZipPath_ + path;
std::lock_guard<std::mutex> guard(lock_);
// Figure out the file size first.
struct zip_stat zstat;
zip_stat(zip_file_, temp_path, ZIP_FL_NOCASE | ZIP_FL_UNCHANGED, &zstat);
zip_file *file = zip_fopen(zip_file_, temp_path, ZIP_FL_NOCASE | ZIP_FL_UNCHANGED);
zip_stat(zip_file_, temp_path.c_str(), ZIP_FL_NOCASE | ZIP_FL_UNCHANGED, &zstat);
zip_file *file = zip_fopen(zip_file_, temp_path.c_str(), ZIP_FL_NOCASE | ZIP_FL_UNCHANGED);
if (!file) {
ERROR_LOG(IO, "Error opening %s from ZIP", temp_path);
ERROR_LOG(IO, "Error opening %s from ZIP", temp_path.c_str());
return 0;
}
uint8_t *contents = new uint8_t[zstat.size + 1];
@ -72,8 +74,10 @@ uint8_t *ZipFileReader::ReadFile(const char *path, size_t *size) {
}
bool ZipFileReader::GetFileListing(const char *orig_path, std::vector<File::FileInfo> *listing, const char *filter = 0) {
char path[2048];
snprintf(path, sizeof(path), "%s%s", inZipPath_, orig_path);
std::string path = std::string(inZipPath_) + orig_path;
if (!path.empty() && path.back() != '/') {
path.push_back('/');
}
std::set<std::string> filters;
std::string tmp;
@ -95,17 +99,27 @@ bool ZipFileReader::GetFileListing(const char *orig_path, std::vector<File::File
// We just loop through the whole ZIP file and deduce what files are in this directory, and what subdirectories there are.
std::set<std::string> files;
std::set<std::string> directories;
GetZipListings(path, files, directories);
bool success = GetZipListings(path, files, directories);
if (!success) {
// This means that no file prefix matched the path.
return false;
}
listing->clear();
INFO_LOG(SYSTEM, "Listing %s", orig_path);
for (auto diter = directories.begin(); diter != directories.end(); ++diter) {
File::FileInfo info;
info.name = *diter;
// Remove the "inzip" part of the fullname.
info.fullName = Path(std::string(path).substr(strlen(inZipPath_))) / *diter;
std::string relativePath = std::string(path).substr(inZipPath_.size());
info.fullName = Path(relativePath + *diter);
info.exists = true;
info.isWritable = false;
info.isDirectory = true;
INFO_LOG(SYSTEM, "Found file: %s (%s)", info.name.c_str(), info.fullName.c_str());
listing->push_back(info);
}
@ -113,7 +127,8 @@ bool ZipFileReader::GetFileListing(const char *orig_path, std::vector<File::File
std::string fpath = path;
File::FileInfo info;
info.name = *fiter;
info.fullName = Path(std::string(path).substr(strlen(inZipPath_))) / *fiter;
std::string relativePath = std::string(path).substr(inZipPath_.size());
info.fullName = Path(relativePath + *fiter);
info.exists = true;
info.isWritable = false;
info.isDirectory = false;
@ -123,6 +138,7 @@ bool ZipFileReader::GetFileListing(const char *orig_path, std::vector<File::File
continue;
}
}
INFO_LOG(SYSTEM, "Found dir: %s (%s)", info.name.c_str(), info.fullName.c_str());
listing->push_back(info);
}
@ -130,39 +146,44 @@ bool ZipFileReader::GetFileListing(const char *orig_path, std::vector<File::File
return true;
}
void ZipFileReader::GetZipListings(const char *path, std::set<std::string> &files, std::set<std::string> &directories) {
size_t pathlen = strlen(path);
if (pathlen == 1 && path[0] == '/') {
// Root. We simply use a zero length string.
pathlen = 0;
}
// path here is from the root, so inZipPath needs to already be added.
bool ZipFileReader::GetZipListings(const std::string &path, std::set<std::string> &files, std::set<std::string> &directories) {
_dbg_assert_(path.empty() || path.back() == '/');
std::lock_guard<std::mutex> guard(lock_);
int numFiles = zip_get_num_files(zip_file_);
bool anyPrefixMatched = false;
for (int i = 0; i < numFiles; i++) {
const char* name = zip_get_name(zip_file_, i, 0);
if (!name)
continue; // shouldn't happen, I think
if (!memcmp(name, path, pathlen)) {
const char *slashPos = strchr(name + pathlen + 1, '/');
if (startsWith(name, path)) {
if (strlen(name) == path.size()) {
// Don't want to return the same folder.
continue;
}
const char *slashPos = strchr(name + path.size(), '/');
if (slashPos != 0) {
anyPrefixMatched = true;
// A directory. Let's pick off the only part we care about.
int offset = pathlen;
size_t offset = path.size();
std::string dirName = std::string(name + offset, slashPos - (name + offset));
// We might get a lot of these if the tree is deep. The std::set deduplicates.
directories.insert(dirName);
} else {
anyPrefixMatched = true;
// It's a file.
const char *fn = name + pathlen;
const char *fn = name + path.size();
files.insert(std::string(fn));
}
}
}
return anyPrefixMatched;
}
bool ZipFileReader::GetFileInfo(const char *path, File::FileInfo *info) {
struct zip_stat zstat;
char temp_path[1024];
snprintf(temp_path, sizeof(temp_path), "%s%s", inZipPath_, path);
std::string temp_path = inZipPath_ + path;
// Clear some things to start.
info->isDirectory = false;
@ -171,7 +192,7 @@ bool ZipFileReader::GetFileInfo(const char *path, File::FileInfo *info) {
{
std::lock_guard<std::mutex> guard(lock_);
if (0 != zip_stat(zip_file_, temp_path, ZIP_FL_NOCASE | ZIP_FL_UNCHANGED, &zstat)) {
if (0 != zip_stat(zip_file_, temp_path.c_str(), ZIP_FL_NOCASE | ZIP_FL_UNCHANGED, &zstat)) {
// ZIP files do not have real directories, so we'll end up here if we
// try to stat one. For now that's fine.
info->exists = false;

View File

@ -40,9 +40,11 @@ public:
}
private:
void GetZipListings(const char *path, std::set<std::string> &files, std::set<std::string> &directories);
ZipFileReader(zip *zip_file, const std::string &inZipPath) : zip_file_(zip_file), inZipPath_(inZipPath) {}
// Path has to be either an empty string, or a string ending with a /.
bool GetZipListings(const std::string &path, std::set<std::string> &files, std::set<std::string> &directories);
zip *zip_file_ = nullptr;
std::mutex lock_;
char inZipPath_[256];
std::string inZipPath_;
};

View File

@ -160,7 +160,7 @@ bool TextureReplacer::LoadIni() {
}
INFO_LOG(G3D, "Loading extra texture ini: %s", overrideFilename.c_str());
if (!LoadIniValues(overrideIni, dir, true)) {
if (!LoadIniValues(overrideIni, nullptr, true)) {
delete dir;
return false;
}
@ -261,26 +261,28 @@ bool TextureReplacer::LoadIniValues(IniFile &ini, VFSBackend *dir, bool isOverri
// Scan the root of the texture folder/zip and preinitialize the hash map.
std::vector<File::FileInfo> filesInRoot;
dir->GetFileListing("/", &filesInRoot, nullptr);
for (auto file : filesInRoot) {
if (file.isDirectory)
continue;
if (file.name.empty() || file.name[0] == '.')
continue;
Path path(file.name);
std::string ext = path.GetFileExtension();
if (dir) {
dir->GetFileListing("", &filesInRoot, nullptr);
for (auto file : filesInRoot) {
if (file.isDirectory)
continue;
if (file.name.empty() || file.name[0] == '.')
continue;
Path path(file.name);
std::string ext = path.GetFileExtension();
std::string hash = file.name.substr(0, file.name.size() - ext.size());
if (!((hash.size() >= 26 && hash.size() <= 27 && hash[24] == '_') || hash.size() == 24)) {
continue;
}
// OK, it's hash-like enough to try to parse it into the map.
if (equalsNoCase(ext, ".ktx2") || equalsNoCase(ext, ".png") || equalsNoCase(ext, ".dds")) {
ReplacementCacheKey key(0, 0);
int level = 0; // sscanf might fail to pluck the level, but that's ok, we default to 0. sscanf doesn't write to non-matched outputs.
if (sscanf(hash.c_str(), "%16llx%8x_%d", &key.cachekey, &key.hash, &level) >= 1) {
INFO_LOG(G3D, "hash-like file in root, adding: %s", file.name.c_str());
filenameMap[key][level] = file.name;
std::string hash = file.name.substr(0, file.name.size() - ext.size());
if (!((hash.size() >= 26 && hash.size() <= 27 && hash[24] == '_') || hash.size() == 24)) {
continue;
}
// OK, it's hash-like enough to try to parse it into the map.
if (equalsNoCase(ext, ".ktx2") || equalsNoCase(ext, ".png") || equalsNoCase(ext, ".dds") || equalsNoCase(ext, ".zim")) {
ReplacementCacheKey key(0, 0);
int level = 0; // sscanf might fail to pluck the level, but that's ok, we default to 0. sscanf doesn't write to non-matched outputs.
if (sscanf(hash.c_str(), "%16llx%8x_%d", &key.cachekey, &key.hash, &level) >= 1) {
// INFO_LOG(G3D, "hash-like file in root, adding: %s", file.name.c_str());
filenameMap[key][level] = file.name;
}
}
}
}

View File

@ -811,6 +811,7 @@ ifeq ($(UNITTEST),1)
$(SRC)/unittest/TestSoftwareGPUJit.cpp \
$(SRC)/unittest/TestThreadManager.cpp \
$(SRC)/unittest/TestVertexJit.cpp \
$(SRC)/unittest/TestVFS.cpp \
$(TESTARMEMITTER_FILE) \
$(SRC)/unittest/UnitTest.cpp

BIN
source_assets/ziptest.zip Normal file

Binary file not shown.

94
unittest/TestVFS.cpp Normal file
View File

@ -0,0 +1,94 @@
#include <thread>
#include <vector>
#include "Common/Log.h"
#include "Common/File/VFS/ZipFileReader.h"
#include "UnitTest.h"
static bool CheckContainsDir(const std::vector<File::FileInfo> &listing, const char *name) {
for (auto &file : listing) {
if (file.name == name && file.isDirectory) {
return true;
}
}
return false;
}
static bool CheckContainsFile(const std::vector<File::FileInfo> &listing, const char *name) {
for (auto &file : listing) {
if (file.name == name && !file.isDirectory) {
return true;
}
}
return false;
}
// ziptest.zip file structure:
//
// ziptest/
// data/
// a/
// in_a.txt
// b/
// in_b.txt
// argh.txt
// big.txt
// lang/
// en_us.txt
// sv_se.txt
// langregion.txt
// in_root.txt
// TODO: Also test the filter.
bool TestZipFile() {
// First, check things relative to root, with an empty internal path.
Path zipPath = Path("../source_assets/ziptest.zip");
if (!File::Exists(zipPath)) {
zipPath = Path("source_assets/ziptest.zip");
}
ZipFileReader *dir = ZipFileReader::Create(zipPath, "", true);
EXPECT_TRUE(dir != nullptr);
std::vector<File::FileInfo> listing;
EXPECT_TRUE(dir->GetFileListing("", &listing, nullptr));
EXPECT_EQ_INT(listing.size(), 2);
EXPECT_TRUE(CheckContainsDir(listing, "ziptest"));
EXPECT_TRUE(CheckContainsFile(listing, "in_root.txt"));
EXPECT_FALSE(dir->GetFileListing("ziptestwrong", &listing, nullptr));
// Next, do a file listing in a directory, but keep the root.
EXPECT_TRUE(dir->GetFileListing("ziptest", &listing, nullptr));
EXPECT_EQ_INT(listing.size(), 3);
EXPECT_TRUE(CheckContainsDir(listing, "data"));
EXPECT_TRUE(CheckContainsDir(listing, "lang"));
EXPECT_TRUE(CheckContainsFile(listing, "langregion.txt"));
delete dir;
// Next, we'll destroy the reader and create a new one based in a subdirectory.
dir = ZipFileReader::Create(zipPath, "ziptest/data", true);
EXPECT_TRUE(dir != nullptr);
EXPECT_TRUE(dir->GetFileListing("", &listing, nullptr));
EXPECT_EQ_INT(listing.size(), 4);
EXPECT_TRUE(CheckContainsDir(listing, "a"));
EXPECT_TRUE(CheckContainsDir(listing, "b"));
EXPECT_TRUE(CheckContainsFile(listing, "argh.txt"));
EXPECT_TRUE(CheckContainsFile(listing, "big.txt"));
EXPECT_TRUE(dir->GetFileListing("a", &listing, nullptr));
EXPECT_TRUE(CheckContainsFile(listing, "in_a.txt"));
EXPECT_EQ_INT(listing.size(), 1);
EXPECT_TRUE(dir->GetFileListing("b", &listing, nullptr));
EXPECT_TRUE(CheckContainsFile(listing, "in_b.txt"));
EXPECT_EQ_INT(listing.size(), 1);
delete dir;
return true;
}
bool TestVFS() {
if (!TestZipFile())
return false;
return true;
}

View File

@ -930,6 +930,7 @@ bool TestShaderGenerators();
bool TestSoftwareGPUJit();
bool TestIRPassSimplify();
bool TestThreadManager();
bool TestVFS();
TestItem availableTests[] = {
#if PPSSPP_ARCH(ARM64) || PPSSPP_ARCH(AMD64) || PPSSPP_ARCH(X86)
@ -968,6 +969,7 @@ TestItem availableTests[] = {
TEST_ITEM(DepthMath),
TEST_ITEM(InputMapping),
TEST_ITEM(EscapeMenuString),
TEST_ITEM(VFS),
};
int main(int argc, const char *argv[]) {

View File

@ -397,6 +397,7 @@
<ClCompile Include="TestSoftwareGPUJit.cpp" />
<ClCompile Include="TestThreadManager.cpp" />
<ClCompile Include="TestVertexJit.cpp" />
<ClCompile Include="TestVFS.cpp" />
<ClCompile Include="UnitTest.cpp" />
<ClCompile Include="TestArmEmitter.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">true</ExcludedFromBuild>

View File

@ -16,6 +16,7 @@
<ClCompile Include="TestSoftwareGPUJit.cpp" />
<ClCompile Include="TestIRPassSimplify.cpp" />
<ClCompile Include="TestRiscVEmitter.cpp" />
<ClCompile Include="TestVFS.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="JitHarness.h" />