mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-09-22 04:16:22 +02:00
It used to return the path unchanged when the path didn't end in oldExtension, so a caller that guessed wrong silently went on using the original file - and "the screenshot next to this savestate" quietly becomes "this savestate". Every caller had to know to check the extension first, and most didn't. Now it's [[nodiscard]] bool with an out-param, in the style of ComputePathTo next door, so the mismatch has to be handled. Changing the signature rather than the behaviour means no call site can keep the old assumption by accident. All four callers wanted "skip it" or "fall back", which they now say out loud. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
560 lines
13 KiB
C++
560 lines
13 KiB
C++
#include "ppsspp_config.h"
|
|
|
|
#include <algorithm> // for std::search
|
|
#include <cctype>
|
|
#include <cstring>
|
|
|
|
#include "Common/File/Path.h"
|
|
#include "Common/File/AndroidContentURI.h"
|
|
#include "Common/File/FileUtil.h"
|
|
#include "Common/StringUtils.h"
|
|
#include "Common/Log.h"
|
|
#include "Common/Data/Encoding/Utf8.h"
|
|
|
|
#include "android/jni/app-android.h"
|
|
|
|
#if PPSSPP_PLATFORM(UWP) && !defined(__LIBRETRO__)
|
|
#include "UWP/UWPHelpers/StorageManager.h"
|
|
#endif
|
|
|
|
#if HOST_IS_CASE_SENSITIVE
|
|
#include <dirent.h>
|
|
#include <unistd.h>
|
|
#include <sys/stat.h>
|
|
#endif
|
|
|
|
Path::Path(std::string_view str) {
|
|
Init(str);
|
|
}
|
|
|
|
#if PPSSPP_PLATFORM(WINDOWS)
|
|
Path::Path(const std::wstring &str) {
|
|
type_ = PathType::NATIVE;
|
|
Init(ConvertWStringToUTF8(str));
|
|
}
|
|
#endif
|
|
|
|
void Path::Init(std::string_view str) {
|
|
if (str.empty()) {
|
|
type_ = PathType::UNDEFINED;
|
|
path_.clear();
|
|
} else if (startsWith(str, "http://") || startsWith(str, "https://")) {
|
|
type_ = PathType::HTTP;
|
|
path_ = str;
|
|
} else if (Android_IsContentUri(str)) {
|
|
// Content URIs on non scoped-storage (and possibly other cases?) can contain
|
|
// "raw:/" URIs inside. This happens when picking the Download folder using the folder browser
|
|
// on Android 9.
|
|
// Here's an example:
|
|
// content://com.android.providers.downloads.documents/tree/raw%3A%2Fstorage%2Femulated%2F0%2FDownload%2Fp/document/raw%3A%2Fstorage%2Femulated%2F0%2FDownload%2Fp
|
|
//
|
|
// Since this is a legacy use case, I think it's safe enough to just detect this
|
|
// and flip it to a NATIVE url and hope for the best.
|
|
AndroidContentURI uri(str);
|
|
if (startsWith(uri.FilePath(), "raw:/")) {
|
|
INFO_LOG(Log::System, "Raw path detected: %s", uri.FilePath().c_str());
|
|
path_ = uri.FilePath().substr(4);
|
|
type_ = PathType::NATIVE;
|
|
} else {
|
|
// A normal content URI path.
|
|
type_ = PathType::CONTENT_URI;
|
|
path_ = str;
|
|
}
|
|
} else {
|
|
type_ = PathType::NATIVE;
|
|
path_ = str;
|
|
}
|
|
|
|
#if PPSSPP_PLATFORM(WINDOWS)
|
|
// Flip all the slashes around. We flip them back on ToWString().
|
|
for (size_t i = 0; i < path_.size(); i++) {
|
|
if (path_[i] == '\\') {
|
|
path_[i] = '/';
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// Don't pop_back if it's just "/".
|
|
if (type_ == PathType::NATIVE && path_.size() > 1 && path_.back() == '/') {
|
|
path_.pop_back();
|
|
}
|
|
}
|
|
|
|
// We always use forward slashes internally, we convert to backslash only when
|
|
// converted to a wstring.
|
|
Path Path::operator /(std::string_view subdir) const {
|
|
if (type_ == PathType::CONTENT_URI) {
|
|
AndroidContentURI uri(path_);
|
|
return Path(uri.WithComponent(subdir).ToString());
|
|
}
|
|
|
|
// Direct string manipulation.
|
|
|
|
if (subdir.empty()) {
|
|
return Path(path_);
|
|
}
|
|
std::string fullPath = path_;
|
|
if (subdir.front() != '/' && (fullPath.empty() || fullPath.back() != '/')) {
|
|
fullPath += "/";
|
|
}
|
|
fullPath += subdir;
|
|
// Prevent adding extra slashes.
|
|
if (fullPath.back() == '/') {
|
|
fullPath.pop_back();
|
|
}
|
|
return Path(fullPath);
|
|
}
|
|
|
|
void Path::operator /=(std::string_view subdir) {
|
|
*this = *this / subdir;
|
|
}
|
|
|
|
Path Path::WithExtraExtension(std::string_view ext) const {
|
|
if (type_ == PathType::CONTENT_URI) {
|
|
AndroidContentURI uri(path_);
|
|
return Path(uri.WithExtraExtension(ext).ToString());
|
|
}
|
|
|
|
_dbg_assert_(!ext.empty() && ext[0] == '.');
|
|
return Path(path_ + std::string(ext));
|
|
}
|
|
|
|
bool Path::WithReplacedExtension(const std::string &oldExtension, const std::string &newExtension, Path *out) const {
|
|
_dbg_assert_(!oldExtension.empty() && oldExtension[0] == '.');
|
|
_dbg_assert_(!newExtension.empty() && newExtension[0] == '.');
|
|
|
|
if (type_ == PathType::CONTENT_URI) {
|
|
AndroidContentURI uri(path_);
|
|
AndroidContentURI replaced;
|
|
if (!uri.WithReplacedExtension(oldExtension, newExtension, &replaced)) {
|
|
return false;
|
|
}
|
|
*out = Path(replaced.ToString());
|
|
return true;
|
|
}
|
|
|
|
if (!endsWithNoCase(path_, oldExtension)) {
|
|
return false;
|
|
}
|
|
*out = Path(path_.substr(0, path_.size() - oldExtension.size()) + newExtension);
|
|
return true;
|
|
}
|
|
|
|
Path Path::WithReplacedExtension(const std::string &newExtension) const {
|
|
if (type_ == PathType::CONTENT_URI) {
|
|
AndroidContentURI uri(path_);
|
|
return Path(uri.WithReplacedExtension(newExtension).ToString());
|
|
}
|
|
|
|
_dbg_assert_(!newExtension.empty() && newExtension[0] == '.');
|
|
if (path_.empty()) {
|
|
return Path(*this);
|
|
}
|
|
std::string extension = GetFileExtension();
|
|
std::string newPath = path_.substr(0, path_.size() - extension.size()) + newExtension;
|
|
return Path(newPath);
|
|
}
|
|
|
|
std::string Path::GetFilename() const {
|
|
if (type_ == PathType::CONTENT_URI) {
|
|
AndroidContentURI uri(path_);
|
|
return uri.GetLastPart();
|
|
}
|
|
size_t pos = path_.rfind('/');
|
|
if (pos != std::string::npos) {
|
|
return path_.substr(pos + 1);
|
|
}
|
|
return path_;
|
|
}
|
|
|
|
static std::string GetExtFromString(std::string_view str) {
|
|
size_t pos = str.rfind(".");
|
|
if (pos == std::string::npos) {
|
|
return "";
|
|
}
|
|
size_t slash_pos = str.rfind("/");
|
|
if (slash_pos != std::string::npos && slash_pos > pos) {
|
|
// Don't want to detect "df/file" from "/as.df/file"
|
|
return "";
|
|
}
|
|
std::string ext(str.substr(pos));
|
|
for (size_t i = 0; i < ext.size(); i++) {
|
|
ext[i] = tolower(ext[i]);
|
|
}
|
|
return ext;
|
|
}
|
|
|
|
std::string Path::GetFileExtension() const {
|
|
if (type_ == PathType::CONTENT_URI) {
|
|
AndroidContentURI uri(path_);
|
|
return uri.GetFileExtension();
|
|
}
|
|
return GetExtFromString(path_);
|
|
}
|
|
|
|
std::string Path::GetDirectory() const {
|
|
if (type_ == PathType::CONTENT_URI) {
|
|
// Unclear how meaningful this is.
|
|
AndroidContentURI uri(path_);
|
|
uri.NavigateUp();
|
|
return uri.ToString();
|
|
}
|
|
|
|
size_t pos = path_.rfind('/');
|
|
if (type_ == PathType::HTTP) {
|
|
// Things are a bit different for HTTP, because we probably ended with /.
|
|
if (pos + 1 == path_.size()) {
|
|
pos = path_.rfind('/', pos - 1);
|
|
if (pos != path_.npos && pos > 8) {
|
|
return path_.substr(0, pos + 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (pos != std::string::npos) {
|
|
if (pos == 0) {
|
|
return "/"; // We're at the root.
|
|
}
|
|
return path_.substr(0, pos);
|
|
#if PPSSPP_PLATFORM(WINDOWS)
|
|
} else if (path_.size() == 2 && path_[1] == ':') {
|
|
// Windows fake-root.
|
|
return "/";
|
|
#endif
|
|
} else {
|
|
// There could be a ':', too. Unlike the slash, let's include that
|
|
// in the returned directory.
|
|
size_t c_pos = path_.rfind(':');
|
|
if (c_pos != std::string::npos) {
|
|
return path_.substr(0, c_pos + 1);
|
|
}
|
|
}
|
|
// No directory components, we're a relative path.
|
|
return path_;
|
|
}
|
|
|
|
bool Path::FilePathContainsNoCase(std::string_view needle) const {
|
|
std::string haystack;
|
|
if (type_ == PathType::CONTENT_URI) {
|
|
haystack = AndroidContentURI(path_).FilePath();
|
|
} else {
|
|
haystack = path_;
|
|
}
|
|
auto pred = [](char ch1, char ch2) { return std::toupper(ch1) == std::toupper(ch2); };
|
|
auto found = std::search(haystack.begin(), haystack.end(), needle.begin(), needle.end(), pred);
|
|
return found != haystack.end();
|
|
}
|
|
|
|
bool Path::StartsWith(const Path &other) const {
|
|
if (other.empty()) {
|
|
return true;
|
|
}
|
|
if (type_ != other.type_) {
|
|
// Bad
|
|
return false;
|
|
}
|
|
return startsWith(path_, other.path_);
|
|
}
|
|
|
|
bool Path::StartsWithGlobalAndNotEqual(const Path &other) const {
|
|
if (other.empty()) {
|
|
return true;
|
|
}
|
|
if (type_ != other.type_) {
|
|
// Bad
|
|
return false;
|
|
}
|
|
if (type_ == PathType::CONTENT_URI) {
|
|
AndroidContentURI a(path_);
|
|
AndroidContentURI b(other.path_);
|
|
std::string aLast = a.GetLastPart();
|
|
std::string bLast = b.GetLastPart();
|
|
if (aLast == bLast) {
|
|
return false;
|
|
}
|
|
return CountChar(aLast, '/') != CountChar(bLast, '/') && startsWith(aLast, bLast);
|
|
}
|
|
return *this != other && StartsWith(other);
|
|
}
|
|
|
|
const std::string &Path::ToString() const {
|
|
return path_;
|
|
}
|
|
|
|
#if PPSSPP_PLATFORM(WINDOWS)
|
|
std::wstring Path::ToWString() const {
|
|
std::wstring w = ConvertUTF8ToWString(path_);
|
|
for (size_t i = 0; i < w.size(); i++) {
|
|
if (w[i] == '/') {
|
|
w[i] = '\\';
|
|
}
|
|
}
|
|
return w;
|
|
}
|
|
std::string Path::ToCString() const {
|
|
std::string w = path_;
|
|
for (size_t i = 0; i < w.size(); i++) {
|
|
if (w[i] == '/') {
|
|
w[i] = '\\';
|
|
}
|
|
}
|
|
return w;
|
|
}
|
|
#endif
|
|
|
|
std::string Path::ToVisualString(const char *relativeRoot) const {
|
|
if (type_ == PathType::CONTENT_URI) {
|
|
return AndroidContentURI(path_).ToVisualString();
|
|
#if PPSSPP_PLATFORM(WINDOWS)
|
|
} else if (type_ == PathType::NATIVE) {
|
|
#if PPSSPP_PLATFORM(UWP) && !defined(__LIBRETRO__)
|
|
return GetPreviewPath(path_);
|
|
#else
|
|
// It can be useful to show the path as relative to the memstick
|
|
if (relativeRoot) {
|
|
std::string root = ReplaceAll(relativeRoot, "/", "\\");
|
|
std::string path = ReplaceAll(path_, "/", "\\");
|
|
if (startsWithNoCase(path, root)) {
|
|
return path.substr(root.size());
|
|
} else {
|
|
return path;
|
|
}
|
|
} else {
|
|
return ReplaceAll(path_, "/", "\\");
|
|
}
|
|
#endif
|
|
#else
|
|
if (relativeRoot) {
|
|
std::string root = relativeRoot;
|
|
if (startsWithNoCase(path_, root)) {
|
|
return path_.substr(root.size());
|
|
} else {
|
|
return path_;
|
|
}
|
|
} else {
|
|
return path_;
|
|
}
|
|
#endif
|
|
} else {
|
|
return path_;
|
|
}
|
|
}
|
|
|
|
bool Path::CanNavigateUp() const {
|
|
if (type_ == PathType::CONTENT_URI) {
|
|
return AndroidContentURI(path_).CanNavigateUp();
|
|
} else if (type_ == PathType::HTTP) {
|
|
size_t rootSlash = path_.find_first_of('/', strlen("https://"));
|
|
if (rootSlash == path_.npos || path_.size() == rootSlash + 1) {
|
|
// This means, "http://server" or "http://server/". Can't go up.
|
|
return false;
|
|
}
|
|
}
|
|
if (path_ == "/" || path_.empty()) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
Path Path::NavigateUp() const {
|
|
if (type_ == PathType::CONTENT_URI) {
|
|
AndroidContentURI uri(path_);
|
|
uri.NavigateUp();
|
|
return Path(uri.ToString());
|
|
}
|
|
std::string dir = GetDirectory();
|
|
return Path(dir);
|
|
}
|
|
|
|
Path Path::GetRootVolume() const {
|
|
if (!IsAbsolute()) {
|
|
// Can't do anything
|
|
return Path(path_);
|
|
}
|
|
|
|
if (type_ == PathType::CONTENT_URI) {
|
|
AndroidContentURI uri(path_);
|
|
AndroidContentURI rootPath = uri.WithRootFilePath("");
|
|
return Path(rootPath.ToString());
|
|
}
|
|
|
|
#if PPSSPP_PLATFORM(WINDOWS)
|
|
if (path_[1] == ':') {
|
|
// Windows path with drive letter
|
|
std::string path = path_.substr(0, 2);
|
|
return Path(path);
|
|
}
|
|
// Support UNC and device paths.
|
|
if (path_[0] == '/' && path_[1] == '/') {
|
|
size_t next = 2;
|
|
if ((path_[2] == '.' || path_[2] == '?') && path_[3] == '/') {
|
|
// Device path, or "\\.\UNC" path, skip the dot and consider the device the root.
|
|
next = 4;
|
|
}
|
|
|
|
size_t len = path_.find_first_of('/', next);
|
|
return Path(path_.substr(0, len));
|
|
}
|
|
#endif
|
|
return Path("/");
|
|
}
|
|
|
|
bool Path::IsAbsolute() const {
|
|
if (type_ == PathType::CONTENT_URI) {
|
|
// These don't exist in relative form.
|
|
return true;
|
|
}
|
|
|
|
if (path_.empty())
|
|
return true;
|
|
else if (path_.front() == '/')
|
|
return true;
|
|
#if PPSSPP_PLATFORM(WINDOWS)
|
|
else if (path_.size() > 3 && path_[1] == ':')
|
|
return true; // Windows path with drive letter
|
|
#endif
|
|
else
|
|
return false;
|
|
}
|
|
|
|
bool Path::ComputePathTo(const Path &other, std::string &path) const {
|
|
if (other == *this) {
|
|
path.clear();
|
|
return true;
|
|
}
|
|
|
|
if (!other.StartsWith(*this)) {
|
|
// Can't do this. Should return an error.
|
|
return false;
|
|
}
|
|
|
|
if (*this == other) {
|
|
// Equal, the path is empty.
|
|
path.clear();
|
|
return true;
|
|
}
|
|
|
|
if (type_ == PathType::CONTENT_URI) {
|
|
AndroidContentURI a(path_);
|
|
AndroidContentURI b(other.path_);
|
|
if (a.RootPath() != b.RootPath()) {
|
|
// No common root, can't do anything
|
|
return false;
|
|
}
|
|
return a.ComputePathTo(b, path);
|
|
} else if (path_ == "/") {
|
|
path = other.path_.substr(1);
|
|
return true;
|
|
} else {
|
|
path = other.path_.substr(path_.size() + 1);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
static bool FixFilenameCase(const std::string &path, std::string &filename) {
|
|
#if _WIN32
|
|
// We don't support case-insensitive file systems on Windows.
|
|
return true;
|
|
#else
|
|
|
|
// Are we lucky?
|
|
if (File::Exists(Path(path + filename)))
|
|
return true;
|
|
|
|
size_t filenameSize = filename.size(); // size in bytes, not characters
|
|
for (size_t i = 0; i < filenameSize; i++)
|
|
{
|
|
filename[i] = tolower(filename[i]);
|
|
}
|
|
|
|
//TODO: lookup filename in cache for "path"
|
|
struct dirent *result = NULL;
|
|
|
|
DIR *dirp = opendir(path.c_str());
|
|
if (!dirp)
|
|
return false;
|
|
|
|
bool retValue = false;
|
|
|
|
while ((result = readdir(dirp)))
|
|
{
|
|
if (strlen(result->d_name) != filenameSize)
|
|
continue;
|
|
|
|
size_t i;
|
|
for (i = 0; i < filenameSize; i++)
|
|
{
|
|
if (filename[i] != tolower(result->d_name[i]))
|
|
break;
|
|
}
|
|
|
|
if (i < filenameSize)
|
|
continue;
|
|
|
|
filename = result->d_name;
|
|
retValue = true;
|
|
}
|
|
|
|
closedir(dirp);
|
|
|
|
return retValue;
|
|
#endif
|
|
}
|
|
|
|
bool FixPathCase(const Path &realBasePath, std::string &path, FixPathCaseBehavior behavior) {
|
|
#if _WIN32
|
|
return true;
|
|
#else
|
|
|
|
if (realBasePath.Type() == PathType::CONTENT_URI) {
|
|
// Nothing to do. These are already case insensitive, I think.
|
|
return true;
|
|
}
|
|
|
|
std::string basePath = realBasePath.ToString();
|
|
|
|
size_t len = path.size();
|
|
|
|
if (len == 0)
|
|
return true;
|
|
|
|
if (path[len - 1] == '/') {
|
|
len--;
|
|
|
|
if (len == 0)
|
|
return true;
|
|
}
|
|
|
|
std::string fullPath;
|
|
fullPath.reserve(basePath.size() + len + 1);
|
|
fullPath.append(basePath);
|
|
|
|
size_t start = 0;
|
|
while (start < len) {
|
|
size_t i = path.find('/', start);
|
|
if (i == std::string::npos)
|
|
i = len;
|
|
|
|
if (i > start) {
|
|
std::string component = path.substr(start, i - start);
|
|
|
|
// Fix case and stop on nonexistant path component
|
|
if (FixFilenameCase(fullPath, component) == false) {
|
|
// Still counts as success if partial matches allowed or if this
|
|
// is the last component and only the ones before it are required
|
|
return (behavior == FPC_PARTIAL_ALLOWED || (behavior == FPC_PATH_MUST_EXIST && i >= len));
|
|
}
|
|
|
|
path.replace(start, i - start, component);
|
|
|
|
fullPath.append(1, '/');
|
|
fullPath.append(component);
|
|
}
|
|
|
|
start = i + 1;
|
|
}
|
|
|
|
return true;
|
|
#endif
|
|
}
|