diff --git a/src/xenia/app/emulator_window.cc b/src/xenia/app/emulator_window.cc index 8188e04f4..a5cf3f90d 100644 --- a/src/xenia/app/emulator_window.cc +++ b/src/xenia/app/emulator_window.cc @@ -553,17 +553,26 @@ bool EmulatorWindow::Initialize() { auto main_menu = MenuItem::Create(MenuItem::Type::kNormal); auto file_menu = MenuItem::Create(MenuItem::Type::kPopup, "&File"); auto recent_menu = MenuItem::Create(MenuItem::Type::kPopup, "&Open Recent"); + auto zar_menu = MenuItem::Create(MenuItem::Type::kPopup, "&Zar Package"); FillRecentlyLaunchedTitlesMenu(recent_menu.get()); { file_menu->AddChild( MenuItem::Create(MenuItem::Type::kString, "&Open...", "Ctrl+O", std::bind(&EmulatorWindow::FileOpen, this))); file_menu->AddChild(std::move(recent_menu)); - + file_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator)); file_menu->AddChild( MenuItem::Create(MenuItem::Type::kString, "Install Content...", std::bind(&EmulatorWindow::InstallContent, this))); + zar_menu->AddChild( + MenuItem::Create(MenuItem::Type::kString, "Create", + std::bind(&EmulatorWindow::CreateZarchive, this))); + zar_menu->AddChild( + MenuItem::Create(MenuItem::Type::kString, "Extract", + std::bind(&EmulatorWindow::ExtractZarchive, this))); + file_menu->AddChild(std::move(zar_menu)); #ifdef DEBUG + file_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator)); file_menu->AddChild( MenuItem::Create(MenuItem::Type::kString, "Close", std::bind(&EmulatorWindow::FileClose, this))); @@ -958,20 +967,158 @@ void EmulatorWindow::InstallContent() { paths = file_picker->selected_files(); } - if (!paths.empty()) { - for (auto path : paths) { - // Normalize the path and make absolute. - auto abs_path = std::filesystem::absolute(path); - auto result = emulator_->InstallContentPackage(abs_path); + if (paths.empty()) { + return; + } - if (result != X_STATUS_SUCCESS) { - XELOGE("Failed to install content! Error code: {:08X}", result); + for (auto path : paths) { + // Normalize the path and make absolute. + auto abs_path = std::filesystem::absolute(path); + auto result = emulator_->InstallContentPackage(abs_path); - xe::ui::ImGuiDialog::ShowMessageBox( - imgui_drawer_.get(), "Failed to install content!", - "Failed to install content!\n\nCheck xenia.log for technical " - "details."); - } + if (result != X_STATUS_SUCCESS) { + XELOGE("Failed to install content! Error code: {:08X}", result); + + xe::ui::ImGuiDialog::ShowMessageBox( + imgui_drawer_.get(), "Failed to install content!", + "Failed to install content!\n\nCheck xenia.log for technical " + "details."); + } + } +} + +void EmulatorWindow::ExtractZarchive() { + std::vector zarchive_files; + std::filesystem::path extract_dir; + + auto file_picker = xe::ui::FilePicker::Create(); + file_picker->set_mode(ui::FilePicker::Mode::kOpen); + file_picker->set_type(ui::FilePicker::Type::kFile); + file_picker->set_multi_selection(true); + file_picker->set_title("Select Zar Package"); + file_picker->set_extensions({ + {"Zarchive Files (*.zar)", "*.zar"}, + }); + + if (file_picker->Show(window_.get())) { + zarchive_files = file_picker->selected_files(); + } + + if (zarchive_files.empty()) { + return; + } + + file_picker->set_type(ui::FilePicker::Type::kDirectory); + file_picker->set_title("Select Directory to Extract"); + + if (file_picker->Show(window_.get())) { + extract_dir = file_picker->selected_files().front(); + } + + if (extract_dir.empty()) { + return; + } + + for (auto& zarchive_file_path : zarchive_files) { + // Normalize the path and make absolute. + auto abs_path = std::filesystem::absolute(zarchive_file_path); + std::filesystem::path abs_extract_dir; + + if (zarchive_files.size() > 1) { + abs_extract_dir = + std::filesystem::absolute((extract_dir / abs_path.stem())); + } else { + abs_extract_dir = std::filesystem::absolute(extract_dir); + } + + XELOGI("Extracting zar package: {}\n", + zarchive_file_path.filename().string()); + + auto result = emulator_->ExtractZarchivePackage(abs_path, abs_extract_dir); + + if (result != X_STATUS_SUCCESS) { + std::error_code ec; + + // delete incomplete output file + std::filesystem::remove(abs_extract_dir, ec); + + XELOGE("Failed to extract Zarchive package.", result); + + xe::ui::ImGuiDialog::ShowMessageBox( + imgui_drawer_.get(), "Failed to extract Zarchive package.", + "Failed to extract Zarchive package."); + } + } +} + +void EmulatorWindow::CreateZarchive() { + std::vector content_dirs; + std::filesystem::path zarchive_dir; + + auto file_picker = xe::ui::FilePicker::Create(); + file_picker->set_mode(ui::FilePicker::Mode::kOpen); + file_picker->set_type(ui::FilePicker::Type::kDirectory); + file_picker->set_multi_selection(true); + file_picker->set_title("Select Contents"); + + if (file_picker->Show(window_.get())) { + content_dirs = file_picker->selected_files(); + } + + if (content_dirs.empty()) { + return; + } + + if (content_dirs.size() == 1) { + file_picker->set_mode(ui::FilePicker::Mode::kSave); + file_picker->set_type(ui::FilePicker::Type::kFile); + file_picker->set_multi_selection(false); + file_picker->set_file_name(content_dirs.front().stem().string()); + file_picker->set_default_extension("zar"); + file_picker->set_title("Zarchive File"); + file_picker->set_extensions({ + {"Zarchive File (*.zar)", "*.zar"}, + }); + } else { + file_picker->set_title("Output Directory"); + } + + if (file_picker->Show(window_.get())) { + zarchive_dir = file_picker->selected_files().front(); + } + + if (zarchive_dir.empty()) { + return; + } + + for (auto& content_path : content_dirs) { + // Normalize the path and make absolute. + auto abs_content_dir = std::filesystem::absolute(content_path); + std::filesystem::path abs_zarchive_file; + + if (content_dirs.size() > 1) { + abs_zarchive_file = std::filesystem::absolute( + (zarchive_dir / abs_content_dir.stem()).replace_extension("zar")); + } else { + abs_zarchive_file = std::filesystem::absolute(zarchive_dir); + } + + XELOGI("Creating zar package: {}\n", abs_zarchive_file.filename().string()); + + auto result = + emulator_->CreateZarchivePackage(abs_content_dir, abs_zarchive_file); + + if (result != X_ERROR_SUCCESS) { + std::error_code ec; + + // delete incomplete output file + std::filesystem::remove(abs_zarchive_file, ec); + + XELOGE("Failed to create Zarchive package.", result); + + xe::ui::ImGuiDialog::ShowMessageBox(imgui_drawer_.get(), + "Failed to create Zarchive package.", + "Failed to create Zarchive package."); } } } diff --git a/src/xenia/app/emulator_window.h b/src/xenia/app/emulator_window.h index e1a4ca198..9f8e066a7 100644 --- a/src/xenia/app/emulator_window.h +++ b/src/xenia/app/emulator_window.h @@ -206,6 +206,8 @@ class EmulatorWindow { void FileOpen(); void FileClose(); void InstallContent(); + void ExtractZarchive(); + void CreateZarchive(); void ShowContentDirectory(); void CpuTimeScalarReset(); void CpuTimeScalarSetHalf(); diff --git a/src/xenia/emulator.cc b/src/xenia/emulator.cc index 5c0ead683..1fc04e34b 100644 --- a/src/xenia/emulator.cc +++ b/src/xenia/emulator.cc @@ -16,6 +16,8 @@ #include "third_party/fmt/include/fmt/format.h" #include "third_party/tabulate/single_include/tabulate/tabulate.hpp" #include "third_party/zarchive/include/zarchive/zarchivecommon.h" +#include "third_party/zarchive/include/zarchive/zarchivewriter.h" +#include "third_party/zarchive/src/sha_256.h" #include "xenia/apu/audio_system.h" #include "xenia/base/assert.h" #include "xenia/base/byte_stream.h" @@ -613,6 +615,124 @@ X_STATUS Emulator::InstallContentPackage(const std::filesystem::path& path) { installation_path); } +X_STATUS Emulator::ExtractZarchivePackage( + const std::filesystem::path& path, + const std::filesystem::path& extract_dir) { + std::unique_ptr device = + std::make_unique("", path); + if (!device->Initialize()) { + XELOGE("Failed to initialize device"); + return X_STATUS_INVALID_PARAMETER; + } + + if (std::filesystem::exists(extract_dir)) { + // TODO(Gliniak): Popup + // Do you want to overwrite already existing data? + } else { + std::error_code error_code; + std::filesystem::create_directories(extract_dir, error_code); + if (error_code) { + return error_code.value(); + } + } + + return vfs::VirtualFileSystem::ExtractContentFiles(device.get(), extract_dir); +} + +X_STATUS Emulator::CreateZarchivePackage( + const std::filesystem::path& inputDirectory, + const std::filesystem::path& outputFile) { + std::vector buffer; + buffer.resize(64 * 1024); + + std::error_code ec; + PackContext packContext; + packContext.outputFilePath = outputFile; + + ZArchiveWriter zWriter( + [](int32_t partIndex, void* ctx) { + PackContext* packContext = reinterpret_cast(ctx); + packContext->currentOutputFile = + std::ofstream(packContext->outputFilePath, std::ios::binary); + + if (!packContext->currentOutputFile.is_open()) { + XELOGI("Failed to create output file: {}\n", + packContext->outputFilePath.string()); + packContext->hasError = true; + } + }, + [](const void* data, size_t length, void* ctx) { + PackContext* packContext = reinterpret_cast(ctx); + packContext->currentOutputFile.write( + reinterpret_cast(data), length); + }, + &packContext); + + if (packContext.hasError) { + return X_STATUS_UNSUCCESSFUL; + } + + for (auto const& dirEntry : + std::filesystem::recursive_directory_iterator(inputDirectory)) { + std::filesystem::path pathEntry = + std::filesystem::relative(dirEntry.path(), inputDirectory, ec); + + if (ec) { + XELOGI("Failed to get relative path {}\n", pathEntry.string()); + return X_STATUS_UNSUCCESSFUL; + } + + if (dirEntry.is_directory()) { + if (!zWriter.MakeDir(pathEntry.generic_string().c_str(), false)) { + XELOGI("Failed to create directory {}\n", pathEntry.string()); + return X_STATUS_UNSUCCESSFUL; + } + } else if (dirEntry.is_regular_file()) { + // Don't pack itself to prevent infinite packing. + if (dirEntry == outputFile) { + continue; + } + + XELOGI("Adding file: {}\n", pathEntry.string()); + + if (!zWriter.StartNewFile(pathEntry.generic_string().c_str())) { + XELOGI("Failed to create archive file {}\n", pathEntry.string()); + return X_STATUS_UNSUCCESSFUL; + } + + std::filesystem::path file_to_pack_path = inputDirectory / pathEntry; + FILE* file = xe::filesystem::OpenFile(file_to_pack_path, "rb"); + + if (!file) { + XELOGI("Failed to open input file {}\n", pathEntry.string()); + return X_STATUS_UNSUCCESSFUL; + } + + const uint64_t file_size = std::filesystem::file_size(file_to_pack_path); + uint64_t total_bytes_read = 0; + + while (total_bytes_read < file_size) { + uint64_t bytes_read = + fread_s(buffer.data(), buffer.size(), 1, buffer.size(), file); + + total_bytes_read += bytes_read; + + zWriter.AppendData(buffer.data(), bytes_read); + } + + fclose(file); + } + + if (packContext.hasError) { + return X_STATUS_UNSUCCESSFUL; + } + } + + zWriter.Finalize(); + + return X_STATUS_SUCCESS; +} + void Emulator::Pause() { if (paused_) { return; diff --git a/src/xenia/emulator.h b/src/xenia/emulator.h index e2fcb3b7e..5ef971edc 100644 --- a/src/xenia/emulator.h +++ b/src/xenia/emulator.h @@ -226,6 +226,21 @@ class Emulator { // Extract content of package to content specific directory. X_STATUS InstallContentPackage(const std::filesystem::path& path); + // Extract content of zar package to desired directory. + X_STATUS Emulator::ExtractZarchivePackage( + const std::filesystem::path& path, + const std::filesystem::path& extract_dir); + + // Pack contents of a folder into a zar package. + X_STATUS CreateZarchivePackage(const std::filesystem::path& inputDirectory, + const std::filesystem::path& outputFile); + + struct PackContext { + std::filesystem::path outputFilePath; + std::ofstream currentOutputFile; + bool hasError{false}; + }; + void Pause(); void Resume(); bool is_paused() const { return paused_; } diff --git a/src/xenia/ui/file_picker.h b/src/xenia/ui/file_picker.h index c2e6e1174..1dfc91c37 100644 --- a/src/xenia/ui/file_picker.h +++ b/src/xenia/ui/file_picker.h @@ -50,6 +50,16 @@ class FilePicker { const std::string& title() const { return title_; } void set_title(std::string title) { title_ = std::move(title); } + const std::string& default_extension() const { return default_extension_; } + void set_default_extension(std::string default_extension) { + default_extension_ = std::move(default_extension); + } + + const std::string& file_name() const { return file_name_; } + void set_file_name(std::string file_name) { + file_name_ = std::move(file_name); + } + std::vector> extensions() const { return extensions_; } @@ -76,6 +86,8 @@ class FilePicker { Mode mode_; Type type_; std::string title_; + std::string default_extension_; + std::string file_name_; std::vector> extensions_; bool multi_selection_; diff --git a/src/xenia/ui/file_picker_win.cc b/src/xenia/ui/file_picker_win.cc index f6e43b597..722c4ca77 100644 --- a/src/xenia/ui/file_picker_win.cc +++ b/src/xenia/ui/file_picker_win.cc @@ -111,13 +111,37 @@ Win32FilePicker::Win32FilePicker() = default; Win32FilePicker::~Win32FilePicker() = default; bool Win32FilePicker::Show(Window* parent_window) { - // TODO(benvanik): FileSaveDialog. - assert_true(mode() == Mode::kOpen); + Microsoft::WRL::ComPtr file_dialog; - Microsoft::WRL::ComPtr file_dialog; - HRESULT hr = - CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_INPROC_SERVER, - IID_PPV_ARGS(&file_dialog)); + Microsoft::WRL::ComPtr open_dialog; + Microsoft::WRL::ComPtr save_dialog; + + HRESULT hr; + + if (mode() == Mode::kOpen) { + hr = CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_INPROC_SERVER, + IID_PPV_ARGS(&file_dialog)); + if (!SUCCEEDED(hr)) { + return false; + } + + hr = file_dialog.As(&open_dialog); + } else { + hr = CoCreateInstance(CLSID_FileSaveDialog, NULL, CLSCTX_INPROC_SERVER, + IID_PPV_ARGS(&file_dialog)); + if (!SUCCEEDED(hr)) { + return false; + } + + hr = file_dialog.As(&save_dialog); + } + + if (!SUCCEEDED(hr)) { + return false; + } + + hr = file_dialog->SetDefaultExtension( + (LPCWSTR)xe::to_utf16(default_extension()).c_str()); if (!SUCCEEDED(hr)) { return false; } @@ -127,6 +151,17 @@ bool Win32FilePicker::Show(Window* parent_window) { return false; } + hr = file_dialog->SetDefaultExtension( + (LPCWSTR)xe::to_utf16(default_extension()).c_str()); + if (!SUCCEEDED(hr)) { + return false; + } + + hr = file_dialog->SetFileName((LPCWSTR)xe::to_utf16(file_name()).c_str()); + if (!SUCCEEDED(hr)) { + return false; + } + DWORD flags; hr = file_dialog->GetOptions(&flags); if (!SUCCEEDED(hr)) { @@ -192,23 +227,44 @@ bool Win32FilePicker::Show(Window* parent_window) { return false; } - // Obtain the result once the user clicks the 'Open' button. - // The result is an IShellItem object. - Microsoft::WRL::ComPtr shell_items; - hr = file_dialog->GetResults(&shell_items); - if (!SUCCEEDED(hr)) { - return false; - } + if (mode() == Mode::kOpen) { + // Obtain the result once the user clicks the 'Open' button. + // The result is an IShellItem object. + Microsoft::WRL::ComPtr shell_items; + hr = open_dialog->GetResults(&shell_items); + if (!SUCCEEDED(hr)) { + return false; + } - std::vector selected_files; + std::vector selected_files; - DWORD items_count = 0; - shell_items->GetCount(&items_count); - // Iterate over selected files - for (DWORD i = 0; i < items_count; i++) { + DWORD items_count = 0; + shell_items->GetCount(&items_count); + // Iterate over selected files + for (DWORD i = 0; i < items_count; i++) { + Microsoft::WRL::ComPtr shell_item; + shell_items->GetItemAt(i, &shell_item); + // We are just going to print out the name of the file for sample sake. + PWSTR file_path = nullptr; + hr = shell_item->GetDisplayName(SIGDN_FILESYSPATH, &file_path); + if (!SUCCEEDED(hr)) { + return false; + } + selected_files.push_back(std::filesystem::path(file_path)); + CoTaskMemFree(file_path); + } + set_selected_files(selected_files); + } else { + // Obtain the result once the user clicks the 'Save' button. + // The result is an IShellItem object. Microsoft::WRL::ComPtr shell_item; - shell_items->GetItemAt(i, &shell_item); - // We are just going to print out the name of the file for sample sake. + hr = save_dialog->GetResult(&shell_item); + if (!SUCCEEDED(hr)) { + return false; + } + + std::vector selected_files; + PWSTR file_path = nullptr; hr = shell_item->GetDisplayName(SIGDN_FILESYSPATH, &file_path); if (!SUCCEEDED(hr)) { @@ -216,8 +272,9 @@ bool Win32FilePicker::Show(Window* parent_window) { } selected_files.push_back(std::filesystem::path(file_path)); CoTaskMemFree(file_path); + set_selected_files(selected_files); } - set_selected_files(selected_files); + return true; } diff --git a/third_party/zarchive.lua b/third_party/zarchive.lua index e2a7e803a..0abe4f724 100644 --- a/third_party/zarchive.lua +++ b/third_party/zarchive.lua @@ -18,4 +18,6 @@ project("zarchive") "zarchive/include/zarchive/zarchivewriter.h", "zarchive/src/zarchivereader.cpp", "zarchive/src/zarchivewriter.cpp", + "zarchive/src/sha_256.c", + "zarchive/src/sha_256.h", })