diff --git a/core.h b/core.h index 06cdbac698..40d0a193d4 100644 --- a/core.h +++ b/core.h @@ -65,6 +65,8 @@ typedef struct rarch_system_info const char *input_desc_btn[MAX_USERS][RARCH_FIRST_META_KEY]; char valid_extensions[255]; + bool supports_vfs; + struct retro_disk_control_callback disk_control_cb; struct retro_location_callback location_cb; diff --git a/dynamic.c b/dynamic.c index 274169bf31..0e6683f4d3 100644 --- a/dynamic.c +++ b/dynamic.c @@ -1959,6 +1959,7 @@ bool rarch_environment_cb(unsigned cmd, void *data) RARCH_LOG("Core requested VFS version >= v%d, providing v%d\n", vfs_iface_info->required_interface_version, supported_vfs_version); vfs_iface_info->required_interface_version = supported_vfs_version; vfs_iface_info->iface = &vfs_iface; + system->supports_vfs = true; } else { diff --git a/frontend/drivers/platform_uwp.c b/frontend/drivers/platform_uwp.c index b6fa41002c..7c5205f72d 100644 --- a/frontend/drivers/platform_uwp.c +++ b/frontend/drivers/platform_uwp.c @@ -271,18 +271,48 @@ static int frontend_uwp_parse_drive_list(void *data, bool load_content) enum msg_hash_enums enum_idx = load_content ? MENU_ENUM_LABEL_FILE_DETECT_CORE_LIST_PUSH_DIR : MSG_UNKNOWN; - /* TODO (krzys_h): UWP storage sandboxing */ - char *home_dir = (char*)malloc( - PATH_MAX_LENGTH * sizeof(char)); + char drive[] = " :\\"; + char *home_dir = (char*)malloc(PATH_MAX_LENGTH * sizeof(char)); + bool have_any_drives = false; - fill_pathname_home_dir(home_dir, - PATH_MAX_LENGTH * sizeof(char)); + fill_pathname_home_dir(home_dir, PATH_MAX_LENGTH * sizeof(char)); + + for (drive[0] = 'A'; drive[0] <= 'Z'; drive[0]++) + { + if (uwp_drive_exists(drive)) + { + menu_entries_append_enum(list, + drive, + msg_hash_to_str(MENU_ENUM_LABEL_FILE_DETECT_CORE_LIST_PUSH_DIR), + enum_idx, + FILE_TYPE_DIRECTORY, 0, 0); + have_any_drives = true; + } + } menu_entries_append_enum(list, - home_dir, - msg_hash_to_str(MENU_ENUM_LABEL_FILE_DETECT_CORE_LIST_PUSH_DIR), - enum_idx, - FILE_TYPE_DIRECTORY, 0, 0); + home_dir, + msg_hash_to_str(MENU_ENUM_LABEL_FILE_DETECT_CORE_LIST_PUSH_DIR), + enum_idx, + FILE_TYPE_DIRECTORY, 0, 0); + + if (!have_any_drives) + { + menu_entries_append_enum(list, + msg_hash_to_str(MENU_ENUM_LABEL_VALUE_FILE_BROWSER_OPEN_PICKER), + msg_hash_to_str(MENU_ENUM_LABEL_FILE_BROWSER_OPEN_PICKER), + MENU_ENUM_LABEL_FILE_BROWSER_OPEN_PICKER, + MENU_SETTING_ACTION, 0, 0); + + if (string_is_equal(uwp_device_family, "Windows.Desktop")) + { + menu_entries_append_enum(list, + msg_hash_to_str(MENU_ENUM_LABEL_VALUE_FILE_BROWSER_OPEN_UWP_PERMISSIONS), + msg_hash_to_str(MENU_ENUM_LABEL_FILE_BROWSER_OPEN_UWP_PERMISSIONS), + MENU_ENUM_LABEL_FILE_BROWSER_OPEN_UWP_PERMISSIONS, + MENU_SETTING_ACTION, 0, 0); + } + } free(home_dir); #endif diff --git a/griffin/griffin.c b/griffin/griffin.c index d4ebac5a2b..d2c609c86f 100644 --- a/griffin/griffin.c +++ b/griffin/griffin.c @@ -926,7 +926,9 @@ FILE #include "../libretro-common/streams/file_stream_transforms.c" #include "../libretro-common/streams/interface_stream.c" #include "../libretro-common/streams/memory_stream.c" +#ifndef __WINRT__ #include "../libretro-common/vfs/vfs_implementation.c" +#endif #include "../list_special.c" #include "../libretro-common/string/stdstring.c" #include "../libretro-common/file/nbio/nbio_stdio.c" diff --git a/intl/msg_hash_us.c b/intl/msg_hash_us.c index 6f46d28063..6905970494 100644 --- a/intl/msg_hash_us.c +++ b/intl/msg_hash_us.c @@ -390,6 +390,16 @@ int menu_hash_get_help_us_enum(enum msg_hash_enums msg, char *s, size_t len) snprintf(s, len, "Go back to the parent directory."); break; + case MENU_ENUM_LABEL_FILE_BROWSER_OPEN_UWP_PERMISSIONS: + snprintf(s, len, + "Open Windows permission settings to enable \n" + "the broadFileSystemAccess capability."); + break; + case MENU_ENUM_LABEL_FILE_BROWSER_OPEN_PICKER: + snprintf(s, len, + "Open the system file picker to access \n" + "additional directories."); + break; case MENU_ENUM_LABEL_FILE_BROWSER_SHADER_PRESET: snprintf(s, len, "Shader preset file."); diff --git a/intl/msg_hash_us.h b/intl/msg_hash_us.h index 04877b9445..1632690f8a 100644 --- a/intl/msg_hash_us.h +++ b/intl/msg_hash_us.h @@ -2044,6 +2044,22 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_PARENT_DIRECTORY, "Parent directory" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_FILE_BROWSER_OPEN_UWP_PERMISSIONS, + "Enable external file access" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_FILE_BROWSER_OPEN_UWP_PERMISSIONS, + "Open Windows file access permissions settings" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_FILE_BROWSER_OPEN_PICKER, + "Open..." +) +MSG_HASH( + MENU_ENUM_SUBLABEL_FILE_BROWSER_OPEN_PICKER, + "Open another directory using the system file picker" +) MSG_HASH( MENU_ENUM_LABEL_VALUE_PAUSE_LIBRETRO, "Pause when menu activated" @@ -4151,6 +4167,10 @@ MSG_HASH( MSG_ERROR_LIBRETRO_CORE_REQUIRES_SPECIAL_CONTENT, "Libretro core requires special content, but none were provided." ) +MSG_HASH( + MSG_ERROR_LIBRETRO_CORE_REQUIRES_VFS, + "Core does not support VFS, and loading from a local copy failed" +) MSG_HASH( MSG_ERROR_PARSING_ARGUMENTS, "Error parsing arguments." diff --git a/libretro-common/include/vfs/vfs_implementation.h b/libretro-common/include/vfs/vfs_implementation.h index afc2e22213..9f49f2640d 100644 --- a/libretro-common/include/vfs/vfs_implementation.h +++ b/libretro-common/include/vfs/vfs_implementation.h @@ -44,6 +44,10 @@ typedef struct retro_vfs_dir_handle libretro_vfs_implementation_dir; typedef struct libretro_vfs_implementation_dir libretro_vfs_implementation_dir; #endif +#ifdef __cplusplus +extern "C" { +#endif + libretro_vfs_implementation_file *retro_vfs_file_open_impl(const char *path, unsigned mode, unsigned hints); int retro_vfs_file_close_impl(libretro_vfs_implementation_file *stream); @@ -84,4 +88,8 @@ bool retro_vfs_dirent_is_dir_impl(libretro_vfs_implementation_dir *dirstream); int retro_vfs_closedir_impl(libretro_vfs_implementation_dir *dirstream); +#ifdef __cplusplus +} +#endif + #endif diff --git a/libretro-common/vfs/vfs_implementation_uwp.cpp b/libretro-common/vfs/vfs_implementation_uwp.cpp new file mode 100644 index 0000000000..6c0b8122b6 --- /dev/null +++ b/libretro-common/vfs/vfs_implementation_uwp.cpp @@ -0,0 +1,758 @@ +/* Copyright (C) 2018-2019 The RetroArch team +* +* --------------------------------------------------------------------------------------- +* The following license statement only applies to this file (vfs_implementation_uwp.cpp). +* --------------------------------------------------------------------------------------- +* +* Permission is hereby granted, free of charge, +* to any person obtaining a copy of this software and associated documentation files (the "Software"), +* to deal in the Software without restriction, including without limitation the rights to +* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +* and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Windows::Foundation; +using namespace Windows::Foundation::Collections; +using namespace Windows::Storage; +using namespace Windows::Storage::Streams; +using namespace Windows::Storage::FileProperties; + +#ifdef RARCH_INTERNAL +#ifndef VFS_FRONTEND +#define VFS_FRONTEND +#endif +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + /* Dear Microsoft + * I really appreciate all the effort you took to not provide any + * synchronous file APIs and block all attempts to synchronously + * wait for the results of async tasks for "smooth user experience", + * but I'm not going to run and rewrite all RetroArch cores for + * async I/O. I hope you like this hack I made instead. + */ + template + T RunAsync(std::function()> func) + { + volatile bool finished = false; + volatile Platform::Exception^ exception = nullptr; + volatile T result; + func().then([&finished, &exception, &result](concurrency::task t) { + try + { + result = t.get(); + } + catch (Platform::Exception^ exception_) + { + exception = exception_; + } + finished = true; + }); + + /* Don't stall the UI thread - prevents a deadlock */ + Windows::UI::Core::CoreWindow^ corewindow = Windows::UI::Core::CoreWindow::GetForCurrentThread(); + while (!finished) + { + if (corewindow) + corewindow->Dispatcher->ProcessEvents(Windows::UI::Core::CoreProcessEventsOption::ProcessAllIfPresent); + } + + if (exception != nullptr) + throw exception; + return result; + } + + template + T RunAsyncAndCatchErrors(std::function()> func, T valueOnError) + { + try + { + return RunAsync(func); + } + catch (Platform::Exception^ e) + { + return valueOnError; + } + } + + + void windowsize_path(wchar_t* path) + { + /* UWP deals with paths containing / instead of \ way worse than normal Windows */ + /* and RetroArch may sometimes mix them (e.g. on archive extraction) */ + if (!path) + return; + while (*path) + { + if (*path == '/') + *path = '\\'; + ++path; + } + } +} + +namespace +{ + /* Damn you, UWP, why no functions for that either */ + template + concurrency::task GetItemFromPathAsync(Platform::String^ path) + { + static_assert(false, "StorageFile and StorageFolder only"); + } + template<> + concurrency::task GetItemFromPathAsync(Platform::String^ path) + { + return concurrency::create_task(StorageFile::GetFileFromPathAsync(path)); + } + template<> + concurrency::task GetItemFromPathAsync(Platform::String^ path) + { + return concurrency::create_task(StorageFolder::GetFolderFromPathAsync(path)); + } + + template + concurrency::task GetItemInFolderFromPathAsync(StorageFolder^ folder, Platform::String^ path) + { + static_assert(false, "StorageFile and StorageFolder only"); + } + template<> + concurrency::task GetItemInFolderFromPathAsync(StorageFolder^ folder, Platform::String^ path) + { + if (path->IsEmpty()) + retro_assert(false); /* Attempt to read a folder as a file - this really should have been caught earlier */ + return concurrency::create_task(folder->GetFileAsync(path)); + } + template<> + concurrency::task GetItemInFolderFromPathAsync(StorageFolder^ folder, Platform::String^ path) + { + if (path->IsEmpty()) + return concurrency::create_task(concurrency::create_async([folder]() { return folder; })); + return concurrency::create_task(folder->GetFolderAsync(path)); + } +} + +namespace +{ + /* A list of all StorageFolder objects returned using from the file picker */ + Platform::Collections::Vector accessible_directories; + + concurrency::task TriggerPickerAddDialog() + { + auto folderPicker = ref new Windows::Storage::Pickers::FolderPicker(); + folderPicker->SuggestedStartLocation = Windows::Storage::Pickers::PickerLocationId::Desktop; + folderPicker->FileTypeFilter->Append("*"); + + return concurrency::create_task(folderPicker->PickSingleFolderAsync()).then([](StorageFolder^ folder) { + if (folder == nullptr) + throw ref new Platform::Exception(E_ABORT, L"Operation cancelled by user"); + + /* TODO: check for duplicates */ + accessible_directories.Append(folder); + return folder->Path; + }); + } + + template + concurrency::task LocateStorageItem(Platform::String^ path) + { + /* Look for a matching directory we can use */ + for each (StorageFolder^ folder in accessible_directories) + { + std::wstring folder_path = folder->Path->Data(); + /* Could be C:\ or C:\Users\somebody - remove the trailing slash to unify them */ + if (folder_path[folder_path.size() - 1] == '\\') + folder_path.erase(folder_path.size() - 1); + std::wstring file_path = path->Data(); + if (file_path.find(folder_path) == 0) + { + /* Found a match */ + file_path = file_path.length() > folder_path.length() ? file_path.substr(folder_path.length() + 1) : L""; + return concurrency::create_task(GetItemInFolderFromPathAsync(folder, ref new Platform::String(file_path.data()))); + } + } + + /* No matches - try accessing directly, and fallback to user prompt */ + return concurrency::create_task(GetItemFromPathAsync(path)).then([path](concurrency::task item) { + try + { + T^ storageItem = item.get(); + return concurrency::create_task(concurrency::create_async([storageItem]() { return storageItem; })); + } + catch (Platform::AccessDeniedException^ e) + { + Windows::UI::Popups::MessageDialog^ dialog = + ref new Windows::UI::Popups::MessageDialog("Path \"" + path + "\" is not currently accessible. Please open any containing directory to access it."); + dialog->Commands->Append(ref new Windows::UI::Popups::UICommand("Open file picker")); + dialog->Commands->Append(ref new Windows::UI::Popups::UICommand("Cancel")); + return concurrency::create_task(dialog->ShowAsync()).then([path](Windows::UI::Popups::IUICommand^ cmd) { + if (cmd->Label == "Open file picker") + { + return TriggerPickerAddDialog().then([path](Platform::String^ added_path) { + /* Retry */ + return LocateStorageItem(path); + }); + } + else + { + throw ref new Platform::Exception(E_ABORT, L"Operation cancelled by user"); + } + }); + } + }); + } + + IStorageItem^ LocateStorageFileOrFolder(Platform::String^ path) + { + if (!path || path->IsEmpty()) + return nullptr; + + if (*(path->End() - 1) == '\\') + { + /* Ends with a slash, so it's definitely a directory */ + return RunAsyncAndCatchErrors([=]() { + return concurrency::create_task(LocateStorageItem(path)); + }, nullptr); + } + else + { + /* No final slash - probably a file (since RetroArch usually slash-terminates dirs), but there is still a chance it's a directory */ + IStorageItem^ item; + item = RunAsyncAndCatchErrors([=]() { + return concurrency::create_task(LocateStorageItem(path)); + }, nullptr); + if (!item) + { + item = RunAsyncAndCatchErrors([=]() { + return concurrency::create_task(LocateStorageItem(path)); + }, nullptr); + } + return item; + } + } +} + +#ifdef VFS_FRONTEND +struct retro_vfs_file_handle +#else +struct libretro_vfs_implementation_file +#endif +{ + IRandomAccessStream^ fp; + char* orig_path; +}; + +libretro_vfs_implementation_file *retro_vfs_file_open_impl(const char *path, unsigned mode, unsigned hints) +{ + if (!path || !*path) + return NULL; + + if (!path_is_absolute(path)) + { + RARCH_WARN("Something tried to access files from current directory ('%s'). This is not allowed on UWP.\n", path); + return NULL; + } + + if (path_char_is_slash(path[strlen(path) - 1])) + { + RARCH_WARN("Trying to open a directory as file?! ('%s')\n", path); + return NULL; + } + + char* dirpath = (char*)malloc(PATH_MAX_LENGTH * sizeof(char)); + fill_pathname_basedir(dirpath, path, PATH_MAX_LENGTH); + wchar_t *dirpath_wide = utf8_to_utf16_string_alloc(dirpath); + windowsize_path(dirpath_wide); + Platform::String^ dirpath_str = ref new Platform::String(dirpath_wide); + free(dirpath_wide); + free(dirpath); + + char* filename = (char*)malloc(PATH_MAX_LENGTH * sizeof(char)); + fill_pathname_base(filename, path, PATH_MAX_LENGTH); + wchar_t *filename_wide = utf8_to_utf16_string_alloc(filename); + Platform::String^ filename_str = ref new Platform::String(filename_wide); + free(filename_wide); + free(filename); + + retro_assert(!dirpath_str->IsEmpty() && !filename_str->IsEmpty()); + + return RunAsyncAndCatchErrors([=]() { + return concurrency::create_task(LocateStorageItem(dirpath_str)).then([=](StorageFolder^ dir) { + if (mode == RETRO_VFS_FILE_ACCESS_READ) + return dir->GetFileAsync(filename_str); + else + return dir->CreateFileAsync(filename_str, (mode & RETRO_VFS_FILE_ACCESS_UPDATE_EXISTING) != 0 ? + CreationCollisionOption::OpenIfExists : CreationCollisionOption::ReplaceExisting); + }).then([=](StorageFile^ file) { + FileAccessMode accessMode = mode == RETRO_VFS_FILE_ACCESS_READ ? + FileAccessMode::Read : FileAccessMode::ReadWrite; + return file->OpenAsync(accessMode); + }).then([=](IRandomAccessStream^ fpstream) { + libretro_vfs_implementation_file *stream = (libretro_vfs_implementation_file*)calloc(1, sizeof(*stream)); + if (!stream) + return (libretro_vfs_implementation_file*)NULL; + + stream->orig_path = strdup(path); + stream->fp = fpstream; + stream->fp->Seek(0); + return stream; + }); + }, NULL); +} + +int retro_vfs_file_close_impl(libretro_vfs_implementation_file *stream) +{ + if (!stream || !stream->fp) + return -1; + + /* Apparently, this is how you close a file in WinRT */ + /* Yes, really */ + stream->fp = nullptr; + + return 0; +} + +int retro_vfs_file_error_impl(libretro_vfs_implementation_file *stream) +{ + return false; /* TODO */ +} + +int64_t retro_vfs_file_size_impl(libretro_vfs_implementation_file *stream) +{ + if (!stream || !stream->fp) + return 0; + return stream->fp->Size; +} + +int64_t retro_vfs_file_truncate_impl(libretro_vfs_implementation_file *stream, int64_t length) +{ + if (!stream || !stream->fp) + return -1; + stream->fp->Size = length; + return 0; +} + +int64_t retro_vfs_file_tell_impl(libretro_vfs_implementation_file *stream) +{ + if (!stream || !stream->fp) + return -1; + return stream->fp->Position; +} + +int64_t retro_vfs_file_seek_impl(libretro_vfs_implementation_file *stream, int64_t offset, int seek_position) +{ + if (!stream || !stream->fp) + return -1; + + switch (seek_position) + { + case RETRO_VFS_SEEK_POSITION_START: + stream->fp->Seek(offset); + break; + + case RETRO_VFS_SEEK_POSITION_CURRENT: + stream->fp->Seek(stream->fp->Position + offset); + break; + + case RETRO_VFS_SEEK_POSITION_END: + stream->fp->Seek(stream->fp->Size - offset); + break; + } + + return 0; +} + +/* This is some pure magic and I have absolutely no idea how it works */ +/* Wraps a raw buffer into a WinRT object */ +/* https://stackoverflow.com/questions/10520335/how-to-wrap-a-char-buffer-in-a-winrt-ibuffer-in-c */ +class NativeBuffer : + public Microsoft::WRL::RuntimeClass, + ABI::Windows::Storage::Streams::IBuffer, + Windows::Storage::Streams::IBufferByteAccess> +{ +public: + virtual ~NativeBuffer() + { + } + + HRESULT __stdcall RuntimeClassInitialize(byte *buffer, uint32_t capacity, uint32_t length) + { + m_buffer = buffer; + m_capacity = capacity; + m_length = length; + return S_OK; + } + + HRESULT __stdcall Buffer(byte **value) + { + if (m_buffer == nullptr) + return E_INVALIDARG; + *value = m_buffer; + return S_OK; + } + + HRESULT __stdcall get_Capacity(uint32_t *value) + { + *value = m_capacity; + return S_OK; + } + + HRESULT __stdcall get_Length(uint32_t *value) + { + *value = m_length; + return S_OK; + } + + HRESULT __stdcall put_Length(uint32_t value) + { + if (value > m_capacity) + return E_INVALIDARG; + m_length = value; + return S_OK; + } + +private: + byte *m_buffer; + uint32_t m_capacity; + uint32_t m_length; +}; + +IBuffer^ CreateNativeBuffer(void* buf, uint32_t capacity, uint32_t length) +{ + Microsoft::WRL::ComPtr nativeBuffer; + Microsoft::WRL::Details::MakeAndInitialize(&nativeBuffer, (byte *)buf, capacity, length); + auto iinspectable = (IInspectable *)reinterpret_cast(nativeBuffer.Get()); + IBuffer ^buffer = reinterpret_cast(iinspectable); + return buffer; +} + +int64_t retro_vfs_file_read_impl(libretro_vfs_implementation_file *stream, void *s, uint64_t len) +{ + if (!stream || !stream->fp || !s) + return -1; + + IBuffer^ buffer = CreateNativeBuffer(s, len, 0); + return RunAsyncAndCatchErrors([=]() { + return concurrency::create_task(stream->fp->ReadAsync(buffer, buffer->Capacity, InputStreamOptions::None)).then([=](IBuffer^ buf) { + retro_assert(buf == buffer); + return (int64_t)buffer->Length; + }); + }, -1); +} + +int64_t retro_vfs_file_write_impl(libretro_vfs_implementation_file *stream, const void *s, uint64_t len) +{ + if (!stream || !stream->fp || !s) + return -1; + + IBuffer^ buffer = CreateNativeBuffer(const_cast(s), len, len); + return RunAsyncAndCatchErrors([=]() { + return concurrency::create_task(stream->fp->WriteAsync(buffer)).then([=](unsigned int written) { + return (int64_t)written; + }); + }, -1); +} + +int retro_vfs_file_flush_impl(libretro_vfs_implementation_file *stream) +{ + if (!stream || !stream->fp) + return -1; + + return RunAsyncAndCatchErrors([=]() { + return concurrency::create_task(stream->fp->FlushAsync()).then([=](bool this_value_is_not_even_documented_wtf) { + /* The bool value may be reporting an error or something, but just leave it alone for now */ + /* https://github.com/MicrosoftDocs/winrt-api/issues/841 */ + return 0; + }); + }, -1); +} + +int retro_vfs_file_remove_impl(const char *path) +{ + if (!path || !*path) + return -1; + + wchar_t *path_wide = utf8_to_utf16_string_alloc(path); + windowsize_path(path_wide); + Platform::String^ path_str = ref new Platform::String(path_wide); + free(path_wide); + + return RunAsyncAndCatchErrors([=]() { + return concurrency::create_task(LocateStorageItem(path_str)).then([=](StorageFile^ file) { + return file->DeleteAsync(StorageDeleteOption::PermanentDelete); + }).then([=]() { + return 0; + }); + }, -1); +} + +/* TODO: this may not work if trying to move a directory */ +int retro_vfs_file_rename_impl(const char *old_path, const char *new_path) +{ + if (!old_path || !*old_path || !new_path || !*new_path) + return -1; + + wchar_t* old_path_wide = utf8_to_utf16_string_alloc(old_path); + Platform::String^ old_path_str = ref new Platform::String(old_path_wide); + free(old_path_wide); + + char* new_dir_path = (char*)malloc(PATH_MAX_LENGTH * sizeof(char)); + fill_pathname_basedir(new_dir_path, new_path, PATH_MAX_LENGTH); + wchar_t *new_dir_path_wide = utf8_to_utf16_string_alloc(new_dir_path); + windowsize_path(new_dir_path_wide); + Platform::String^ new_dir_path_str = ref new Platform::String(new_dir_path_wide); + free(new_dir_path_wide); + free(new_dir_path); + + char* new_file_name = (char*)malloc(PATH_MAX_LENGTH * sizeof(char)); + fill_pathname_base(new_file_name, new_path, PATH_MAX_LENGTH); + wchar_t *new_file_name_wide = utf8_to_utf16_string_alloc(new_file_name); + Platform::String^ new_file_name_str = ref new Platform::String(new_file_name_wide); + free(new_file_name_wide); + free(new_file_name); + + retro_assert(!old_path_str->IsEmpty() && !new_dir_path_str->IsEmpty() && !new_file_name_str->IsEmpty()); + + return RunAsyncAndCatchErrors([=]() { + concurrency::task old_file_task = concurrency::create_task(LocateStorageItem(old_path_str)); + concurrency::task new_dir_task = concurrency::create_task(LocateStorageItem(new_dir_path_str)); + return concurrency::create_task([=] { + /* Run these two tasks in parallel */ + /* TODO: There may be some cleaner way to express this */ + concurrency::task_group group; + group.run([=] { return old_file_task; }); + group.run([=] { return new_dir_task; }); + group.wait(); + }).then([=]() { + return old_file_task.get()->MoveAsync(new_dir_task.get(), new_file_name_str, NameCollisionOption::ReplaceExisting); + }).then([=]() { + return 0; + }); + }, -1); +} + +const char *retro_vfs_file_get_path_impl(libretro_vfs_implementation_file *stream) +{ + /* should never happen, do something noisy so caller can be fixed */ + if (!stream) + abort(); + return stream->orig_path; +} + +int retro_vfs_stat_impl(const char *path, int32_t *size) +{ + if (!path || !*path) + return 0; + + wchar_t *path_wide = utf8_to_utf16_string_alloc(path); + windowsize_path(path_wide); + Platform::String^ path_str = ref new Platform::String(path_wide); + free(path_wide); + + IStorageItem^ item = LocateStorageFileOrFolder(path_str); + if (!item) + return 0; + + return RunAsyncAndCatchErrors([=]() { + return concurrency::create_task(item->GetBasicPropertiesAsync()).then([=](BasicProperties^ properties) { + if (size) + *size = properties->Size; + return item->IsOfType(StorageItemTypes::Folder) ? RETRO_VFS_STAT_IS_VALID | RETRO_VFS_STAT_IS_DIRECTORY : RETRO_VFS_STAT_IS_VALID; + }); + }, 0); +} + +int retro_vfs_mkdir_impl(const char *dir) +{ + if (!dir || !*dir) + return -1; + + char* dir_local = strdup(dir); + /* If the path ends with a slash, we have to remove it for basename to work */ + char* tmp = dir_local + strlen(dir_local) - 1; + if (path_char_is_slash(*tmp)) + *tmp = 0; + + char* dir_name = (char*)malloc(PATH_MAX_LENGTH * sizeof(char)); + fill_pathname_base(dir_name, dir_local, PATH_MAX_LENGTH); + wchar_t *dir_name_wide = utf8_to_utf16_string_alloc(dir_name); + Platform::String^ dir_name_str = ref new Platform::String(dir_name_wide); + free(dir_name_wide); + free(dir_name); + + char* parent_path = (char*)malloc(PATH_MAX_LENGTH * sizeof(char)); + fill_pathname_parent_dir(parent_path, dir_local, PATH_MAX_LENGTH); + wchar_t *parent_path_wide = utf8_to_utf16_string_alloc(parent_path); + windowsize_path(parent_path_wide); + Platform::String^ parent_path_str = ref new Platform::String(parent_path_wide); + free(parent_path_wide); + free(parent_path); + + retro_assert(!dir_name_str->IsEmpty() && !parent_path_str->IsEmpty()); + + free(dir_local); + + return RunAsyncAndCatchErrors([=]() { + return concurrency::create_task(LocateStorageItem(parent_path_str)).then([=](StorageFolder^ parent) { + return parent->CreateFolderAsync(dir_name_str); + }).then([=](concurrency::task new_dir) { + try + { + new_dir.get(); + } + catch (Platform::COMException^ e) + { + if (e->HResult == HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS)) + return -2; + throw; + } + return 0; + }); + }, -1); +} + +#ifdef VFS_FRONTEND +struct retro_vfs_dir_handle +#else +struct libretro_vfs_implementation_dir +#endif +{ + IVectorView^ directory; + IIterator^ entry; + char *entry_name; +}; + +libretro_vfs_implementation_dir *retro_vfs_opendir_impl(const char *name, bool include_hidden) +{ + libretro_vfs_implementation_dir *rdir; + + if (!name || !*name) + return NULL; + + rdir = (libretro_vfs_implementation_dir*)calloc(1, sizeof(*rdir)); + if (!rdir) + return NULL; + + wchar_t *name_wide = utf8_to_utf16_string_alloc(name); + windowsize_path(name_wide); + Platform::String^ name_str = ref new Platform::String(name_wide); + free(name_wide); + + rdir->directory = RunAsyncAndCatchErrors^>([=]() { + return concurrency::create_task(LocateStorageItem(name_str)).then([=](StorageFolder^ folder) { + return folder->GetItemsAsync(); + }); + }, nullptr); + + if (rdir->directory) + return rdir; + + free(rdir); + return NULL; +} + +bool retro_vfs_readdir_impl(libretro_vfs_implementation_dir *rdir) +{ + if (!rdir->entry) + { + rdir->entry = rdir->directory->First(); + return rdir->entry->HasCurrent; + } + else + { + return rdir->entry->MoveNext(); + } +} + +const char *retro_vfs_dirent_get_name_impl(libretro_vfs_implementation_dir *rdir) +{ + if (rdir->entry_name) + free(rdir->entry_name); + rdir->entry_name = utf16_to_utf8_string_alloc(rdir->entry->Current->Name->Data()); + return rdir->entry_name; +} + +bool retro_vfs_dirent_is_dir_impl(libretro_vfs_implementation_dir *rdir) +{ + return rdir->entry->Current->IsOfType(StorageItemTypes::Folder); +} + +int retro_vfs_closedir_impl(libretro_vfs_implementation_dir *rdir) +{ + if (!rdir) + return -1; + + if (rdir->entry_name) + free(rdir->entry_name); + rdir->entry = nullptr; + rdir->directory = nullptr; + + free(rdir); + return 0; +} + +bool uwp_is_path_accessible_using_standard_io(char *path) +{ + char *relative_path_abbrev = (char*)malloc(PATH_MAX_LENGTH * sizeof(char)); + fill_pathname_abbreviate_special(relative_path_abbrev, path, PATH_MAX_LENGTH * sizeof(char)); + + bool result = strlen(relative_path_abbrev) >= 2 && (relative_path_abbrev[0] == ':' || relative_path_abbrev[0] == '~') && path_char_is_slash(relative_path_abbrev[1]); + + free(relative_path_abbrev); + return result; +} + +bool uwp_drive_exists(const char *path) +{ + if (!path || !*path) + return 0; + + wchar_t *path_wide = utf8_to_utf16_string_alloc(path); + Platform::String^ path_str = ref new Platform::String(path_wide); + free(path_wide); + + return RunAsyncAndCatchErrors([=]() { + return concurrency::create_task(StorageFolder::GetFolderFromPathAsync(path_str)).then([](StorageFolder^ properties) { + return true; + }); + }, false); +} + +char* uwp_trigger_picker(void) +{ + return RunAsyncAndCatchErrors([=]() { + return TriggerPickerAddDialog().then([](Platform::String^ path) { + return utf16_to_utf8_string_alloc(path->Data()); + }); + }, NULL); +} diff --git a/menu/cbs/menu_cbs_ok.c b/menu/cbs/menu_cbs_ok.c index 18019b2f7b..404ccc5a02 100644 --- a/menu/cbs/menu_cbs_ok.c +++ b/menu/cbs/menu_cbs_ok.c @@ -81,6 +81,10 @@ #include "../../record/record_driver.h" +#ifdef __WINRT__ +#include "../../uwp/uwp_func.h" +#endif + enum { ACTION_OK_LOAD_PRESET = 0, @@ -3976,6 +3980,39 @@ default_action_ok_func(action_ok_push_accounts_youtube_list, ACTION_OK_DL_ACCOUN default_action_ok_func(action_ok_push_accounts_twitch_list, ACTION_OK_DL_ACCOUNTS_TWITCH_LIST) default_action_ok_func(action_ok_open_archive, ACTION_OK_DL_OPEN_ARCHIVE) +static int action_ok_open_uwp_permission_settings(const char *path, + const char *label, unsigned type, size_t idx, size_t entry_idx) +{ +#ifdef __WINRT__ + uwp_open_broadfilesystemaccess_settings(); +#else + retro_assert(false); +#endif + return 0; +} + +static int action_ok_open_picker(const char *path, + const char *label, unsigned type, size_t idx, size_t entry_idx) +{ + char* new_path; + int ret; +#ifdef __WINRT__ + new_path = uwp_trigger_picker(); + if (!new_path) + return 0; /* User aborted */ +#else + retro_assert(false); +#endif + + ret = generic_action_ok_displaylist_push(path, new_path, + msg_hash_to_str(MENU_ENUM_LABEL_FAVORITES), + type, idx, + entry_idx, ACTION_OK_DL_CONTENT_LIST); + + free(new_path); + return ret; +} + static int action_ok_shader_pass(const char *path, const char *label, unsigned type, size_t idx, size_t entry_idx) { @@ -5476,6 +5513,12 @@ static int menu_cbs_init_bind_ok_compare_label(menu_file_list_cbs_t *cbs, case MENU_ENUM_LABEL_MENU_FILE_BROWSER_SETTINGS: BIND_ACTION_OK(cbs, action_ok_menu_file_browser_list); break; + case MENU_ENUM_LABEL_FILE_BROWSER_OPEN_UWP_PERMISSIONS: + BIND_ACTION_OK(cbs, action_ok_open_uwp_permission_settings); + break; + case MENU_ENUM_LABEL_FILE_BROWSER_OPEN_PICKER: + BIND_ACTION_OK(cbs, action_ok_open_picker); + break; case MENU_ENUM_LABEL_RETRO_ACHIEVEMENTS_SETTINGS: BIND_ACTION_OK(cbs, action_ok_retro_achievements_list); break; diff --git a/menu/cbs/menu_cbs_sublabel.c b/menu/cbs/menu_cbs_sublabel.c index 1bd69f3765..520edfe59b 100644 --- a/menu/cbs/menu_cbs_sublabel.c +++ b/menu/cbs/menu_cbs_sublabel.c @@ -334,6 +334,8 @@ default_sublabel_macro(action_bind_sublabel_goto_images, default_sublabel_macro(action_bind_sublabel_goto_music, MENU_ENUM_SUBLABEL_GOTO_MUSIC) default_sublabel_macro(action_bind_sublabel_goto_video, MENU_ENUM_SUBLABEL_GOTO_VIDEO) default_sublabel_macro(action_bind_sublabel_menu_filebrowser_settings, MENU_ENUM_SUBLABEL_MENU_FILE_BROWSER_SETTINGS) +default_sublabel_macro(action_bind_sublabel_menu_filebrowser_open_uwp_permissions, MENU_ENUM_SUBLABEL_FILE_BROWSER_OPEN_UWP_PERMISSIONS) +default_sublabel_macro(action_bind_sublabel_menu_filebrowser_open_picker, MENU_ENUM_SUBLABEL_FILE_BROWSER_OPEN_PICKER) default_sublabel_macro(action_bind_sublabel_auto_remaps_enable, MENU_ENUM_SUBLABEL_AUTO_REMAPS_ENABLE) default_sublabel_macro(action_bind_sublabel_auto_overrides_enable, MENU_ENUM_SUBLABEL_AUTO_OVERRIDES_ENABLE) default_sublabel_macro(action_bind_sublabel_game_specific_options, MENU_ENUM_SUBLABEL_GAME_SPECIFIC_OPTIONS) @@ -1379,6 +1381,12 @@ int menu_cbs_init_bind_sublabel(menu_file_list_cbs_t *cbs, case MENU_ENUM_LABEL_MENU_FILE_BROWSER_SETTINGS: BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_menu_filebrowser_settings); break; + case MENU_ENUM_LABEL_FILE_BROWSER_OPEN_UWP_PERMISSIONS: + BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_menu_filebrowser_open_uwp_permissions); + break; + case MENU_ENUM_LABEL_FILE_BROWSER_OPEN_PICKER: + BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_menu_filebrowser_open_picker); + break; case MENU_ENUM_LABEL_ADD_TO_FAVORITES: BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_add_to_favorites); break; diff --git a/msg_hash.h b/msg_hash.h index d72ee9c761..e0c9295136 100644 --- a/msg_hash.h +++ b/msg_hash.h @@ -260,6 +260,7 @@ enum msg_hash_enums MSG_COMPILED_AGAINST_API, MSG_ERROR_LIBRETRO_CORE_REQUIRES_SPECIAL_CONTENT, MSG_ERROR_LIBRETRO_CORE_REQUIRES_CONTENT, + MSG_ERROR_LIBRETRO_CORE_REQUIRES_VFS, MSG_SEVERAL_PATCHES_ARE_EXPLICITLY_DEFINED, MSG_DID_NOT_FIND_A_VALID_CONTENT_PATCH, MSG_FAILED_TO_ALLOCATE_MEMORY_FOR_PATCHED_CONTENT, @@ -823,6 +824,9 @@ enum msg_hash_enums MENU_LABEL(PARENT_DIRECTORY), + MENU_LABEL(FILE_BROWSER_OPEN_UWP_PERMISSIONS), + MENU_LABEL(FILE_BROWSER_OPEN_PICKER), + MENU_ENUM_LABEL_CONTENT_ACTIONS, /* Menu settings */ diff --git a/pkg/msvc-uwp/RetroArch-msvc2017-UWP/Package.appxmanifest b/pkg/msvc-uwp/RetroArch-msvc2017-UWP/Package.appxmanifest index e1d4376bbd..1d99be534b 100644 --- a/pkg/msvc-uwp/RetroArch-msvc2017-UWP/Package.appxmanifest +++ b/pkg/msvc-uwp/RetroArch-msvc2017-UWP/Package.appxmanifest @@ -1,5 +1,5 @@ - + @@ -25,5 +25,6 @@ + diff --git a/pkg/msvc-uwp/RetroArch-msvc2017-UWP/RetroArch-msvc2017-UWP.vcxproj b/pkg/msvc-uwp/RetroArch-msvc2017-UWP/RetroArch-msvc2017-UWP.vcxproj index 1f21ca7a6c..4d8b42ab77 100644 --- a/pkg/msvc-uwp/RetroArch-msvc2017-UWP/RetroArch-msvc2017-UWP.vcxproj +++ b/pkg/msvc-uwp/RetroArch-msvc2017-UWP/RetroArch-msvc2017-UWP.vcxproj @@ -282,6 +282,9 @@ + + + diff --git a/pkg/msvc-uwp/RetroArch-msvc2017-UWP/RetroArch-msvc2017-UWP.vcxproj.filters b/pkg/msvc-uwp/RetroArch-msvc2017-UWP/RetroArch-msvc2017-UWP.vcxproj.filters index 3fcb574ea4..dd9507e079 100644 --- a/pkg/msvc-uwp/RetroArch-msvc2017-UWP/RetroArch-msvc2017-UWP.vcxproj.filters +++ b/pkg/msvc-uwp/RetroArch-msvc2017-UWP/RetroArch-msvc2017-UWP.vcxproj.filters @@ -10,6 +10,9 @@ {c3155604-6d38-494a-bfe0-861cef871cb2} + + {d41660c5-7f5b-442c-b5d7-03e6e9af8172} + @@ -40,6 +43,9 @@ griffin + + libretro-common-uwp + diff --git a/tasks/task_content.c b/tasks/task_content.c index f3dcb8f891..c4adcd7948 100644 --- a/tasks/task_content.c +++ b/tasks/task_content.c @@ -39,6 +39,10 @@ #endif #endif +#ifdef __WINRT__ +#include +#endif + #ifdef HAVE_CONFIG_H #include "../config.h" #endif @@ -558,6 +562,8 @@ static bool content_file_load( retro_ctx_load_content_info_t load_info; size_t msg_size = 1024 * sizeof(char); char *msg = (char*)malloc(msg_size); + rarch_system_info_t *system = runloop_get_system_info(); + bool used_vfs_fallback_copy = false; msg[0] = '\0'; @@ -606,7 +612,6 @@ static bool content_file_load( } else { - #ifdef HAVE_COMPRESSION if ( !content_ctx->block_extract && need_fullpath @@ -618,6 +623,80 @@ static bool content_file_load( error_string)) goto error; #endif + +#ifdef __WINRT__ + /* TODO: When support for the 'actual' VFS is added, there will need to be some more logic here */ + if (!system->supports_vfs && !uwp_is_path_accessible_using_standard_io(path)) + { + /* Fallback to a file copy into an accessible directory */ + + union string_list_elem_attr attributes; + size_t new_basedir_size = PATH_MAX_LENGTH * sizeof(char); + size_t new_path_size = PATH_MAX_LENGTH * sizeof(char); + char *new_basedir = (char*)malloc(new_basedir_size); + char *new_path = (char*)malloc(new_path_size); + char* buf; + int64_t len; + + new_path[0] = '\0'; + new_basedir[0] = '\0'; + attributes.i = 0; + + RARCH_LOG("Core does not support VFS - copying to cache directory\n"); + + if (!string_is_empty(content_ctx->directory_cache)) + strlcpy(new_basedir, content_ctx->directory_cache, new_basedir_size); + if (string_is_empty(new_basedir) || !path_is_directory(new_basedir) || !uwp_is_path_accessible_using_standard_io(new_basedir)) + { + RARCH_WARN("Tried copying to cache directory, but " + "cache directory was not set or found. " + "Setting cache directory to root of " + "writable app directory...\n"); + strlcpy(new_basedir, uwp_dir_data, new_basedir_size); + } + + fill_pathname_join(new_path, new_basedir, + path_basename(path), new_path_size); + + /* TODO: This may fail on very large files... but copying large files is not a good idea anyway */ + if (!filestream_read_file(path, &buf, &len)) + { + snprintf(msg, + msg_size, + "%s \"%s\". (during copy read)\n", + msg_hash_to_str(MSG_COULD_NOT_READ_CONTENT_FILE), + path); + *error_string = strdup(msg); + goto error; + } + if (!filestream_write_file(new_path, buf, len)) + { + free(buf); + snprintf(msg, + msg_size, + "%s \"%s\". (during copy write)\n", + msg_hash_to_str(MSG_COULD_NOT_READ_CONTENT_FILE), + path); + *error_string = strdup(msg); + goto error; + } + free(buf); + + string_list_append(additional_path_allocs, new_path, attributes); + info[i].path = + additional_path_allocs->elems[additional_path_allocs->size - 1].data; + + string_list_append(content_ctx->temporary_content, + new_path, attributes); + + free(new_basedir); + free(new_path); + + used_vfs_fallback_copy = true; + } +#endif + + RARCH_LOG("%s\n", msg_hash_to_str(MSG_CONTENT_LOADING_SKIPPED_IMPLEMENTATION_WILL_DO_IT)); content_rom_crc = file_crc32(0, path); RARCH_LOG("CRC32: 0x%x .\n", (unsigned)content_rom_crc); @@ -631,9 +710,19 @@ static bool content_file_load( if (!core_load_game(&load_info)) { - snprintf(msg, + if (used_vfs_fallback_copy) + { + /* This is probably going to fail on multifile ROMs etc. so give a visible explanation of what is likely wrong */ + snprintf(msg, + msg_size, + "%s.", msg_hash_to_str(MSG_ERROR_LIBRETRO_CORE_REQUIRES_VFS)); + } + else + { + snprintf(msg, msg_size, "%s.", msg_hash_to_str(MSG_FAILED_TO_LOAD_CONTENT)); + } *error_string = strdup(msg); goto error; } diff --git a/uwp/uwp_func.h b/uwp/uwp_func.h index ecab3b8a03..a48382fbe6 100644 --- a/uwp/uwp_func.h +++ b/uwp/uwp_func.h @@ -26,6 +26,11 @@ extern char uwp_dir_install[PATH_MAX_LENGTH]; extern char uwp_dir_data[PATH_MAX_LENGTH]; extern char uwp_device_family[128]; +void uwp_open_broadfilesystemaccess_settings(void); +bool uwp_is_path_accessible_using_standard_io(char *path); +bool uwp_drive_exists(const char *path); +char* uwp_trigger_picker(void); + void* uwp_get_corewindow(void); void uwp_input_next_frame(void); diff --git a/uwp/uwp_main.cpp b/uwp/uwp_main.cpp index bc1c9b5e13..30b2b8b5a2 100644 --- a/uwp/uwp_main.cpp +++ b/uwp/uwp_main.cpp @@ -627,4 +627,9 @@ extern "C" { return 0; } + + void uwp_open_broadfilesystemaccess_settings(void) + { + Windows::System::Launcher::LaunchUriAsync(ref new Uri("ms-settings:privacy-broadfilesystemaccess")); + } }