Files
ppsspp/Common/File/DirListing.cpp
T
libretroadmin f0b97a2e90 Remove the standalone frontends
The libretro core is the frontend, and the headless tools are the test and
debugging hosts. The Windows, UWP, Android, iOS, macOS and SDL apps, the UI
framework and imgui go, along with what only they used: the Win32 debugger
hooks (g_frameMutex, the shutdown lock), JNI thread attachment, the Android
text drawer, RAIntegration, Discord, recent files, game installs, tilt input,
the audio resamplers, the control mapper, memstick moving and the remote ISO
and upload server modes. The web server keeps the debugger. The MSVC, Gradle
and ndk-build projects go too.
2026-10-09 04:56:10 +02:00

408 lines
11 KiB
C++

#include "ppsspp_config.h"
#if PPSSPP_PLATFORM(WINDOWS)
#define WIN32_LEAN_AND_MEAN
#include "Common/CommonWindows.h"
#include <direct.h>
#if PPSSPP_PLATFORM(UWP)
#include <fileapifromapp.h>
#endif
#else
#include <strings.h>
#include <dirent.h>
#include <unistd.h>
#endif
#include <cstring>
#include <string>
#include <set>
#include <cstdio>
#include <sys/stat.h>
#include <cctype>
#include <algorithm> // remove_if
#include "Common/Data/Encoding/Utf8.h"
#include "Common/StringUtils.h"
#include "Common/Net/URL.h"
#include "Common/File/DirListing.h"
#include "Common/File/FileUtil.h"
#include "Common/File/AndroidStorage.h"
#include "Common/TimeUtil.h"
#include "Common/Log.h"
#if !defined(__linux__) && !defined(_WIN32) && !defined(__QNX__)
#define stat64 stat
#endif
#ifdef HAVE_LIBNX
// Far from optimal, but I guess it works...
#define fseeko fseek
#define ftello ftell
#define fileno
#endif // HAVE_LIBNX
constexpr bool LOG_IO = false;
namespace File {
#if PPSSPP_PLATFORM(WINDOWS)
static uint64_t FiletimeToStatTime(FILETIME ft, int* out_usec) {
constexpr int64_t windowsTickResolution = 10000000;
constexpr int64_t secToUnixEpoch = 11644473600LL;
const int64_t ticks = ((uint64_t)ft.dwHighDateTime << 32) | ft.dwLowDateTime;
if (out_usec) {
// The remainder will be between 0 and 9,999,999 ticks.
// Dividing by 10 gives 0 to 999,999 microseconds.
*out_usec = (int)((ticks % windowsTickResolution) / 10);
}
return (uint64_t)(ticks / windowsTickResolution - secToUnixEpoch);
}
// This is used for both WIN32_FILE_ATTRIBUTE_DATA and WIN32_FIND_DATA, which have the same time and size fields.
template<class T>
void ReadFileAttributes(FileInfo *fileInfo, const T &attrs) {
fileInfo->atime = FiletimeToStatTime(attrs.ftLastAccessTime, &fileInfo->atimeUs);
fileInfo->mtime = FiletimeToStatTime(attrs.ftLastWriteTime, &fileInfo->mtimeUs);
fileInfo->ctime = FiletimeToStatTime(attrs.ftCreationTime, &fileInfo->ctimeUs);
fileInfo->exists = true;
fileInfo->size = ((uint64_t)attrs.nFileSizeHigh << 32) | attrs.nFileSizeLow;
fileInfo->isDirectory = (attrs.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
fileInfo->isWritable = (attrs.dwFileAttributes & FILE_ATTRIBUTE_READONLY) == 0;
if (attrs.dwFileAttributes & FILE_ATTRIBUTE_READONLY) {
fileInfo->access = 0444; // Read
} else {
fileInfo->access = 0666; // Read/Write
}
if (attrs.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
fileInfo->access |= 0111; // Execute
}
}
#endif
bool GetFileInfo(const Path &path, FileInfo * fileInfo) {
if (LOG_IO) {
INFO_LOG(Log::IO, "GetFileInfo %s", path.ToVisualString().c_str());
}
switch (path.Type()) {
case PathType::NATIVE:
break; // OK
case PathType::CONTENT_URI:
return Android_GetFileInfo(path.ToString(), fileInfo);
default:
return false;
}
// TODO: Expand relative paths?
fileInfo->fullName = path;
#if PPSSPP_PLATFORM(WINDOWS)
WIN32_FILE_ATTRIBUTE_DATA attrs;
#if PPSSPP_PLATFORM(UWP)
if (!GetFileAttributesExFromAppW(path.ToWString().c_str(), GetFileExInfoStandard, &attrs)) {
#else
if (!GetFileAttributesExW(path.ToWString().c_str(), GetFileExInfoStandard, &attrs)) {
#endif
fileInfo->size = 0;
fileInfo->isDirectory = false;
fileInfo->exists = false;
return false;
}
ReadFileAttributes(fileInfo, attrs);
#else
#if (defined __ANDROID__) && (__ANDROID_API__ < 21)
struct stat file_info;
int result = stat(path.c_str(), &file_info);
#else
struct stat64 file_info;
int result = stat64(path.c_str(), &file_info);
#endif
if (result < 0) {
fileInfo->exists = false;
return false;
}
fileInfo->isDirectory = S_ISDIR(file_info.st_mode);
fileInfo->isWritable = false;
fileInfo->size = file_info.st_size;
fileInfo->exists = true;
fileInfo->atime = file_info.st_atime;
fileInfo->mtime = file_info.st_mtime;
fileInfo->ctime = file_info.st_ctime;
// TODO: Use statx to provide higher precision timestamps on Linux.
fileInfo->atimeUs = 0;
fileInfo->mtimeUs = 0;
fileInfo->ctimeUs = 0;
fileInfo->access = file_info.st_mode & 0x1ff;
// HACK: approximation
if (file_info.st_mode & 0200) {
fileInfo->isWritable = true;
}
#endif
return true;
}
bool GetModifTimeT(const Path &filename, time_t *return_time) {
FileInfo info;
if (GetFileInfo(filename, &info)) {
*return_time = info.mtime;
return true;
} else {
*return_time = 0;
return false;
}
}
bool GetModifTime(const Path &filename, tm & return_time) {
memset(&return_time, 0, sizeof(return_time));
FileInfo info;
if (GetFileInfo(filename, &info)) {
time_t t = info.mtime;
localtime_r((time_t*)&t, &return_time);
return true;
} else {
return false;
}
}
bool FileInfo::operator <(const FileInfo & other) const {
if (isDirectory && !other.isDirectory)
return true;
else if (!isDirectory && other.isDirectory)
return false;
if (strcasecmp(name.c_str(), other.name.c_str()) < 0)
return true;
else
return false;
}
std::vector<File::FileInfo> ApplyFilter(std::vector<File::FileInfo> files, const char *extensionFilter, std::string_view prefix) {
std::set<std::string> filters;
if (extensionFilter) {
std::string tmp;
while (*extensionFilter) {
if (*extensionFilter == ':') {
filters.emplace("." + tmp);
tmp.clear();
} else {
tmp.push_back(*extensionFilter);
}
extensionFilter++;
}
if (!tmp.empty())
filters.emplace("." + tmp);
}
auto pred = [&](const File::FileInfo &info) {
// WARNING: Keep in mind that if we return true here, the files is REMOVED from the list.
// It's not retain_if.
if (!startsWith(info.name, prefix)) {
return true;
}
if (info.isDirectory || !extensionFilter)
return false;
std::string ext = info.fullName.GetFileExtension();
return filters.find(ext) == filters.end();
};
files.erase(std::remove_if(files.begin(), files.end(), pred), files.end());
return files;
}
bool GetFilesInDir(const Path &directory, std::vector<FileInfo> *files, const char *filter, int flags, std::string_view prefix) {
if (LOG_IO) {
INFO_LOG(Log::IO, "GetFilesInDir '%s' (ext %s, prefix %.*s)", directory.ToVisualString().c_str(), filter, (int)prefix.size(), prefix.data());
}
if (directory.Type() == PathType::CONTENT_URI) {
bool exists = false;
// TODO: Move prefix filtering over to the Java side for more speed.
std::vector<File::FileInfo> fileList = Android_ListContentUri(directory.ToString(), std::string(prefix), &exists);
int beforeFilter = (int)fileList.size();
*files = ApplyFilter(fileList, filter, prefix);
std::sort(files->begin(), files->end());
DEBUG_LOG(Log::IO, "GetFilesInDir: Found %d entries (%d before filter). Path: %s", (int)files->size(), beforeFilter, directory.ToVisualString().c_str());
return exists;
}
std::set<std::string> filters;
if (filter) {
std::string tmp;
while (*filter) {
if (*filter == ':') {
filters.insert(tmp);
tmp.clear();
} else {
tmp.push_back(*filter);
}
filter++;
}
if (!tmp.empty())
filters.insert(tmp);
}
#if PPSSPP_PLATFORM(WINDOWS)
if (directory.IsRoot()) {
// Special path that means root of file system.
// This does not respect prefix filtering.
std::vector<std::string> drives = File::GetWindowsDrives();
for (auto drive = drives.begin(); drive != drives.end(); ++drive) {
if (*drive == "A:/" || *drive == "B:/")
continue;
File::FileInfo fake;
fake.fullName = Path(*drive);
fake.name = *drive;
fake.isDirectory = true;
fake.exists = true;
fake.size = 0;
fake.isWritable = false;
files->push_back(fake);
}
return files->size();
}
// Find the first file in the directory.
WIN32_FIND_DATA ffd;
std::wstring wpath = directory.ToWString();
wpath += L"\\*";
#if PPSSPP_PLATFORM(UWP)
HANDLE hFind = FindFirstFileExFromAppW(wpath.c_str(), FindExInfoStandard, &ffd, FindExSearchNameMatch, NULL, 0);
#else
HANDLE hFind = FindFirstFileEx(wpath.c_str(), FindExInfoStandard, &ffd, FindExSearchNameMatch, NULL, 0);
#endif
if (hFind == INVALID_HANDLE_VALUE) {
return false;
}
do {
const std::string virtualName = ConvertWStringToUTF8(ffd.cFileName);
// check for "." and ".."
if (!(flags & GETFILES_GET_NAVIGATION_ENTRIES) && (virtualName == "." || virtualName == ".."))
continue;
// Remove dotfiles (optional with flag.)
if (!(flags & GETFILES_GETHIDDEN)) {
if ((ffd.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN) != 0)
continue;
}
if (!startsWith(virtualName, prefix)) {
continue;
}
if (LOG_IO) {
// INFO_LOG(Log::IO, "GetFilesInDir item %s", virtualName.c_str());
}
FileInfo info;
info.name = virtualName;
info.fullName = directory / virtualName;
ReadFileAttributes(&info, ffd);
if (!info.isDirectory) {
std::string ext = info.fullName.GetFileExtension();
if (!ext.empty()) {
ext = ext.substr(1); // Remove the dot.
if (filter && filters.find(ext) == filters.end()) {
continue;
}
}
}
files->push_back(info);
} while (FindNextFile(hFind, &ffd) != 0);
FindClose(hFind);
#else
struct dirent *result = NULL;
DIR *dirp = opendir(directory.c_str());
if (!dirp)
return false;
while ((result = readdir(dirp))) {
const std::string virtualName(result->d_name);
// check for "." and ".."
if (!(flags & GETFILES_GET_NAVIGATION_ENTRIES) && (virtualName == "." || virtualName == ".."))
continue;
// Remove dotfiles (optional with flag.)
if (!(flags & GETFILES_GETHIDDEN)) {
if (virtualName[0] == '.')
continue;
}
if (!startsWith(virtualName, prefix)) {
continue;
}
// Let's just reuse GetFileInfo. We're calling stat anyway to get isDirectory information.
Path fullName = directory / virtualName;
FileInfo info;
info.name = virtualName;
if (!GetFileInfo(fullName, &info)) {
continue;
}
if (!info.isDirectory) {
std::string ext = info.fullName.GetFileExtension();
if (!ext.empty()) {
ext = ext.substr(1); // Remove the dot.
if (filter && filters.find(ext) == filters.end()) {
continue;
}
}
}
files->push_back(info);
}
closedir(dirp);
#endif
std::sort(files->begin(), files->end());
if (LOG_IO) {
INFO_LOG(Log::IO, "GetFilesInDir: Found %d files", (int)files->size());
}
return true;
}
#if PPSSPP_PLATFORM(WINDOWS)
// Returns a vector with the device names
std::vector<std::string> GetWindowsDrives() {
std::vector<std::string> drives;
#if PPSSPP_PLATFORM(UWP)
DWORD logicaldrives = GetLogicalDrives();
for (int i = 0; i < 26; i++)
{
if (logicaldrives & (1 << i))
{
CHAR driveName[] = { (CHAR)(TEXT('A') + i), TEXT(':'), TEXT('\\'), TEXT('\0') };
std::string str(driveName);
drives.push_back(driveName);
}
}
return drives;
#else
const DWORD buffsize = GetLogicalDriveStrings(0, NULL);
std::vector<wchar_t> buff(buffsize);
if (GetLogicalDriveStrings(buffsize, buff.data()) == buffsize - 1) {
auto drive = buff.data();
while (*drive) {
std::string str(ConvertWStringToUTF8(drive));
str.pop_back(); // we don't want the final backslash
str += "/";
drives.push_back(str);
// advance to next drive
while (*drive++) {}
}
}
return drives;
#endif // PPSSPP_PLATFORM(UWP)
}
#endif // PPSSPP_PLATFORM(WINDOWS)
} // namespace File