ppsspp/Common/File/AndroidContentURI.cpp
Henrik Rydgård 081bdb323a Fix running some file formats from the Downloads folder
Due to how we mount stuff, we need to be able to navigate one step up
from the executable, and then re-attach the executable filename. To
allow this, in content URIs, treat ':' as a directory separator for
navigation purposes.

End result, you can now download cube.elf from the website and run it directly
from Downloads without using a file manager to move it.
2023-05-16 16:44:28 +02:00

206 lines
5.5 KiB
C++

#include "Common/File/AndroidContentURI.h"
bool AndroidContentURI::Parse(const std::string &path) {
const char *prefix = "content://";
if (!startsWith(path, prefix)) {
return false;
}
std::string components = path.substr(strlen(prefix));
std::vector<std::string> parts;
SplitString(components, '/', parts);
if (parts.size() == 3) {
// Single file URI.
provider = parts[0];
if (parts[1] == "tree") {
// Single directory URI.
// Not sure when we encounter these?
// file empty signals this type.
root = UriDecode(parts[2]);
return true;
} else if (parts[1] == "document") {
// root empty signals this type.
file = UriDecode(parts[2]);
return true;
} else {
// What's this?
return false;
}
} else if (parts.size() == 5) {
// Tree URI
provider = parts[0];
if (parts[1] != "tree") {
return false;
}
root = UriDecode(parts[2]);
if (parts[3] != "document") {
return false;
}
file = UriDecode(parts[4]);
// Sanity check file path.
return startsWith(file, root);
} else {
// Invalid Content URI
return false;
}
}
AndroidContentURI AndroidContentURI::WithRootFilePath(const std::string &filePath) {
if (root.empty()) {
ERROR_LOG(SYSTEM, "WithRootFilePath cannot be used with single file URIs.");
return *this;
}
AndroidContentURI uri = *this;
uri.file = uri.root;
if (!filePath.empty()) {
uri.file += "/" + filePath;
}
return uri;
}
AndroidContentURI AndroidContentURI::WithComponent(const std::string &filePath) {
AndroidContentURI uri = *this;
if (uri.file.empty()) {
// Not sure what to do.
return uri;
}
if (uri.file.back() == ':') {
// Special case handling for Document URIs: Treat the ':' as a directory separator too (but preserved in the filename).
uri.file = uri.file + filePath;
} else {
uri.file = uri.file + "/" + filePath;
}
return uri;
}
AndroidContentURI AndroidContentURI::WithExtraExtension(const std::string &extension) {
AndroidContentURI uri = *this;
uri.file = uri.file + extension;
return uri;
}
AndroidContentURI AndroidContentURI::WithReplacedExtension(const std::string &oldExtension, const std::string &newExtension) const {
_dbg_assert_(!oldExtension.empty() && oldExtension[0] == '.');
_dbg_assert_(!newExtension.empty() && newExtension[0] == '.');
AndroidContentURI uri = *this;
if (endsWithNoCase(file, oldExtension)) {
uri.file = file.substr(0, file.size() - oldExtension.size()) + newExtension;
}
return uri;
}
AndroidContentURI AndroidContentURI::WithReplacedExtension(const std::string &newExtension) const {
_dbg_assert_(!newExtension.empty() && newExtension[0] == '.');
AndroidContentURI uri = *this;
if (file.empty()) {
return uri;
}
std::string extension = GetFileExtension();
uri.file = file.substr(0, file.size() - extension.size()) + newExtension;
return uri;
}
bool AndroidContentURI::CanNavigateUp() const {
if (IsTreeURI()) {
return file.size() > root.size();
} else {
return file.find(':') != std::string::npos && file.back() != ':';
}
}
// Only goes downwards in hierarchies. No ".." will ever be generated.
bool AndroidContentURI::ComputePathTo(const AndroidContentURI &other, std::string &path) const {
size_t offset = FilePath().size() + 1;
std::string otherFilePath = other.FilePath();
if (offset >= otherFilePath.size()) {
ERROR_LOG(SYSTEM, "Bad call to PathTo. '%s' -> '%s'", FilePath().c_str(), other.FilePath().c_str());
return false;
}
path = other.FilePath().substr(FilePath().size() + 1);
return true;
}
std::string AndroidContentURI::GetFileExtension() const {
size_t pos = file.rfind(".");
if (pos == std::string::npos) {
return "";
}
size_t slash_pos = file.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 = file.substr(pos);
for (size_t i = 0; i < ext.size(); i++) {
ext[i] = tolower(ext[i]);
}
return ext;
}
std::string AndroidContentURI::GetLastPart() const {
if (file.empty()) {
// Can't do anything anyway.
return std::string();
}
if (!CanNavigateUp()) {
size_t colon = file.rfind(':');
if (file.back() == ':') {
return file;
}
if (colon == std::string::npos) {
return std::string();
}
return file.substr(colon + 1);
}
size_t slash = file.rfind('/');
if (slash == std::string::npos) {
// ok, look for the final colon. If it's the last char, we would have been caught above in !CanNavigateUp.
size_t colon = file.rfind(':');
if (colon == std::string::npos) {
return std::string();
}
return file.substr(colon + 1);
}
std::string part = file.substr(slash + 1);
return part;
}
bool AndroidContentURI::NavigateUp() {
if (!CanNavigateUp()) {
return false;
}
size_t slash = file.rfind('/');
if (slash == std::string::npos) {
// ok, look for the final colon.
size_t colon = file.rfind(':');
if (colon == std::string::npos) {
return false;
}
file = file.substr(0, colon + 1); // Note: we include the colon in these paths.
return true;
}
file = file.substr(0, slash);
return true;
}
std::string AndroidContentURI::ToString() const {
if (file.empty()) {
// Tree URI
return StringFromFormat("content://%s/tree/%s", provider.c_str(), UriEncode(root).c_str());
} else if (root.empty()) {
// Single file URI
return StringFromFormat("content://%s/document/%s", provider.c_str(), UriEncode(file).c_str());
} else {
// File URI from Tree
return StringFromFormat("content://%s/tree/%s/document/%s", provider.c_str(), UriEncode(root).c_str(), UriEncode(file).c_str());
}
}