GameList: Handle non-extension-suffixed urls based on content type

This commit is contained in:
Connor McLaughlin
2022-09-18 13:00:55 +10:00
parent cc0127d5ed
commit d9722516c3
9 changed files with 211 additions and 36 deletions

View File

@ -69,7 +69,7 @@ static inline bool FileSystemCharacterIsSane(char32_t c, bool strip_slashes)
if (c == '*')
return false;
// macos doesn't allow colons, apparently
// macos doesn't allow colons, apparently
#ifdef __APPLE__
if (c == U':')
return false;
@ -145,6 +145,37 @@ std::string Path::SanitizeFileName(const std::string_view& str, bool strip_slash
return ret;
}
void Path::SanitizeFileName(std::string* str, bool strip_slashes /* = true */)
{
const size_t len = str->length();
char small_buf[128];
std::unique_ptr<char[]> large_buf;
char* str_copy = small_buf;
if (len >= std::size(small_buf))
{
large_buf = std::make_unique<char[]>(len + 1);
str_copy = large_buf.get();
}
std::memcpy(str_copy, str->c_str(), sizeof(char) * (len + 1));
str->clear();
size_t pos = 0;
while (pos < len)
{
char32_t ch;
pos += StringUtil::DecodeUTF8(str_copy + pos, pos - len, &ch);
ch = FileSystemCharacterIsSane(ch, strip_slashes) ? ch : U'_';
StringUtil::EncodeAndAppendUTF8(*str, ch);
}
#ifdef _WIN32
// Windows: Can't end filename with a period.
if (str->length() > 0 && str->back() == '.')
str->back() = '_';
#endif
}
bool Path::IsAbsolute(const std::string_view& path)
{
#ifdef _WIN32

View File

@ -1,6 +1,7 @@
#include "http_downloader.h"
#include "assert.h"
#include "log.h"
#include "string_util.h"
#include "timer.h"
Log_SetChannel(HTTPDownloader);
@ -100,7 +101,7 @@ void HTTPDownloader::LockedPollRequests(std::unique_lock<std::mutex>& lock)
m_pending_http_requests.erase(m_pending_http_requests.begin() + index);
lock.unlock();
req->callback(-1, Request::Data());
req->callback(-1, std::string(), Request::Data());
CloseRequest(req);
@ -122,7 +123,7 @@ void HTTPDownloader::LockedPollRequests(std::unique_lock<std::mutex>& lock)
// run callback with lock unheld
lock.unlock();
req->callback(req->status_code, req->data);
req->callback(req->status_code, std::move(req->content_type), std::move(req->data));
CloseRequest(req);
lock.lock();
}
@ -253,4 +254,97 @@ std::string HTTPDownloader::URLDecode(const std::string_view& str)
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
static constexpr const char* table[][2] = {
{"audio/aac", "aac"},
{"application/x-abiword", "abw"},
{"application/x-freearc", "arc"},
{"image/avif", "avif"},
{"video/x-msvideo", "avi"},
{"application/vnd.amazon.ebook", "azw"},
{"application/octet-stream", "bin"},
{"image/bmp", "bmp"},
{"application/x-bzip", "bz"},
{"application/x-bzip2", "bz2"},
{"application/x-cdf", "cda"},
{"application/x-csh", "csh"},
{"text/css", "css"},
{"text/csv", "csv"},
{"application/msword", "doc"},
{"application/vnd.openxmlformats-officedocument.wordprocessingml.document", "docx"},
{"application/vnd.ms-fontobject", "eot"},
{"application/epub+zip", "epub"},
{"application/gzip", "gz"},
{"image/gif", "gif"},
{"text/html", "htm"},
{"image/vnd.microsoft.icon", "ico"},
{"text/calendar", "ics"},
{"application/java-archive", "jar"},
{"image/jpeg", "jpg"},
{"text/javascript", "js"},
{"application/json", "json"},
{"application/ld+json", "jsonld"},
{"audio/midi audio/x-midi", "mid"},
{"text/javascript", "mjs"},
{"audio/mpeg", "mp3"},
{"video/mp4", "mp4"},
{"video/mpeg", "mpeg"},
{"application/vnd.apple.installer+xml", "mpkg"},
{"application/vnd.oasis.opendocument.presentation", "odp"},
{"application/vnd.oasis.opendocument.spreadsheet", "ods"},
{"application/vnd.oasis.opendocument.text", "odt"},
{"audio/ogg", "oga"},
{"video/ogg", "ogv"},
{"application/ogg", "ogx"},
{"audio/opus", "opus"},
{"font/otf", "otf"},
{"image/png", "png"},
{"application/pdf", "pdf"},
{"application/x-httpd-php", "php"},
{"application/vnd.ms-powerpoint", "ppt"},
{"application/vnd.openxmlformats-officedocument.presentationml.presentation", "pptx"},
{"application/vnd.rar", "rar"},
{"application/rtf", "rtf"},
{"application/x-sh", "sh"},
{"image/svg+xml", "svg"},
{"application/x-tar", "tar"},
{"image/tiff", "tif"},
{"video/mp2t", "ts"},
{"font/ttf", "ttf"},
{"text/plain", "txt"},
{"application/vnd.visio", "vsd"},
{"audio/wav", "wav"},
{"audio/webm", "weba"},
{"video/webm", "webm"},
{"image/webp", "webp"},
{"font/woff", "woff"},
{"font/woff2", "woff2"},
{"application/xhtml+xml", "xhtml"},
{"application/vnd.ms-excel", "xls"},
{"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "xlsx"},
{"application/xml", "xml"},
{"text/xml", "xml"},
{"application/vnd.mozilla.xul+xml", "xul"},
{"application/zip", "zip"},
{"video/3gpp", "3gp"},
{"audio/3gpp", "3gp"},
{"video/3gpp2", "3g2"},
{"audio/3gpp2", "3g2"},
{"application/x-7z-compressed", "7z"},
};
std::string ret;
for (size_t i = 0; i < std::size(table); i++)
{
if (StringUtil::Strncasecmp(table[i][0], content_type.data(), content_type.length()) == 0)
{
ret = table[i][1];
break;
}
}
return ret;
}
} // namespace Common

View File

@ -21,7 +21,7 @@ public:
struct Request
{
using Data = std::vector<u8>;
using Callback = std::function<void(s32 status_code, const Data& data)>;
using Callback = std::function<void(s32 status_code, std::string content_type, Data data)>;
enum class Type
{
@ -42,6 +42,7 @@ public:
Callback callback;
std::string url;
std::string post_data;
std::string content_type;
Data data;
u64 start_time;
s32 status_code = 0;
@ -56,6 +57,7 @@ public:
static std::unique_ptr<HTTPDownloader> Create(const char* 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);
void SetMaxActiveRequests(u32 max_active_requests);

View File

@ -88,6 +88,11 @@ void HTTPDownloaderCurl::ProcessRequest(Request* req)
long response_code = 0;
curl_easy_getinfo(req->handle, CURLINFO_RESPONSE_CODE, &response_code);
req->status_code = static_cast<s32>(response_code);
char* content_type = nullptr;
if (!curl_easy_getinfo(req->handle, CURLINFO_CONTENT_TYPE, &content_type) && content_type)
req->content_type = content_type;
Log_DevPrintf("Request for '%s' returned status code %d and %zu bytes", req->url.c_str(), req->status_code,
req->data.size());
}
@ -159,4 +164,4 @@ void HTTPDownloaderCurl::CloseRequest(HTTPDownloader::Request* request)
req->closed.store(true);
}
} // namespace FrontendCommon
} // namespace Common

View File

@ -130,6 +130,20 @@ void CALLBACK HTTPDownloaderWinHttp::HTTPStatusCallback(HINTERNET hRequest, DWOR
req->content_length = 0;
}
DWORD content_type_length = 0;
if (!WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_CONTENT_TYPE, WINHTTP_HEADER_NAME_BY_INDEX,
WINHTTP_NO_OUTPUT_BUFFER, &content_type_length, WINHTTP_NO_HEADER_INDEX) &&
GetLastError() == ERROR_INSUFFICIENT_BUFFER && content_type_length >= sizeof(content_type_length))
{
std::wstring content_type_wstring;
content_type_wstring.resize((content_type_length / sizeof(wchar_t)) - 1);
if (WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_CONTENT_TYPE, WINHTTP_HEADER_NAME_BY_INDEX,
content_type_wstring.data(), &content_type_length, WINHTTP_NO_HEADER_INDEX))
{
req->content_type = StringUtil::WideStringToUTF8String(content_type_wstring);
}
}
Log_DevPrintf("Status code %d, content-length is %u", req->status_code, req->content_length);
req->data.reserve(req->content_length);
req->state = Request::State::Receiving;
@ -224,7 +238,7 @@ bool HTTPDownloaderWinHttp::StartRequest(HTTPDownloader::Request* request)
if (!WinHttpCrackUrl(url_wide.c_str(), static_cast<DWORD>(url_wide.size()), 0, &uc))
{
Log_ErrorPrintf("WinHttpCrackUrl() failed: %u", GetLastError());
req->callback(-1, req->data);
req->callback(-1, std::string(), req->data);
delete req;
return false;
}
@ -236,7 +250,7 @@ bool HTTPDownloaderWinHttp::StartRequest(HTTPDownloader::Request* request)
if (!req->hConnection)
{
Log_ErrorPrintf("Failed to start HTTP request for '%s': %u", req->url.c_str(), GetLastError());
req->callback(-1, req->data);
req->callback(-1, std::string(), req->data);
delete req;
return false;
}
@ -297,4 +311,4 @@ void HTTPDownloaderWinHttp::CloseRequest(HTTPDownloader::Request* request)
delete req;
}
} // namespace FrontendCommon
} // namespace Common

View File

@ -23,6 +23,7 @@ void Canonicalize(std::string* path);
/// Sanitizes a filename for use in a filesystem.
std::string SanitizeFileName(const std::string_view& str, bool strip_slashes = true);
void SanitizeFileName(std::string* str, bool strip_slashes = true);
/// Returns true if the specified path is an absolute path (C:\Path on Windows or /path on Unix).
bool IsAbsolute(const std::string_view& path);