mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-09-21 20:06:25 +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>
210 lines
5.5 KiB
C++
210 lines
5.5 KiB
C++
#include "Common/File/AndroidContentURI.h"
|
|
|
|
bool AndroidContentURI::Parse(std::string_view path) {
|
|
const char *prefix = "content://";
|
|
if (!startsWith(path, prefix)) {
|
|
return false;
|
|
}
|
|
|
|
std::string_view components = path.substr(strlen(prefix));
|
|
|
|
std::vector<std::string_view> 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(Log::IO, "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(std::string_view 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.append(filePath);
|
|
} else {
|
|
uri.file.push_back('/');
|
|
uri.file.append(filePath);
|
|
}
|
|
return uri;
|
|
}
|
|
|
|
AndroidContentURI AndroidContentURI::WithExtraExtension(std::string_view extension) {
|
|
AndroidContentURI uri = *this;
|
|
uri.file.append(extension);
|
|
return uri;
|
|
}
|
|
|
|
bool AndroidContentURI::WithReplacedExtension(const std::string &oldExtension, const std::string &newExtension, AndroidContentURI *out) const {
|
|
_dbg_assert_(!oldExtension.empty() && oldExtension[0] == '.');
|
|
_dbg_assert_(!newExtension.empty() && newExtension[0] == '.');
|
|
if (!endsWithNoCase(file, oldExtension)) {
|
|
return false;
|
|
}
|
|
AndroidContentURI uri = *this;
|
|
uri.file = file.substr(0, file.size() - oldExtension.size()) + newExtension;
|
|
*out = uri;
|
|
return true;
|
|
}
|
|
|
|
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;
|
|
const auto &otherFilePath = other.FilePath();
|
|
if (offset >= otherFilePath.size()) {
|
|
ERROR_LOG(Log::IO, "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 (colon == std::string::npos) {
|
|
return std::string();
|
|
}
|
|
if (file.back() == ':') {
|
|
return file;
|
|
}
|
|
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());
|
|
}
|
|
}
|