ppsspp/Common/File/Path.cpp
Henrik Rydgård ef5abac1d3 Fix an oversight when saving replacement textures on scoped storage
We ended up not properly mangling the filename when changing extensions
if the file URI is an Android content URI.

Fixes #17320
2023-04-22 00:05:07 +02:00

510 lines
12 KiB
C++

#include "ppsspp_config.h"
#include <algorithm>
#include <cctype>
#include <cstring>
#include "Common/File/Path.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"
#include "android/jni/AndroidContentURI.h"
#if HOST_IS_CASE_SENSITIVE
#include <dirent.h>
#include <unistd.h>
#include <sys/stat.h>
#endif
Path::Path(const std::string &str) {
Init(str);
}
#if PPSSPP_PLATFORM(WINDOWS)
Path::Path(const std::wstring &str) {
type_ = PathType::NATIVE;
Init(ConvertWStringToUTF8(str));
}
#endif
void Path::Init(const std::string &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(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 /(const std::string &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 /=(const std::string &subdir) {
*this = *this / subdir;
}
Path Path::WithExtraExtension(const std::string &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_ + ext);
}
Path Path::WithReplacedExtension(const std::string &oldExtension, const std::string &newExtension) const {
if (type_ == PathType::CONTENT_URI) {
AndroidContentURI uri(path_);
return Path(uri.WithReplacedExtension(oldExtension, newExtension).ToString());
}
_dbg_assert_(!oldExtension.empty() && oldExtension[0] == '.');
_dbg_assert_(!newExtension.empty() && newExtension[0] == '.');
if (endsWithNoCase(path_, oldExtension)) {
std::string newPath = path_.substr(0, path_.size() - oldExtension.size());
return Path(newPath + newExtension);
} else {
return Path(*this);
}
}
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(const std::string &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 GetExtFromString(uri.FilePath());
}
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(const std::string &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 (type_ != other.type_) {
// Bad
return false;
}
return startsWith(path_, other.path_);
}
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;
}
#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) {
// 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_, "/", "\\");
}
#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();
}
if (path_ == "/" || path_.empty()) {
return false;
}
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;
}
}
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;
}
}
#if HOST_IS_CASE_SENSITIVE
static bool FixFilenameCase(const std::string &path, std::string &filename) {
// 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;
}
bool FixPathCase(const Path &realBasePath, std::string &path, FixPathCaseBehavior behavior) {
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