From 515cbc7b2919e6147c2d2ae4f2701c24a4dfdfec Mon Sep 17 00:00:00 2001 From: Stenzek Date: Wed, 13 Mar 2024 19:51:44 +1000 Subject: [PATCH] Path: Add CreateFileURL() --- common/FileSystem.cpp | 109 ++++++++++++++++++++++++++++++ common/HTTPDownloader.cpp | 63 ----------------- common/HTTPDownloader.h | 2 - common/Path.h | 9 +++ pcsx2-qt/QtHost.cpp | 2 +- pcsx2/GameList.cpp | 8 +-- tests/ctest/common/path_tests.cpp | 10 +++ 7 files changed, 133 insertions(+), 70 deletions(-) diff --git a/common/FileSystem.cpp b/common/FileSystem.cpp index 15cd10d685..0960d42007 100644 --- a/common/FileSystem.cpp +++ b/common/FileSystem.cpp @@ -759,6 +759,115 @@ std::string Path::Combine(const std::string_view& base, const std::string_view& return ret; } + +std::string Path::URLEncode(std::string_view str) +{ + std::string ret; + ret.reserve(str.length() + ((str.length() + 3) / 4) * 3); + + for (size_t i = 0, l = str.size(); i < l; i++) + { + const char c = str[i]; + if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '-' || c == '_' || + c == '.' || c == '!' || c == '~' || c == '*' || c == '\'' || c == '(' || c == ')') + { + ret.push_back(c); + } + else + { + ret.push_back('%'); + + const unsigned char n1 = static_cast(c) >> 4; + const unsigned char n2 = static_cast(c) & 0x0F; + ret.push_back((n1 >= 10) ? ('a' + (n1 - 10)) : ('0' + n1)); + ret.push_back((n2 >= 10) ? ('a' + (n2 - 10)) : ('0' + n2)); + } + } + + return ret; +} + +std::string Path::URLDecode(std::string_view str) +{ + std::string ret; + ret.reserve(str.length()); + + for (size_t i = 0, l = str.size(); i < l; i++) + { + const char c = str[i]; + if (c == '+') + { + ret.push_back(c); + } + else if (c == '%') + { + if ((i + 2) >= str.length()) + break; + + const char clower = str[i + 1]; + const char cupper = str[i + 2]; + const unsigned char lower = + (clower >= '0' && clower <= '9') ? + static_cast(clower - '0') : + ((clower >= 'a' && clower <= 'f') ? + static_cast(clower - 'a') : + ((clower >= 'A' && clower <= 'F') ? static_cast(clower - 'A') : 0)); + const unsigned char upper = + (cupper >= '0' && cupper <= '9') ? + static_cast(cupper - '0') : + ((cupper >= 'a' && cupper <= 'f') ? + static_cast(cupper - 'a') : + ((cupper >= 'A' && cupper <= 'F') ? static_cast(cupper - 'A') : 0)); + const char dch = static_cast(lower | (upper << 4)); + ret.push_back(dch); + } + else + { + ret.push_back(c); + } + } + + return std::string(str); +} + +std::string Path::CreateFileURL(std::string_view path) +{ + pxAssert(IsAbsolute(path)); + + std::string ret; + ret.reserve(path.length() + 10); + ret.append("file://"); + + const std::vector components = SplitNativePath(path); + pxAssertRel(!components.empty(), "Trying to create a URL from an empty path."); + + const std::string_view& first = components.front(); +#ifdef _WIN32 + // Windows doesn't urlencode the drive letter. + // UNC paths should be omit the leading slash. + if (first.starts_with("\\\\")) + { + // file://hostname/... + ret.append(first.substr(2)); + } + else + { + // file:///c:/... + fmt::format_to(std::back_inserter(ret), "/{}", first); + } +#else + // Don't append a leading slash for the first component. + ret.append(first); +#endif + + for (size_t comp = 1; comp < components.size(); comp++) + { + fmt::format_to(std::back_inserter(ret), "/{}", URLEncode(components[comp])); + } + + return ret; +} + std::FILE* FileSystem::OpenCFile(const char* filename, const char* mode, Error* error) { #ifdef _WIN32 diff --git a/common/HTTPDownloader.cpp b/common/HTTPDownloader.cpp index d9ec5a12e6..c830781b49 100644 --- a/common/HTTPDownloader.cpp +++ b/common/HTTPDownloader.cpp @@ -227,69 +227,6 @@ bool HTTPDownloader::HasAnyRequests() return !m_pending_http_requests.empty(); } -std::string HTTPDownloader::URLEncode(const std::string_view& str) -{ - std::string ret; - ret.reserve(str.length() + ((str.length() + 3) / 4) * 3); - - for (size_t i = 0, l = str.size(); i < l; i++) - { - const char c = str[i]; - if ((c >= '0' && c <= '9') || - (c >= 'a' && c <= 'z') || - (c >= 'A' && c <= 'Z') || - c == '-' || c == '_' || c == '.' || c == '!' || c == '~' || - c == '*' || c == '\'' || c == '(' || c == ')') - { - ret.push_back(c); - } - else - { - ret.push_back('%'); - - const unsigned char n1 = static_cast(c) >> 4; - const unsigned char n2 = static_cast(c) & 0x0F; - ret.push_back((n1 >= 10) ? ('a' + (n1 - 10)) : ('0' + n1)); - ret.push_back((n2 >= 10) ? ('a' + (n2 - 10)) : ('0' + n2)); - } - } - - return ret; -} - -std::string HTTPDownloader::URLDecode(const std::string_view& str) -{ - std::string ret; - ret.reserve(str.length()); - - for (size_t i = 0, l = str.size(); i < l; i++) - { - const char c = str[i]; - if (c == '+') - { - ret.push_back(c); - } - else if (c == '%') - { - if ((i + 2) >= str.length()) - break; - - const char clower = str[i + 1]; - const char cupper = str[i + 2]; - const unsigned char lower = (clower >= '0' && clower <= '9') ? static_cast(clower - '0') : ((clower >= 'a' && clower <= 'f') ? static_cast(clower - 'a') : ((clower >= 'A' && clower <= 'F') ? static_cast(clower - 'A') : 0)); - const unsigned char upper = (cupper >= '0' && cupper <= '9') ? static_cast(cupper - '0') : ((cupper >= 'a' && cupper <= 'f') ? static_cast(cupper - 'a') : ((cupper >= 'A' && cupper <= 'F') ? static_cast(cupper - 'A') : 0)); - const char dch = static_cast(lower | (upper << 4)); - ret.push_back(dch); - } - else - { - ret.push_back(c); - } - } - - return std::string(str); -} - std::string HTTPDownloader::GetExtensionForContentType(const std::string& content_type) { // Based on https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types diff --git a/common/HTTPDownloader.h b/common/HTTPDownloader.h index 40fddd83ce..6e2bed3d54 100644 --- a/common/HTTPDownloader.h +++ b/common/HTTPDownloader.h @@ -66,8 +66,6 @@ public: virtual ~HTTPDownloader(); static std::unique_ptr Create(std::string user_agent = DEFAULT_USER_AGENT); - static std::string URLEncode(const std::string_view& str); - static std::string URLDecode(const std::string_view& str); static std::string GetExtensionForContentType(const std::string& content_type); void SetTimeout(float timeout); diff --git a/common/Path.h b/common/Path.h index 758a786407..7e89014465 100644 --- a/common/Path.h +++ b/common/Path.h @@ -75,4 +75,13 @@ namespace Path /// Splits a path into its components, only handling native separators. std::vector SplitNativePath(const std::string_view& path); std::string JoinNativePath(const std::vector& components); + + /// URL encodes the specified string. + std::string URLEncode(std::string_view str); + + /// Decodes the specified escaped string. + std::string URLDecode(std::string_view str); + + /// Returns a URL for a given path. The path should be absolute. + std::string CreateFileURL(std::string_view path); } // namespace Path diff --git a/pcsx2-qt/QtHost.cpp b/pcsx2-qt/QtHost.cpp index 8fc16565d1..11b66922b9 100644 --- a/pcsx2-qt/QtHost.cpp +++ b/pcsx2-qt/QtHost.cpp @@ -1367,7 +1367,7 @@ QString QtHost::GetResourcesBasePath() std::string QtHost::GetRuntimeDownloadedResourceURL(std::string_view name) { - return fmt::format("{}/{}", RUNTIME_RESOURCES_URL, HTTPDownloader::URLEncode(name)); + return fmt::format("{}/{}", RUNTIME_RESOURCES_URL, Path::URLEncode(name)); } std::optional QtHost::DownloadFile(QWidget* parent, const QString& title, std::string url, std::vector* data) diff --git a/pcsx2/GameList.cpp b/pcsx2/GameList.cpp index 8162d49d57..fc47552ba8 100644 --- a/pcsx2/GameList.cpp +++ b/pcsx2/GameList.cpp @@ -1260,11 +1260,11 @@ bool GameList::DownloadCovers(const std::vector& url_templates, boo { std::string url(url_template); if (has_title) - StringUtil::ReplaceAll(&url, "${title}", HTTPDownloader::URLEncode(entry.title)); + StringUtil::ReplaceAll(&url, "${title}", Path::URLEncode(entry.title)); if (has_file_title) - StringUtil::ReplaceAll(&url, "${filetitle}", HTTPDownloader::URLEncode(Path::GetFileTitle(entry.path))); + StringUtil::ReplaceAll(&url, "${filetitle}", Path::URLEncode(Path::GetFileTitle(entry.path))); if (has_serial) - StringUtil::ReplaceAll(&url, "${serial}", HTTPDownloader::URLEncode(entry.serial)); + StringUtil::ReplaceAll(&url, "${serial}", Path::URLEncode(entry.serial)); download_urls.emplace_back(entry.path, std::move(url)); } @@ -1305,7 +1305,7 @@ bool GameList::DownloadCovers(const std::vector& url_templates, boo } // we could actually do a few in parallel here... - std::string filename(HTTPDownloader::URLDecode(url)); + std::string filename = Path::URLDecode(url); downloader->CreateRequest( std::move(url), [use_serial, &save_callback, entry_path = std::move(entry_path), filename = std::move(filename)]( s32 status_code, const std::string& content_type, HTTPDownloader::Request::Data data) { diff --git a/tests/ctest/common/path_tests.cpp b/tests/ctest/common/path_tests.cpp index 986988bfc5..0d8ad330b8 100644 --- a/tests/ctest/common/path_tests.cpp +++ b/tests/ctest/common/path_tests.cpp @@ -228,6 +228,16 @@ TEST(Path, ChangeFileName) #endif } +TEST(Path, CreateFileURL) +{ +#ifdef _WIN32 + ASSERT_EQ(Path::CreateFileURL("C:\\foo\\bar"), "file:///C:/foo/bar"); + ASSERT_EQ(Path::CreateFileURL("\\\\server\\share\\file.txt"), "file://server/share/file.txt"); +#else + ASSERT_EQ(Path::CreateFileURL("/foo/bar"), "file:///foo/bar"); +#endif +} + #if 0 // Relies on presence of files.