From 24c6ec5e6a37e1d7d637c44b6dcffdfd5af7126d Mon Sep 17 00:00:00 2001 From: PabloMK7 Date: Sun, 12 May 2024 20:17:06 +0200 Subject: [PATCH] Add Artic Base support (#105) * Add Artic Base support * Add Android support --- .../java/org/citra/citra_emu/NativeLibrary.kt | 66 +- .../fragments/HomeSettingsFragment.kt | 41 + src/android/app/src/main/jni/native.cpp | 10 +- .../app/src/main/res/drawable/ic_network.xml | 9 + .../app/src/main/res/values-es/strings.xml | 7 + .../app/src/main/res/values/strings.xml | 7 + src/citra_qt/configuration/config.cpp | 4 + src/citra_qt/main.cpp | 125 ++- src/citra_qt/main.h | 3 + src/citra_qt/main.ui | 6 + src/citra_qt/uisettings.h | 1 + src/common/static_lru_cache.h | 1 + src/core/CMakeLists.txt | 12 +- src/core/core.cpp | 14 +- src/core/core.h | 18 + src/core/file_sys/archive_artic.cpp | 535 ++++++++++ src/core/file_sys/archive_artic.h | 268 +++++ src/core/file_sys/archive_backend.cpp | 3 +- src/core/file_sys/archive_backend.h | 56 +- src/core/file_sys/archive_extsavedata.cpp | 229 ++++- src/core/file_sys/archive_extsavedata.h | 39 +- src/core/file_sys/archive_ncch.cpp | 23 +- src/core/file_sys/archive_ncch.h | 37 +- src/core/file_sys/archive_other_savedata.cpp | 22 +- src/core/file_sys/archive_other_savedata.h | 12 +- src/core/file_sys/archive_savedata.cpp | 10 +- src/core/file_sys/archive_savedata.h | 8 +- src/core/file_sys/archive_sdmc.cpp | 12 +- src/core/file_sys/archive_sdmc.h | 14 +- src/core/file_sys/archive_sdmcwriteonly.cpp | 9 +- src/core/file_sys/archive_sdmcwriteonly.h | 10 +- src/core/file_sys/archive_selfncch.cpp | 15 +- src/core/file_sys/archive_selfncch.h | 4 +- .../file_sys/archive_source_sd_savedata.cpp | 115 ++- .../file_sys/archive_source_sd_savedata.h | 29 +- src/core/file_sys/archive_systemsavedata.cpp | 3 +- src/core/file_sys/archive_systemsavedata.h | 4 +- src/core/file_sys/artic_cache.cpp | 235 +++++ src/core/file_sys/artic_cache.h | 154 +++ src/core/file_sys/directory_backend.h | 6 +- src/core/file_sys/disk_archive.cpp | 4 +- src/core/file_sys/disk_archive.h | 6 +- src/core/file_sys/file_backend.h | 4 +- src/core/file_sys/ivfc_archive.cpp | 15 +- src/core/file_sys/ivfc_archive.h | 20 +- src/core/file_sys/romfs_reader.cpp | 102 ++ src/core/file_sys/romfs_reader.h | 53 + src/core/file_sys/savedata_archive.cpp | 10 +- src/core/file_sys/savedata_archive.h | 10 +- src/core/file_sys/secure_value_backend.cpp | 74 ++ src/core/file_sys/secure_value_backend.h | 65 ++ .../file_sys/secure_value_backend_artic.cpp | 119 +++ .../file_sys/secure_value_backend_artic.h | 53 + src/core/hle/ipc_helpers.h | 5 + src/core/hle/kernel/hle_ipc.h | 5 + src/core/hle/service/am/am.cpp | 19 +- src/core/hle/service/am/am.h | 8 +- src/core/hle/service/apt/apt.cpp | 2 +- src/core/hle/service/boss/online_service.cpp | 2 +- src/core/hle/service/cecd/cecd.cpp | 20 +- src/core/hle/service/cfg/cfg.cpp | 4 +- src/core/hle/service/fs/archive.cpp | 139 ++- src/core/hle/service/fs/archive.h | 59 +- src/core/hle/service/fs/file.cpp | 149 ++- src/core/hle/service/fs/fs_user.cpp | 964 +++++++++++++++--- src/core/hle/service/fs/fs_user.h | 27 +- src/core/hle/service/http/http_c.cpp | 2 +- src/core/hle/service/news/news.cpp | 6 +- src/core/hle/service/ptm/ptm.cpp | 5 +- src/core/hle/service/soc/soc_u.cpp | 10 +- src/core/loader/artic.cpp | 564 ++++++++++ src/core/loader/artic.h | 135 +++ src/core/loader/loader.cpp | 30 + src/core/loader/loader.h | 10 + src/core/perf_stats.cpp | 4 + src/core/perf_stats.h | 46 + src/core/savestate.cpp | 12 + src/network/CMakeLists.txt | 5 + src/network/artic_base/artic_base_client.cpp | 735 +++++++++++++ src/network/artic_base/artic_base_client.h | 288 ++++++ src/network/artic_base/artic_base_common.h | 92 ++ src/network/socket_manager.cpp | 31 + src/network/socket_manager.h | 19 + 83 files changed, 5592 insertions(+), 516 deletions(-) create mode 100644 src/android/app/src/main/res/drawable/ic_network.xml create mode 100644 src/core/file_sys/archive_artic.cpp create mode 100644 src/core/file_sys/archive_artic.h create mode 100644 src/core/file_sys/artic_cache.cpp create mode 100644 src/core/file_sys/artic_cache.h create mode 100644 src/core/file_sys/secure_value_backend.cpp create mode 100644 src/core/file_sys/secure_value_backend.h create mode 100644 src/core/file_sys/secure_value_backend_artic.cpp create mode 100644 src/core/file_sys/secure_value_backend_artic.h create mode 100644 src/core/loader/artic.cpp create mode 100644 src/core/loader/artic.h create mode 100644 src/network/artic_base/artic_base_client.cpp create mode 100644 src/network/artic_base/artic_base_client.h create mode 100644 src/network/artic_base/artic_base_common.h create mode 100644 src/network/socket_manager.cpp create mode 100644 src/network/socket_manager.h diff --git a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt index bfbe658f8..84a62d898 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt @@ -183,13 +183,13 @@ object NativeLibrary { private var coreErrorAlertResult = false private val coreErrorAlertLock = Object() - private fun onCoreErrorImpl(title: String, message: String) { + private fun onCoreErrorImpl(title: String, message: String, canContinue: Boolean) { val emulationActivity = sEmulationActivity.get() if (emulationActivity == null) { Log.error("[NativeLibrary] EmulationActivity not present") return } - val fragment = CoreErrorDialogFragment.newInstance(title, message) + val fragment = CoreErrorDialogFragment.newInstance(title, message, canContinue) fragment.show(emulationActivity.supportFragmentManager, CoreErrorDialogFragment.TAG) } @@ -207,6 +207,7 @@ object NativeLibrary { } val title: String val message: String + val canContinue: Boolean when (error) { CoreError.ErrorSystemFiles -> { title = emulationActivity.getString(R.string.system_archive_not_found) @@ -214,16 +215,25 @@ object NativeLibrary { R.string.system_archive_not_found_message, details.ifEmpty { emulationActivity.getString(R.string.system_archive_general) } ) + canContinue = true } CoreError.ErrorSavestate -> { title = emulationActivity.getString(R.string.save_load_error) message = details + canContinue = true + } + + CoreError.ErrorArticDisconnected -> { + title = emulationActivity.getString(R.string.artic_base) + message = emulationActivity.getString(R.string.artic_server_comm_error) + canContinue = false } CoreError.ErrorUnknown -> { title = emulationActivity.getString(R.string.fatal_error) message = emulationActivity.getString(R.string.fatal_error_message) + canContinue = true } else -> { @@ -232,7 +242,7 @@ object NativeLibrary { } // Show the AlertDialog on the main thread. - emulationActivity.runOnUiThread(Runnable { onCoreErrorImpl(title, message) }) + emulationActivity.runOnUiThread(Runnable { onCoreErrorImpl(title, message, canContinue) }) // Wait for the lock to notify that it is complete. synchronized(coreErrorAlertLock) { @@ -346,6 +356,11 @@ object NativeLibrary { return } + if (resultCode == EmulationErrorDialogFragment.ShutdownRequested) { + emulationActivity.finish() + return + } + emulationActivity.runOnUiThread { EmulationErrorDialogFragment.newInstance(resultCode).showNow( emulationActivity.supportFragmentManager, @@ -361,16 +376,23 @@ object NativeLibrary { emulationActivity = requireActivity() as EmulationActivity var captionId = R.string.loader_error_invalid_format - if (requireArguments().getInt(RESULT_CODE) == ErrorLoader_ErrorEncrypted) { + val result = requireArguments().getInt(RESULT_CODE) + if (result == ErrorLoader_ErrorEncrypted) { captionId = R.string.loader_error_encrypted } + if (result == ErrorArticDisconnected) { + captionId = R.string.artic_base + } val alert = MaterialAlertDialogBuilder(requireContext()) .setTitle(captionId) .setMessage( Html.fromHtml( - CitraApplication.appContext.resources.getString(R.string.redump_games), - Html.FROM_HTML_MODE_LEGACY + if (result == ErrorArticDisconnected) + CitraApplication.appContext.resources.getString(R.string.artic_server_comm_error) + else + CitraApplication.appContext.resources.getString(R.string.redump_games), + Html.FROM_HTML_MODE_LEGACY ) ) .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> @@ -398,7 +420,10 @@ object NativeLibrary { const val ErrorLoader = 4 const val ErrorLoader_ErrorEncrypted = 5 const val ErrorLoader_ErrorInvalidFormat = 6 - const val ErrorSystemFiles = 7 + const val ErrorLoader_ErrorGBATitle = 7 + const val ErrorSystemFiles = 8 + const val ErrorSavestate = 9 + const val ErrorArticDisconnected = 10 const val ShutdownRequested = 11 const val ErrorUnknown = 12 @@ -619,6 +644,7 @@ object NativeLibrary { enum class CoreError { ErrorSystemFiles, ErrorSavestate, + ErrorArticDisconnected, ErrorUnknown } @@ -633,23 +659,33 @@ object NativeLibrary { } class CoreErrorDialogFragment : DialogFragment() { + private var userChosen = false override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val title = requireArguments().getString(TITLE) val message = requireArguments().getString(MESSAGE) - return MaterialAlertDialogBuilder(requireContext()) + val canContinue = requireArguments().getBoolean(CAN_CONTINUE) + val dialog = MaterialAlertDialogBuilder(requireContext()) .setTitle(title) .setMessage(message) - .setPositiveButton(R.string.continue_button) { _: DialogInterface?, _: Int -> + if (canContinue) { + dialog.setPositiveButton(R.string.continue_button) { _: DialogInterface?, _: Int -> coreErrorAlertResult = true + userChosen = true } - .setNegativeButton(R.string.abort_button) { _: DialogInterface?, _: Int -> - coreErrorAlertResult = false - }.show() + } + dialog.setNegativeButton(R.string.abort_button) { _: DialogInterface?, _: Int -> + coreErrorAlertResult = false + userChosen = true + } + return dialog.show() } override fun onDismiss(dialog: DialogInterface) { super.onDismiss(dialog) - coreErrorAlertResult = true + val canContinue = requireArguments().getBoolean(CAN_CONTINUE) + if (!userChosen) { + coreErrorAlertResult = canContinue + } synchronized(coreErrorAlertLock) { coreErrorAlertLock.notify() } } @@ -658,12 +694,14 @@ object NativeLibrary { const val TITLE = "title" const val MESSAGE = "message" + const val CAN_CONTINUE = "canContinue" - fun newInstance(title: String, message: String): CoreErrorDialogFragment { + fun newInstance(title: String, message: String, canContinue: Boolean): CoreErrorDialogFragment { val frag = CoreErrorDialogFragment() val args = Bundle() args.putString(TITLE, title) args.putString(MESSAGE, message) + args.putBoolean(CAN_CONTINUE, canContinue) frag.arguments = args return frag } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/HomeSettingsFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/HomeSettingsFragment.kt index b19fdd7b9..091616054 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/HomeSettingsFragment.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/HomeSettingsFragment.kt @@ -16,6 +16,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.updatePadding +import androidx.core.widget.doOnTextChanged import androidx.documentfile.provider.DocumentFile import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels @@ -23,14 +24,19 @@ import androidx.navigation.findNavController import androidx.navigation.fragment.findNavController import androidx.preference.PreferenceManager import androidx.recyclerview.widget.GridLayoutManager +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.transition.MaterialSharedAxis import org.citra.citra_emu.CitraApplication +import org.citra.citra_emu.HomeNavigationDirections import org.citra.citra_emu.R import org.citra.citra_emu.adapters.HomeSettingAdapter +import org.citra.citra_emu.databinding.DialogSoftwareKeyboardBinding import org.citra.citra_emu.databinding.FragmentHomeSettingsBinding import org.citra.citra_emu.features.settings.model.Settings +import org.citra.citra_emu.features.settings.model.StringSetting import org.citra.citra_emu.features.settings.ui.SettingsActivity import org.citra.citra_emu.features.settings.utils.SettingsFile +import org.citra.citra_emu.model.Game import org.citra.citra_emu.model.HomeSetting import org.citra.citra_emu.ui.main.MainActivity import org.citra.citra_emu.utils.GameHelper @@ -76,6 +82,41 @@ class HomeSettingsFragment : Fragment() { R.drawable.ic_settings, { SettingsActivity.launch(requireContext(), SettingsFile.FILE_NAME_CONFIG, "") } ), + HomeSetting( + R.string.artic_base_connect, + R.string.artic_base_connect_description, + R.drawable.ic_network, + { + val inflater = LayoutInflater.from(context) + val inputBinding = DialogSoftwareKeyboardBinding.inflate(inflater) + var textInputValue: String = "" + + inputBinding.editTextInput.setText(textInputValue) + inputBinding.editTextInput.doOnTextChanged { text, _, _, _ -> + textInputValue = text.toString() + } + + val dialog = context?.let { + MaterialAlertDialogBuilder(it) + .setView(inputBinding.root) + .setTitle(getString(R.string.artic_base_enter_address)) + .setPositiveButton(android.R.string.ok) { _, _ -> + if (textInputValue.isNotEmpty()) { + val menu = Game( + title = getString(R.string.artic_base), + path = "articbase://$textInputValue", + filename = "" + ) + val action = + HomeNavigationDirections.actionGlobalEmulationActivity(menu) + binding.root.findNavController().navigate(action) + } + } + .setNegativeButton(android.R.string.cancel) {_, _ -> } + .show() + } + } + ), HomeSetting( R.string.system_files, R.string.system_files_description, diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index 13dbc0f2c..d84562e1a 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -82,6 +82,7 @@ static jobject ToJavaCoreError(Core::System::ResultStatus result) { static const std::map CoreErrorNameMap{ {Core::System::ResultStatus::ErrorSystemFiles, "ErrorSystemFiles"}, {Core::System::ResultStatus::ErrorSavestate, "ErrorSavestate"}, + {Core::System::ResultStatus::ErrorArticDisconnected, "ErrorArticDisconnected"}, {Core::System::ResultStatus::ErrorUnknown, "ErrorUnknown"}, }; @@ -178,6 +179,7 @@ static Core::System::ResultStatus RunCitra(const std::string& filepath) { auto app_loader = Loader::GetLoader(filepath); if (app_loader) { app_loader->ReadProgramId(program_id); + system.RegisterAppLoaderEarly(app_loader); GameSettings::LoadOverrides(program_id); } system.ApplySettings(); @@ -231,6 +233,10 @@ static Core::System::ResultStatus RunCitra(const std::string& filepath) { InputManager::NDKMotionHandler()->DisableSensors(); if (!HandleCoreError(result, system.GetStatusDetails())) { // Frontend requests us to abort + // If the error was an Artic disconnect, return shutdown request. + if (result == Core::System::ResultStatus::ErrorArticDisconnected) { + return Core::System::ResultStatus::ShutdownRequested; + } return result; } InputManager::NDKMotionHandler()->EnableSensors(); @@ -314,7 +320,9 @@ void Java_org_citra_citra_1emu_NativeLibrary_doFrame([[maybe_unused]] JNIEnv* en if (stop_run || pause_emulation) { return; } - window->TryPresenting(); + if (window) { + window->TryPresenting(); + } } void JNICALL Java_org_citra_citra_1emu_NativeLibrary_initializeGpuDriver( diff --git a/src/android/app/src/main/res/drawable/ic_network.xml b/src/android/app/src/main/res/drawable/ic_network.xml new file mode 100644 index 000000000..91559b988 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_network.xml @@ -0,0 +1,9 @@ + + + diff --git a/src/android/app/src/main/res/values-es/strings.xml b/src/android/app/src/main/res/values-es/strings.xml index 7755c81cc..7c7ba9e53 100644 --- a/src/android/app/src/main/res/values-es/strings.xml +++ b/src/android/app/src/main/res/values-es/strings.xml @@ -657,4 +657,11 @@ Se esperan fallos gráficos temporales cuando ésta esté activado. Noviembre Diciembre + + Fallo de comunicación con el servidor Artic Base. La emulación se detendrá. + Artic Base + Conectar con Artic Base + Conectar con una consola real que esté ejecutando un servidor Artic Base + Introduce la dirección del servidor Artic Base + diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 781e78da1..6614e97f1 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -683,4 +683,11 @@ November December + + Failed to communicate with the Artic Base server. Emulation will stop. + Artic Base + Connect to a real console that is running an Artic Base server + Connect to Artic Base + Enter Artic Base server address + diff --git a/src/citra_qt/configuration/config.cpp b/src/citra_qt/configuration/config.cpp index 7ddbc7eb9..e54fcee87 100644 --- a/src/citra_qt/configuration/config.cpp +++ b/src/citra_qt/configuration/config.cpp @@ -636,6 +636,8 @@ void Config::ReadPathValues() { UISettings::values.game_dirs.append(game_dir); } } + UISettings::values.last_artic_base_addr = + ReadSetting(QStringLiteral("last_artic_base_addr"), QString{}).toString(); UISettings::values.recent_files = ReadSetting(QStringLiteral("recentFiles")).toStringList(); UISettings::values.language = ReadSetting(QStringLiteral("language"), QString{}).toString(); } @@ -1135,6 +1137,8 @@ void Config::SavePathValues() { WriteSetting(QStringLiteral("expanded"), game_dir.expanded, true); } qt_config->endArray(); + WriteSetting(QStringLiteral("last_artic_base_addr"), + UISettings::values.last_artic_base_addr, QString{}); WriteSetting(QStringLiteral("recentFiles"), UISettings::values.recent_files); WriteSetting(QStringLiteral("language"), UISettings::values.language, QString{}); } diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp index e724ea37b..efe2bc05b 100644 --- a/src/citra_qt/main.cpp +++ b/src/citra_qt/main.cpp @@ -381,6 +381,10 @@ void GMainWindow::InitializeWidgets() { progress_bar->hide(); statusBar()->addPermanentWidget(progress_bar); + artic_traffic_label = new QLabel(); + artic_traffic_label->setToolTip( + tr("Current Artic Base traffic speed. Higher values indicate bigger transfer loads.")); + emu_speed_label = new QLabel(); emu_speed_label->setToolTip(tr("Current emulation speed. Values higher or lower than 100% " "indicate emulation is running faster or slower than a 3DS.")); @@ -392,7 +396,8 @@ void GMainWindow::InitializeWidgets() { tr("Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For " "full-speed emulation this should be at most 16.67 ms.")); - for (auto& label : {emu_speed_label, game_fps_label, emu_frametime_label}) { + for (auto& label : + {artic_traffic_label, emu_speed_label, game_fps_label, emu_frametime_label}) { label->setVisible(false); label->setFrameStyle(QFrame::NoFrame); label->setContentsMargins(4, 0, 4, 0); @@ -866,6 +871,7 @@ void GMainWindow::ConnectMenuEvents() { // File connect_menu(ui->action_Load_File, &GMainWindow::OnMenuLoadFile); connect_menu(ui->action_Install_CIA, &GMainWindow::OnMenuInstallCIA); + connect_menu(ui->action_Connect_Artic, &GMainWindow::OnMenuConnectArticBase); for (u32 region = 0; region < Core::NUM_SYSTEM_TITLE_REGIONS; region++) { connect_menu(ui->menu_Boot_Home_Menu->actions().at(region), [this, region] { OnMenuBootHomeMenu(region); }); @@ -1203,6 +1209,11 @@ bool GMainWindow::LoadROM(const QString& filename) { tr("GBA Virtual Console ROMs are not supported by Citra.")); break; + case Core::System::ResultStatus::ErrorArticDisconnected: + QMessageBox::critical( + this, tr("Artic Base Server"), + tr("An error has occurred whilst communicating with the Artic Base Server.")); + break; default: QMessageBox::critical( this, tr("Error while loading ROM!"), @@ -1223,7 +1234,9 @@ bool GMainWindow::LoadROM(const QString& filename) { } void GMainWindow::BootGame(const QString& filename) { - if (filename.endsWith(QStringLiteral(".cia"))) { + const bool is_artic = filename.startsWith(QString::fromStdString("articbase://")); + + if (!is_artic && filename.endsWith(QStringLiteral(".cia"))) { const auto answer = QMessageBox::question( this, tr("CIA must be installed before usage"), tr("Before using this CIA, you must install it. Do you want to install it now?"), @@ -1235,8 +1248,12 @@ void GMainWindow::BootGame(const QString& filename) { return; } + show_artic_label = is_artic; + LOG_INFO(Frontend, "Citra starting..."); - StoreRecentFile(filename); // Put the filename on top of the list + if (!is_artic) { + StoreRecentFile(filename); // Put the filename on top of the list + } if (movie_record_on_start) { movie.PrepareForRecording(); @@ -1246,16 +1263,26 @@ void GMainWindow::BootGame(const QString& filename) { } const std::string path = filename.toStdString(); - const auto loader = Loader::GetLoader(path); + auto loader = Loader::GetLoader(path); u64 title_id{0}; - loader->ReadProgramId(title_id); + Loader::ResultStatus res = loader->ReadProgramId(title_id); + + if (Loader::ResultStatus::Success == res) { + // Load per game settings + const std::string name{is_artic ? "" : FileUtil::GetFilename(filename.toStdString())}; + const std::string config_file_name = + title_id == 0 ? name : fmt::format("{:016X}", title_id); + LOG_INFO(Frontend, "Loading per game config file for title {}", config_file_name); + Config per_game_config(config_file_name, Config::ConfigType::PerGameConfig); + } + + // Artic Base Server cannot accept a client multiple times, so multiple loaders are not + // possible. Instead register the app loader early and do not create it again on system load. + if (!loader->SupportsMultipleInstancesForSameFile()) { + system.RegisterAppLoaderEarly(loader); + } - // Load per game settings - const std::string name{FileUtil::GetFilename(filename.toStdString())}; - const std::string config_file_name = title_id == 0 ? name : fmt::format("{:016X}", title_id); - LOG_INFO(Frontend, "Loading per game config file for title {}", config_file_name); - Config per_game_config(config_file_name, Config::ConfigType::PerGameConfig); system.ApplySettings(); Settings::LogSettings(); @@ -1265,8 +1292,11 @@ void GMainWindow::BootGame(const QString& filename) { game_list->SaveInterfaceLayout(); config->Save(); - if (!LoadROM(filename)) + if (!LoadROM(filename)) { + render_window->ReleaseRenderTarget(); + secondary_window->ReleaseRenderTarget(); return; + } // Set everything up if (movie_record_on_start) { @@ -1420,6 +1450,8 @@ void GMainWindow::ShutdownGame() { // Disable status bar updates status_bar_update_timer.stop(); message_label_used_for_movie = false; + show_artic_label = false; + artic_traffic_label->setVisible(false); emu_speed_label->setVisible(false); game_fps_label->setVisible(false); emu_frametime_label->setVisible(false); @@ -1759,6 +1791,17 @@ void GMainWindow::OnMenuInstallCIA() { InstallCIA(filepaths); } +void GMainWindow::OnMenuConnectArticBase() { + bool ok = false; + auto res = QInputDialog::getText(this, tr("Connect to Artic Base"), + tr("Enter Artic Base server address:"), QLineEdit::Normal, + UISettings::values.last_artic_base_addr, &ok); + if (ok) { + UISettings::values.last_artic_base_addr = res; + BootGame(QString::fromStdString("articbase://").append(res)); + } +} + void GMainWindow::OnMenuBootHomeMenu(u32 region) { BootGame(QString::fromStdString(Core::GetHomeMenuNcchPath(region))); } @@ -2575,6 +2618,51 @@ void GMainWindow::UpdateStatusBar() { auto results = system.GetAndResetPerfStats(); + if (show_artic_label) { + const bool do_mb = results.artic_transmitted >= (1000.0 * 1000.0); + const double value = do_mb ? (results.artic_transmitted / (1000.0 * 1000.0)) + : (results.artic_transmitted / 1000.0); + static const std::array, 4> + perf_events = { + std::make_pair(Core::PerfStats::PerfArticEventBits::ARTIC_SHARED_EXT_DATA, + tr("(Accessing SharedExtData)")), + std::make_pair(Core::PerfStats::PerfArticEventBits::ARTIC_BOSS_EXT_DATA, + tr("(Accessing BossExtData)")), + std::make_pair(Core::PerfStats::PerfArticEventBits::ARTIC_EXT_DATA, + tr("(Accessing ExtData)")), + std::make_pair(Core::PerfStats::PerfArticEventBits::ARTIC_SAVE_DATA, + tr("(Accessing SaveData)")), + }; + + const QString unit = do_mb ? tr("MB/s") : tr("KB/s"); + QString event{}; + for (auto p : perf_events) { + if (results.artic_events.Get(p.first)) { + event = QString::fromStdString(" ") + p.second; + break; + } + } + + static const std::array label_color = {QStringLiteral("#ffffff"), QStringLiteral("#eed202"), + QStringLiteral("#ff3333")}; + + int style_index; + + if (value > 200.0) { + style_index = 2; + } else if (value > 125.0) { + style_index = 1; + } else { + style_index = 0; + } + const QString style_sheet = + QStringLiteral("QLabel { color: %0; }").arg(label_color[style_index]); + + artic_traffic_label->setText( + tr("Artic Base Traffic: %1 %2%3").arg(value, 0, 'f', 0).arg(unit).arg(event)); + artic_traffic_label->setStyleSheet(style_sheet); + } + if (Settings::values.frame_limit.GetValue() == 0) { emu_speed_label->setText(tr("Speed: %1%").arg(results.emulation_speed * 100.0, 0, 'f', 0)); } else { @@ -2585,6 +2673,9 @@ void GMainWindow::UpdateStatusBar() { game_fps_label->setText(tr("Game: %1 FPS").arg(results.game_fps, 0, 'f', 0)); emu_frametime_label->setText(tr("Frame: %1 ms").arg(results.frametime * 1000.0, 0, 'f', 2)); + if (show_artic_label) { + artic_traffic_label->setVisible(true); + } emu_speed_label->setVisible(true); game_fps_label->setVisible(true); emu_frametime_label->setVisible(true); @@ -2736,6 +2827,7 @@ void GMainWindow::OnCoreError(Core::System::ResultStatus result, std::string det QString title, message; QMessageBox::Icon error_severity_icon; + bool can_continue = true; if (result == Core::System::ResultStatus::ErrorSystemFiles) { const QString common_message = tr("%1 is missing. Please + @@ -222,6 +223,11 @@ Install CIA... + + + Connect to Artic Base... + + JPN diff --git a/src/citra_qt/uisettings.h b/src/citra_qt/uisettings.h index 6dedff0f0..5ac61e204 100644 --- a/src/citra_qt/uisettings.h +++ b/src/citra_qt/uisettings.h @@ -116,6 +116,7 @@ struct Values { bool game_dir_deprecated_deepscan; QVector game_dirs; QStringList recent_files; + QString last_artic_base_addr; QString language; QString theme; diff --git a/src/common/static_lru_cache.h b/src/common/static_lru_cache.h index b91f046a0..bd692e94e 100644 --- a/src/common/static_lru_cache.h +++ b/src/common/static_lru_cache.h @@ -14,6 +14,7 @@ //---------------------------------------------------------------------------// #pragma once +#include #include #include #include diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 62a9cde64..de4439ece 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -40,6 +40,8 @@ add_library(citra_core STATIC dumping/backend.h dumping/ffmpeg_backend.cpp dumping/ffmpeg_backend.h + file_sys/archive_artic.cpp + file_sys/archive_artic.h file_sys/archive_backend.cpp file_sys/archive_backend.h file_sys/archive_extsavedata.cpp @@ -60,6 +62,8 @@ add_library(citra_core STATIC file_sys/archive_source_sd_savedata.h file_sys/archive_systemsavedata.cpp file_sys/archive_systemsavedata.h + file_sys/artic_cache.cpp + file_sys/artic_cache.h file_sys/cia_common.h file_sys/cia_container.cpp file_sys/cia_container.h @@ -87,6 +91,10 @@ add_library(citra_core STATIC file_sys/romfs_reader.h file_sys/savedata_archive.cpp file_sys/savedata_archive.h + file_sys/secure_value_backend_artic.cpp + file_sys/secure_value_backend_artic.h + file_sys/secure_value_backend.cpp + file_sys/secure_value_backend.h file_sys/seed_db.cpp file_sys/seed_db.h file_sys/ticket.cpp @@ -445,6 +453,8 @@ add_library(citra_core STATIC hw/y2r.h loader/3dsx.cpp loader/3dsx.h + loader/artic.cpp + loader/artic.h loader/elf.cpp loader/elf.h loader/loader.cpp @@ -470,7 +480,7 @@ add_library(citra_core STATIC tracer/citrace.h tracer/recorder.cpp tracer/recorder.h -) + ) create_target_directory_groups(citra_core) diff --git a/src/core/core.cpp b/src/core/core.cpp index 3db61dfcd..9f4fcf406 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -256,7 +256,11 @@ System::ResultStatus System::SingleStep() { System::ResultStatus System::Load(Frontend::EmuWindow& emu_window, const std::string& filepath, Frontend::EmuWindow* secondary_window) { FileUtil::SetCurrentRomPath(filepath); - app_loader = Loader::GetLoader(filepath); + if (early_app_loader) { + app_loader = std::move(early_app_loader); + } else { + app_loader = Loader::GetLoader(filepath); + } if (!app_loader) { LOG_CRITICAL(Core, "Failed to obtain loader for {}!", filepath); return ResultStatus::ErrorGetLoader; @@ -286,6 +290,8 @@ System::ResultStatus System::Load(Frontend::EmuWindow& emu_window, const std::st return ResultStatus::ErrorLoader_ErrorInvalidFormat; case Loader::ResultStatus::ErrorGbaTitle: return ResultStatus::ErrorLoader_ErrorGbaTitle; + case Loader::ResultStatus::ErrorArtic: + return ResultStatus::ErrorArticDisconnected; default: return ResultStatus::ErrorSystemMode; } @@ -334,6 +340,8 @@ System::ResultStatus System::Load(Frontend::EmuWindow& emu_window, const std::st return ResultStatus::ErrorLoader_ErrorInvalidFormat; case Loader::ResultStatus::ErrorGbaTitle: return ResultStatus::ErrorLoader_ErrorGbaTitle; + case Loader::ResultStatus::ErrorArtic: + return ResultStatus::ErrorArticDisconnected; default: return ResultStatus::ErrorLoader; } @@ -691,6 +699,10 @@ void System::ApplySettings() { } } +void System::RegisterAppLoaderEarly(std::unique_ptr& loader) { + early_app_loader = std::move(loader); +} + template void System::serialize(Archive& ar, const unsigned int file_version) { diff --git a/src/core/core.h b/src/core/core.h index 50d31ff74..a395360ac 100644 --- a/src/core/core.h +++ b/src/core/core.h @@ -99,6 +99,7 @@ public: ///< Console ErrorSystemFiles, ///< Error in finding system files ErrorSavestate, ///< Error saving or loading + ErrorArticDisconnected, ///< Error when artic base disconnects ShutdownRequested, ///< Emulated program requested a system shutdown ErrorUnknown ///< Any other error }; @@ -169,6 +170,18 @@ public: [[nodiscard]] PerfStats::Results GetAndResetPerfStats(); + void ReportArticTraffic(u32 bytes) { + if (perf_stats) { + perf_stats->AddArticBaseTraffic(bytes); + } + } + + void ReportPerfArticEvent(PerfStats::PerfArticEventBits event, bool set) { + if (perf_stats) { + perf_stats->ReportPerfArticEvent(event, set); + } + } + [[nodiscard]] PerfStats::Results GetLastPerfStats(); /** @@ -346,6 +359,8 @@ public: /// Applies any changes to settings to this core instance. void ApplySettings(); + void RegisterAppLoaderEarly(std::unique_ptr& loader); + private: /** * Initialize the emulated system. @@ -366,6 +381,9 @@ private: /// AppLoader used to load the current executing application std::unique_ptr app_loader; + // Temporary app loader passed from frontend + std::unique_ptr early_app_loader; + /// ARM11 CPU core std::vector> cpu_cores; ARM_Interface* running_core = nullptr; diff --git a/src/core/file_sys/archive_artic.cpp b/src/core/file_sys/archive_artic.cpp new file mode 100644 index 000000000..36f395a23 --- /dev/null +++ b/src/core/file_sys/archive_artic.cpp @@ -0,0 +1,535 @@ +// Copyright 2024 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "archive_artic.h" + +namespace FileSys { + +std::vector ArticArchive::BuildFSPath(const Path& path) { + std::vector ret(sizeof(u32) * 2); + u32* raw_data = reinterpret_cast(ret.data()); + auto path_type = path.GetType(); + auto binary = path.AsBinary(); + raw_data[0] = static_cast(path_type); + raw_data[1] = static_cast(binary.size()); + if (!binary.empty()) { + ret.insert(ret.end(), binary.begin(), binary.end()); + } + + // The insert may have invalidated the pointer + raw_data = reinterpret_cast(ret.data()); + if (path_type != LowPathType::Binary && path_type != LowPathType::Invalid) { + if (path_type == LowPathType::Wchar) { + raw_data[1] += 2; + ret.push_back(0); + ret.push_back(0); + } else { + raw_data[1] += 1; + ret.push_back(0); + } + } + + return ret; +} + +Result ArticArchive::RespResult(const std::optional& resp) { + if (!resp.has_value() || !resp->Succeeded()) { + return ResultUnknown; + } + return Result(static_cast(resp->GetMethodResult())); +} + +ArticArchive::~ArticArchive() { + if (clear_cache_on_close) { + cache_provider->ClearAllCache(); + } + if (archive_handle != -1) { + auto req = client->NewRequest("FSUSER_CloseArchive"); + req.AddParameterS64(archive_handle); + client->Send(req); + if (report_artic_event != Core::PerfStats::PerfArticEventBits::NONE) { + client->ReportArticEvent(static_cast(report_artic_event)); + } + } +} + +ResultVal> ArticArchive::Open( + std::shared_ptr& client, Service::FS::ArchiveIdCode archive_id, + const Path& path, Core::PerfStats::PerfArticEventBits report_artic_event, + ArticCacheProvider& cache_provider, bool clear_cache_on_close) { + + auto req = client->NewRequest("FSUSER_OpenArchive"); + + req.AddParameterS32(static_cast(archive_id)); + auto path_buf = BuildFSPath(path); + req.AddParameterBuffer(path_buf.data(), path_buf.size()); + + auto resp = client->Send(req); + if (!resp.has_value() || !resp->Succeeded()) { + return ResultUnknown; + } + Result res(static_cast(resp->GetMethodResult())); + if (res.IsError()) + return res; + + auto handle_opt = resp->GetResponseS64(0); + if (!handle_opt.has_value()) { + return ResultUnknown; + } + + return std::make_unique(client, *handle_opt, report_artic_event, cache_provider, + path, clear_cache_on_close); +} + +void ArticArchive::Close() { + if (clear_cache_on_close) { + cache_provider->ClearAllCache(); + } + + auto req = client->NewRequest("FSUSER_CloseArchive"); + req.AddParameterS64(archive_handle); + if (RespResult(client->Send(req)).IsSuccess()) { + archive_handle = -1; + if (report_artic_event != Core::PerfStats::PerfArticEventBits::NONE) { + client->ReportArticEvent(static_cast(report_artic_event)); + } + } +} + +std::string ArticArchive::GetName() const { + return "ArticArchive"; +} + +ResultVal> ArticArchive::OpenFile(const Path& path, const Mode& mode, + u32 attributes) { + if (mode.create_flag) { + auto cache = cache_provider->ProvideCache( + client, cache_provider->PathsToVector(archive_path, path), false); + if (cache != nullptr) { + cache->Clear(); + } + } + auto req = client->NewRequest("FSUSER_OpenFile"); + + req.AddParameterS64(archive_handle); + auto path_buf = BuildFSPath(path); + req.AddParameterBuffer(path_buf.data(), path_buf.size()); + req.AddParameterU32(mode.hex); + req.AddParameterU32(attributes); + + auto resp = client->Send(req); + auto res = RespResult(resp); + if (res.IsError()) + return res; + + auto handle_opt = resp->GetResponseS32(0); + if (!handle_opt.has_value()) + return ResultUnknown; + + auto size_opt = resp->GetResponseU64(1); + if (size_opt.has_value()) { + auto cache = cache_provider->ProvideCache( + client, cache_provider->PathsToVector(archive_path, path), true); + if (cache != nullptr) { + cache->ForceSetSize(static_cast(*size_opt)); + } + } + + if (open_reporter->open_files++ == 0 && + report_artic_event != Core::PerfStats::PerfArticEventBits::NONE) { + client->ReportArticEvent(static_cast(report_artic_event) | (1ULL << 32)); + } + + return std::make_unique(client, *handle_opt, open_reporter, archive_path, + *cache_provider, path); +} + +Result ArticArchive::DeleteFile(const Path& path) const { + auto cache = cache_provider->ProvideCache( + client, cache_provider->PathsToVector(archive_path, path), false); + if (cache != nullptr) { + cache->Clear(); + } + + auto req = client->NewRequest("FSUSER_DeleteFile"); + + req.AddParameterS64(archive_handle); + auto path_buf = BuildFSPath(path); + req.AddParameterBuffer(path_buf.data(), path_buf.size()); + + return RespResult(client->Send(req)); +} + +Result ArticArchive::RenameFile(const Path& src_path, const Path& dest_path) const { + auto cache = cache_provider->ProvideCache( + client, cache_provider->PathsToVector(archive_path, src_path), false); + if (cache != nullptr) { + cache->Clear(); + } + cache = cache_provider->ProvideCache( + client, cache_provider->PathsToVector(archive_path, dest_path), false); + if (cache != nullptr) { + cache->Clear(); + } + + auto req = client->NewRequest("FSUSER_RenameFile"); + + req.AddParameterS64(archive_handle); + auto src_path_buf = BuildFSPath(src_path); + req.AddParameterBuffer(src_path_buf.data(), src_path_buf.size()); + req.AddParameterS64(archive_handle); + auto dest_path_buf = BuildFSPath(dest_path); + req.AddParameterBuffer(dest_path_buf.data(), dest_path_buf.size()); + + return RespResult(client->Send(req)); +} + +Result ArticArchive::DeleteDirectory(const Path& path) const { + cache_provider->ClearAllCache(); + + auto req = client->NewRequest("FSUSER_DeleteDirectory"); + + req.AddParameterS64(archive_handle); + auto path_buf = BuildFSPath(path); + req.AddParameterBuffer(path_buf.data(), path_buf.size()); + + return RespResult(client->Send(req)); +} + +Result ArticArchive::DeleteDirectoryRecursively(const Path& path) const { + cache_provider->ClearAllCache(); + + auto req = client->NewRequest("FSUSER_DeleteDirectoryRec"); + + req.AddParameterS64(archive_handle); + auto path_buf = BuildFSPath(path); + req.AddParameterBuffer(path_buf.data(), path_buf.size()); + + return RespResult(client->Send(req)); +} + +Result ArticArchive::CreateFile(const Path& path, u64 size, u32 attributes) const { + auto cache = cache_provider->ProvideCache( + client, cache_provider->PathsToVector(archive_path, path), false); + if (cache != nullptr) { + cache->Clear(); + } + + auto req = client->NewRequest("FSUSER_CreateFile"); + + req.AddParameterS64(archive_handle); + auto path_buf = BuildFSPath(path); + req.AddParameterBuffer(path_buf.data(), path_buf.size()); + req.AddParameterU32(attributes); + req.AddParameterU64(size); + + return RespResult(client->Send(req)); +} + +Result ArticArchive::CreateDirectory(const Path& path, u32 attributes) const { + auto req = client->NewRequest("FSUSER_CreateDirectory"); + + req.AddParameterS64(archive_handle); + auto path_buf = BuildFSPath(path); + req.AddParameterBuffer(path_buf.data(), path_buf.size()); + req.AddParameterU32(attributes); + + return RespResult(client->Send(req)); +} + +Result ArticArchive::RenameDirectory(const Path& src_path, const Path& dest_path) const { + cache_provider->ClearAllCache(); + + auto req = client->NewRequest("FSUSER_RenameDirectory"); + + req.AddParameterS64(archive_handle); + auto src_path_buf = BuildFSPath(src_path); + req.AddParameterBuffer(src_path_buf.data(), src_path_buf.size()); + req.AddParameterS64(archive_handle); + auto dest_path_buf = BuildFSPath(dest_path); + req.AddParameterBuffer(dest_path_buf.data(), dest_path_buf.size()); + + return RespResult(client->Send(req)); +} + +ResultVal> ArticArchive::OpenDirectory(const Path& path) { + auto req = client->NewRequest("FSUSER_OpenDirectory"); + + req.AddParameterS64(archive_handle); + auto path_buf = BuildFSPath(path); + req.AddParameterBuffer(path_buf.data(), path_buf.size()); + + auto resp = client->Send(req); + auto res = RespResult(resp); + if (res.IsError()) + return res; + + auto handle_opt = resp->GetResponseS32(0); + if (!handle_opt.has_value()) + return ResultUnknown; + + if (open_reporter->open_files++ == 0 && + report_artic_event != Core::PerfStats::PerfArticEventBits::NONE) { + client->ReportArticEvent(static_cast(report_artic_event) | (1ULL << 32)); + } + + return std::make_unique(client, *handle_opt, archive_path, + open_reporter); +} + +u64 ArticArchive::GetFreeBytes() const { + auto req = client->NewRequest("FSUSER_GetFreeBytes"); + + req.AddParameterS64(archive_handle); + + auto resp = client->Send(req); + auto res = RespResult(resp); + if (res.IsError()) // TODO(PabloMK7): Return error code and not u64 + return 0; + + auto free_bytes_opt = resp->GetResponseS64(0); + return free_bytes_opt.has_value() ? static_cast(*free_bytes_opt) : 0; +} + +Result ArticArchive::Control(u32 action, u8* input, size_t input_size, u8* output, + size_t output_size) { + auto req = client->NewRequest("FSUSER_ControlArchive"); + + req.AddParameterS64(archive_handle); + req.AddParameterU32(action); + req.AddParameterBuffer(input, input_size); + req.AddParameterU32(static_cast(output_size)); + + auto resp = client->Send(req); + auto res = RespResult(resp); + if (res.IsError()) + return res; + + auto output_buf = resp->GetResponseBuffer(0); + if (!output_buf.has_value()) + return res; + + if (output_buf->second != output_size) + return ResultUnknown; + + memcpy(output, output_buf->first, output_buf->second); + return res; +} + +Result ArticArchive::SetSaveDataSecureValue(u32 secure_value_slot, u64 secure_value, bool flush) { + auto req = client->NewRequest("FSUSER_SetSaveDataSecureValue"); + + req.AddParameterS64(archive_handle); + req.AddParameterU32(secure_value_slot); + req.AddParameterU64(secure_value); + req.AddParameterS8(flush != 0); + + return RespResult(client->Send(req)); +} + +ResultVal> ArticArchive::GetSaveDataSecureValue(u32 secure_value_slot) { + auto req = client->NewRequest("FSUSER_GetSaveDataSecureValue"); + + req.AddParameterS64(archive_handle); + req.AddParameterU32(secure_value_slot); + + auto resp = client->Send(req); + auto res = RespResult(resp); + if (res.IsError()) + return res; + + struct { + bool exists; + bool isGamecard; + u64 secure_value; + } secure_value_result; + static_assert(sizeof(secure_value_result) == 0x10); + + auto output_buf = resp->GetResponseBuffer(0); + if (!output_buf.has_value()) + return res; + + if (output_buf->second != sizeof(secure_value_result)) + return ResultUnknown; + + memcpy(&secure_value_result, output_buf->first, output_buf->second); + return std::make_tuple(secure_value_result.exists, secure_value_result.isGamecard, + secure_value_result.secure_value); +} + +void ArticArchive::OpenFileReporter::OnFileClosed() { + if (--open_files == 0 && report_artic_event != Core::PerfStats::PerfArticEventBits::NONE) { + client->ReportArticEvent(static_cast(report_artic_event)); + } +} + +void ArticArchive::OpenFileReporter::OnDirectoryClosed() { + if (--open_files == 0 && report_artic_event != Core::PerfStats::PerfArticEventBits::NONE) { + client->ReportArticEvent(static_cast(report_artic_event)); + } +} + +ArticFileBackend::~ArticFileBackend() { + if (file_handle != -1) { + auto req = client->NewRequest("FSFILE_Close"); + req.AddParameterS32(file_handle); + client->Send(req); + open_reporter->OnFileClosed(); + } +} + +ResultVal ArticFileBackend::Read(u64 offset, std::size_t length, u8* buffer) const { + auto cache = cache_provider->ProvideCache( + client, cache_provider->PathsToVector(archive_path, file_path), true); + + if (cache != nullptr) { + return cache->Read(file_handle, offset, length, buffer); + } + + auto req = client->NewRequest("FSFILE_Read"); + + req.AddParameterS32(file_handle); + req.AddParameterU64(offset); + req.AddParameterU32(static_cast(length)); + + auto resp = client->Send(req); + auto res = ArticArchive::RespResult(resp); + if (res.IsError()) + return res; + + auto read_buf = resp->GetResponseBuffer(0); + if (!read_buf || read_buf->second > length) { + return std::size_t(0); + } + + memcpy(buffer, read_buf->first, read_buf->second); + return read_buf->second; +} + +ResultVal ArticFileBackend::Write(u64 offset, std::size_t length, bool flush, + bool update_timestamp, const u8* buffer) { + u32 flags = (flush ? 1 : 0) | (update_timestamp ? (1 << 8) : 0); + auto cache = cache_provider->ProvideCache( + client, cache_provider->PathsToVector(archive_path, file_path), true); + if (cache != nullptr) { + return cache->Write(file_handle, offset, length, buffer, flags); + } else { + auto req = client->NewRequest("FSFILE_Write"); + + req.AddParameterS32(file_handle); + req.AddParameterU64(offset); + req.AddParameterU32(static_cast(length)); + req.AddParameterU32(flags); + req.AddParameterBuffer(buffer, length); + + auto resp = client->Send(req); + auto res = ArticArchive::RespResult(resp); + if (res.IsError()) + return res; + + auto writen_buf = resp->GetResponseS32(0); + if (!writen_buf) { + return std::size_t(0); + } + + return std::size_t(*writen_buf); + } +} + +u64 ArticFileBackend::GetSize() const { + auto cache = cache_provider->ProvideCache( + client, cache_provider->PathsToVector(archive_path, file_path), true); + if (cache != nullptr) { + auto res = cache->GetSize(file_handle); + if (res.Failed()) + return 0; + return res.Unwrap(); + } else { + + auto req = client->NewRequest("FSFILE_GetSize"); + + req.AddParameterS32(file_handle); + + auto resp = client->Send(req); + auto res = ArticArchive::RespResult(resp); + if (res.IsError()) + return 0; + + auto size_buf = resp->GetResponseS64(0); + if (!size_buf) { + return 0; + } + return *size_buf; + } +} + +bool ArticFileBackend::SetSize(u64 size) const { + auto req = client->NewRequest("FSFILE_SetSize"); + + req.AddParameterS32(file_handle); + req.AddParameterU64(size); + + return ArticArchive::RespResult(client->Send(req)).IsSuccess(); +} + +bool ArticFileBackend::Close() { + auto req = client->NewRequest("FSFILE_Close"); + req.AddParameterS32(file_handle); + bool ret = ArticArchive::RespResult(client->Send(req)).IsSuccess(); + if (ret) { + file_handle = -1; + open_reporter->OnFileClosed(); + } + return ret; +} + +void ArticFileBackend::Flush() const { + auto req = client->NewRequest("FSFILE_Flush"); + + req.AddParameterS32(file_handle); + + client->Send(req); +} + +ArticDirectoryBackend::~ArticDirectoryBackend() { + if (dir_handle != -1) { + auto req = client->NewRequest("FSDIR_Close"); + req.AddParameterS32(dir_handle); + client->Send(req); + open_reporter->OnDirectoryClosed(); + } +} + +u32 ArticDirectoryBackend::Read(const u32 count, Entry* entries) { + auto req = client->NewRequest("FSDIR_Read"); + + req.AddParameterS32(dir_handle); + req.AddParameterU32(count); + + auto resp = client->Send(req); + auto res = ArticArchive::RespResult(resp); + if (res.IsError()) + return 0; + + auto entry_buf = resp->GetResponseBuffer(0); + if (!entry_buf) { + return 0; + } + u32 ret_count = static_cast(entry_buf->second / sizeof(Entry)); + + memcpy(entries, entry_buf->first, ret_count * sizeof(Entry)); + return ret_count; +} + +bool ArticDirectoryBackend::Close() { + auto req = client->NewRequest("FSDIR_Close"); + req.AddParameterS32(dir_handle); + bool ret = ArticArchive::RespResult(client->Send(req)).IsSuccess(); + if (ret) { + dir_handle = -1; + open_reporter->OnDirectoryClosed(); + } + return ret; +} +} // namespace FileSys diff --git a/src/core/file_sys/archive_artic.h b/src/core/file_sys/archive_artic.h new file mode 100644 index 000000000..beb0f63b2 --- /dev/null +++ b/src/core/file_sys/archive_artic.h @@ -0,0 +1,268 @@ +// Copyright 2024 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "atomic" + +#include +#include "common/common_types.h" +#include "core/file_sys/archive_backend.h" +#include "core/file_sys/artic_cache.h" +#include "core/file_sys/directory_backend.h" +#include "core/file_sys/file_backend.h" +#include "core/hle/service/fs/archive.h" +#include "core/perf_stats.h" +#include "network/artic_base/artic_base_client.h" + +namespace FileSys { + +class ArticArchive : public ArchiveBackend { +public: + static std::vector BuildFSPath(const Path& path); + static Result RespResult(const std::optional& resp); + + explicit ArticArchive(std::shared_ptr& _client, s64 _archive_handle, + Core::PerfStats::PerfArticEventBits _report_artic_event, + ArticCacheProvider& _cache_provider, const Path& _archive_path, + bool _clear_cache_on_close) + : client(_client), archive_handle(_archive_handle), report_artic_event(_report_artic_event), + cache_provider(&_cache_provider), archive_path(_archive_path), + clear_cache_on_close(_clear_cache_on_close) { + open_reporter = std::make_shared(_client, _report_artic_event); + } + ~ArticArchive() override; + + static ResultVal> Open( + std::shared_ptr& client, Service::FS::ArchiveIdCode archive_id, + const Path& path, Core::PerfStats::PerfArticEventBits report_artic_event, + ArticCacheProvider& cache_provider, bool clear_cache_on_close); + + void Close() override; + + /** + * Get a descriptive name for the archive (e.g. "RomFS", "SaveData", etc.) + */ + std::string GetName() const override; + + /** + * Open a file specified by its path, using the specified mode + * @param path Path relative to the archive + * @param mode Mode to open the file with + * @return Opened file, or error code + */ + ResultVal> OpenFile(const Path& path, const Mode& mode, + u32 attributes) override; + + /** + * Delete a file specified by its path + * @param path Path relative to the archive + * @return Result of the operation + */ + Result DeleteFile(const Path& path) const override; + + /** + * Rename a File specified by its path + * @param src_path Source path relative to the archive + * @param dest_path Destination path relative to the archive + * @return Result of the operation + */ + Result RenameFile(const Path& src_path, const Path& dest_path) const override; + + /** + * Delete a directory specified by its path + * @param path Path relative to the archive + * @return Result of the operation + */ + Result DeleteDirectory(const Path& path) const override; + + /** + * Delete a directory specified by its path and anything under it + * @param path Path relative to the archive + * @return Result of the operation + */ + Result DeleteDirectoryRecursively(const Path& path) const override; + + /** + * Create a file specified by its path + * @param path Path relative to the Archive + * @param size The size of the new file, filled with zeroes + * @return Result of the operation + */ + Result CreateFile(const Path& path, u64 size, u32 attributes) const override; + + /** + * Create a directory specified by its path + * @param path Path relative to the archive + * @return Result of the operation + */ + Result CreateDirectory(const Path& path, u32 attributes) const override; + + /** + * Rename a Directory specified by its path + * @param src_path Source path relative to the archive + * @param dest_path Destination path relative to the archive + * @return Result of the operation + */ + Result RenameDirectory(const Path& src_path, const Path& dest_path) const override; + + /** + * Open a directory specified by its path + * @param path Path relative to the archive + * @return Opened directory, or error code + */ + ResultVal> OpenDirectory(const Path& path) override; + + /** + * Get the free space + * @return The number of free bytes in the archive + */ + u64 GetFreeBytes() const override; + + Result Control(u32 action, u8* input, size_t input_size, u8* output, + size_t output_size) override; + + Result SetSaveDataSecureValue(u32 secure_value_slot, u64 secure_value, bool flush) override; + + ResultVal> GetSaveDataSecureValue(u32 secure_value_slot) override; + + bool IsSlow() override { + return true; + } + + const Path& GetArchivePath() { + return archive_path; + } + +protected: + ArticArchive() = default; + +private: + friend class ArticFileBackend; + friend class ArticDirectoryBackend; + class OpenFileReporter { + public: + OpenFileReporter(const std::shared_ptr& cli, + Core::PerfStats::PerfArticEventBits _report_artic_event) + : client(cli), report_artic_event(_report_artic_event) {} + + void OnFileClosed(); + + void OnDirectoryClosed(); + + std::shared_ptr client; + Core::PerfStats::PerfArticEventBits report_artic_event = + Core::PerfStats::PerfArticEventBits::NONE; + std::atomic open_files = 0; + }; + + std::shared_ptr client; + s64 archive_handle; + std::shared_ptr open_reporter; + Core::PerfStats::PerfArticEventBits report_artic_event = + Core::PerfStats::PerfArticEventBits::NONE; + ArticCacheProvider* cache_provider = nullptr; + Path archive_path; + bool clear_cache_on_close; + + template + void serialize(Archive& ar, const unsigned int) { + ar& boost::serialization::base_object(*this); + ar& archive_handle; + } + friend class boost::serialization::access; +}; + +class ArticFileBackend : public FileBackend { +public: + explicit ArticFileBackend(std::shared_ptr& _client, + s32 _file_handle, + const std::shared_ptr& _open_reporter, + const Path& _archive_path, ArticCacheProvider& _cache_provider, + const Path& _file_path) + : client(_client), file_handle(_file_handle), open_reporter(_open_reporter), + archive_path(_archive_path), cache_provider(&_cache_provider), file_path(_file_path) {} + ~ArticFileBackend() override; + + ResultVal Read(u64 offset, std::size_t length, u8* buffer) const override; + + ResultVal Write(u64 offset, std::size_t length, bool flush, bool update_timestamp, + const u8* buffer) override; + + u64 GetSize() const override; + + bool SetSize(u64 size) const override; + + bool Close() override; + + void Flush() const override; + + bool AllowsCachedReads() const override { + return true; + } + + bool CacheReady(std::size_t file_offset, std::size_t length) override { + auto cache = cache_provider->ProvideCache( + client, cache_provider->PathsToVector(archive_path, file_path), true); + if (cache == nullptr) { + return false; + } + return cache->CacheReady(file_offset, length); + } + +protected: + ArticFileBackend() = default; + +private: + std::shared_ptr client; + s32 file_handle; + std::shared_ptr open_reporter; + Path archive_path; + ArticCacheProvider* cache_provider = nullptr; + Path file_path; + + template + void serialize(Archive& ar, const unsigned int) { + ar& boost::serialization::base_object(*this); + ar& file_handle; + } + friend class boost::serialization::access; +}; + +class ArticDirectoryBackend : public DirectoryBackend { +public: + explicit ArticDirectoryBackend( + std::shared_ptr& _client, s32 _dir_handle, + const Path& _archive_path, + const std::shared_ptr& _open_reporter) + : client(_client), dir_handle(_dir_handle), archive_path(_archive_path), + open_reporter(_open_reporter) {} + ~ArticDirectoryBackend() override; + + u32 Read(const u32 count, Entry* entries) override; + bool Close() override; + + bool IsSlow() override { + return true; + } + +protected: + ArticDirectoryBackend() = default; + +private: + std::shared_ptr client; + s32 dir_handle; + Path archive_path; + std::shared_ptr open_reporter; + + template + void serialize(Archive& ar, const unsigned int) { + ar& boost::serialization::base_object(*this); + ar& dir_handle; + } + friend class boost::serialization::access; +}; +} // namespace FileSys + +BOOST_CLASS_EXPORT_KEY(FileSys::ArticArchive) +BOOST_CLASS_EXPORT_KEY(FileSys::ArticFileBackend) +BOOST_CLASS_EXPORT_KEY(FileSys::ArticDirectoryBackend) \ No newline at end of file diff --git a/src/core/file_sys/archive_backend.cpp b/src/core/file_sys/archive_backend.cpp index bc4df30d0..1600171fa 100644 --- a/src/core/file_sys/archive_backend.cpp +++ b/src/core/file_sys/archive_backend.cpp @@ -105,8 +105,7 @@ std::vector Path::AsBinary() const { std::vector to_return(u16str.size() * 2); for (std::size_t i = 0; i < u16str.size(); ++i) { u16 tmp_char = u16str.at(i); - to_return[i * 2] = (tmp_char & 0xFF00) >> 8; - to_return[i * 2 + 1] = (tmp_char & 0x00FF); + *reinterpret_cast(to_return.data() + i * 2) = tmp_char; } return to_return; } diff --git a/src/core/file_sys/archive_backend.h b/src/core/file_sys/archive_backend.h index 7eb3893ba..7997c72ab 100644 --- a/src/core/file_sys/archive_backend.h +++ b/src/core/file_sys/archive_backend.h @@ -103,6 +103,7 @@ struct ArchiveFormatInfo { u8 duplicate_data; ///< Whether the archive should duplicate the data. }; static_assert(std::is_trivial_v, "ArchiveFormatInfo is not POD"); +static_assert(sizeof(ArchiveFormatInfo) == 16, "Invalid ArchiveFormatInfo size"); class ArchiveBackend : NonCopyable { public: @@ -119,8 +120,8 @@ public: * @param mode Mode to open the file with * @return Opened file, or error code */ - virtual ResultVal> OpenFile(const Path& path, - const Mode& mode) const = 0; + virtual ResultVal> OpenFile(const Path& path, const Mode& mode, + u32 attributes = 0) = 0; /** * Delete a file specified by its path @@ -157,14 +158,14 @@ public: * @param size The size of the new file, filled with zeroes * @return Result of the operation */ - virtual Result CreateFile(const Path& path, u64 size) const = 0; + virtual Result CreateFile(const Path& path, u64 size, u32 attributes = 0) const = 0; /** * Create a directory specified by its path * @param path Path relative to the archive * @return Result of the operation */ - virtual Result CreateDirectory(const Path& path) const = 0; + virtual Result CreateDirectory(const Path& path, u32 attributes = 0) const = 0; /** * Rename a Directory specified by its path @@ -179,7 +180,7 @@ public: * @param path Path relative to the archive * @return Opened directory, or error code */ - virtual ResultVal> OpenDirectory(const Path& path) const = 0; + virtual ResultVal> OpenDirectory(const Path& path) = 0; /** * Get the free space @@ -187,6 +188,20 @@ public: */ virtual u64 GetFreeBytes() const = 0; + /** + * Close the archive + */ + virtual void Close() {} + + virtual Result Control(u32 action, u8* input, size_t input_size, u8* output, + size_t output_size) { + LOG_WARNING(Service_FS, + "(STUBBED) called, archive={}, action={:08X}, input_size={:08X}, " + "output_size={:08X}", + GetName(), action, input_size, output_size); + return ResultSuccess; + } + u64 GetOpenDelayNs() { if (delay_generator != nullptr) { return delay_generator->GetOpenDelayNs(); @@ -196,6 +211,31 @@ public: return delay_generator->GetOpenDelayNs(); } + virtual Result SetSaveDataSecureValue(u32 secure_value_slot, u64 secure_value, bool flush) { + + // TODO: Generate and Save the Secure Value + + LOG_WARNING(Service_FS, + "(STUBBED) called, value=0x{:016x} secure_value_slot=0x{:04X} " + "flush={}", + secure_value, secure_value_slot, flush); + + return ResultSuccess; + } + + virtual ResultVal> GetSaveDataSecureValue(u32 secure_value_slot) { + + // TODO: Implement Secure Value Lookup & Generation + + LOG_WARNING(Service_FS, "(STUBBED) called secure_value_slot=0x{:08X}", secure_value_slot); + + return std::make_tuple(false, true, 0); + } + + virtual bool IsSlow() { + return false; + } + protected: std::unique_ptr delay_generator; @@ -232,7 +272,7 @@ public: * @return Result of the operation, 0 on success */ virtual Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, - u64 program_id) = 0; + u64 program_id, u32 directory_buckets, u32 file_buckets) = 0; /** * Retrieves the format info about the archive with the specified path @@ -242,6 +282,10 @@ public: */ virtual ResultVal GetFormatInfo(const Path& path, u64 program_id) const = 0; + virtual bool IsSlow() { + return false; + } + template void serialize(Archive& ar, const unsigned int) {} friend class boost::serialization::access; diff --git a/src/core/file_sys/archive_extsavedata.cpp b/src/core/file_sys/archive_extsavedata.cpp index cfa97fe7f..54fc25ee5 100644 --- a/src/core/file_sys/archive_extsavedata.cpp +++ b/src/core/file_sys/archive_extsavedata.cpp @@ -10,6 +10,7 @@ #include "common/common_types.h" #include "common/file_util.h" #include "common/logging/log.h" +#include "core/file_sys/archive_artic.h" #include "core/file_sys/archive_extsavedata.h" #include "core/file_sys/disk_archive.h" #include "core/file_sys/errors.h" @@ -37,7 +38,7 @@ public: return false; } - ResultVal Write(u64 offset, std::size_t length, bool flush, + ResultVal Write(u64 offset, std::size_t length, bool flush, bool update_timestamp, const u8* buffer) override { if (offset > size) { return ResultWriteBeyondEnd; @@ -49,7 +50,7 @@ public: length = size - offset; } - return DiskFile::Write(offset, length, flush, buffer); + return DiskFile::Write(offset, length, flush, update_timestamp, buffer); } private: @@ -100,8 +101,8 @@ public: return "ExtSaveDataArchive: " + mount_point; } - ResultVal> OpenFile(const Path& path, - const Mode& mode) const override { + ResultVal> OpenFile(const Path& path, const Mode& mode, + u32 attributes) override { LOG_DEBUG(Service_FS, "called path={} mode={:01X}", path.DebugStr(), mode.hex); const PathParser path_parser(path); @@ -234,69 +235,187 @@ Path ArchiveFactory_ExtSaveData::GetCorrectedPath(const Path& path) { return {binary_data}; } -ResultVal> ArchiveFactory_ExtSaveData::Open(const Path& path, - u64 program_id) { - const auto directory = type == ExtSaveDataType::Boss ? "boss/" : "user/"; - const auto fullpath = GetExtSaveDataPath(mount_point, GetCorrectedPath(path)) + directory; - if (!FileUtil::Exists(fullpath)) { - // TODO(Subv): Verify the archive behavior of SharedExtSaveData compared to ExtSaveData. - // ExtSaveData seems to return FS_NotFound (120) when the archive doesn't exist. - if (type != ExtSaveDataType::Shared) { - return ResultNotFoundInvalidState; - } else { - return ResultNotFormatted; - } +static Service::FS::ArchiveIdCode ExtSaveDataTypeToArchiveID(ExtSaveDataType type) { + switch (type) { + case FileSys::ExtSaveDataType::Normal: + return Service::FS::ArchiveIdCode::ExtSaveData; + case FileSys::ExtSaveDataType::Shared: + return Service::FS::ArchiveIdCode::SharedExtSaveData; + case FileSys::ExtSaveDataType::Boss: + return Service::FS::ArchiveIdCode::BossExtSaveData; + default: + return Service::FS::ArchiveIdCode::ExtSaveData; } - std::unique_ptr delay_generator = std::make_unique(); - return std::make_unique(fullpath, std::move(delay_generator)); } -Result ArchiveFactory_ExtSaveData::Format(const Path& path, - const FileSys::ArchiveFormatInfo& format_info, - u64 program_id) { - auto corrected_path = GetCorrectedPath(path); - - // These folders are always created with the ExtSaveData - std::string user_path = GetExtSaveDataPath(mount_point, corrected_path) + "user/"; - std::string boss_path = GetExtSaveDataPath(mount_point, corrected_path) + "boss/"; - FileUtil::CreateFullPath(user_path); - FileUtil::CreateFullPath(boss_path); - - // Write the format metadata - std::string metadata_path = GetExtSaveDataPath(mount_point, corrected_path) + "metadata"; - FileUtil::IOFile file(metadata_path, "wb"); - - if (!file.IsOpen()) { - // TODO(Subv): Find the correct error code - return ResultUnknown; +static Core::PerfStats::PerfArticEventBits ExtSaveDataTypeToPerfArtic(ExtSaveDataType type) { + switch (type) { + case FileSys::ExtSaveDataType::Normal: + return Core::PerfStats::PerfArticEventBits::ARTIC_EXT_DATA; + case FileSys::ExtSaveDataType::Shared: + return Core::PerfStats::PerfArticEventBits::ARTIC_SHARED_EXT_DATA; + case FileSys::ExtSaveDataType::Boss: + return Core::PerfStats::PerfArticEventBits::ARTIC_BOSS_EXT_DATA; + default: + return Core::PerfStats::PerfArticEventBits::ARTIC_EXT_DATA; } +} - file.WriteBytes(&format_info, sizeof(format_info)); - return ResultSuccess; +ResultVal> ArchiveFactory_ExtSaveData::Open(const Path& path, + u64 program_id) { + if (IsUsingArtic()) { + EnsureCacheCreated(); + return ArticArchive::Open(artic_client, ExtSaveDataTypeToArchiveID(type), path, + ExtSaveDataTypeToPerfArtic(type), *this, + type != FileSys::ExtSaveDataType::Normal); + } else { + const auto directory = type == ExtSaveDataType::Boss ? "boss/" : "user/"; + const auto fullpath = GetExtSaveDataPath(mount_point, GetCorrectedPath(path)) + directory; + if (!FileUtil::Exists(fullpath)) { + // TODO(Subv): Verify the archive behavior of SharedExtSaveData compared to ExtSaveData. + // ExtSaveData seems to return FS_NotFound (120) when the archive doesn't exist. + if (type != ExtSaveDataType::Shared) { + return ResultNotFoundInvalidState; + } else { + return ResultNotFormatted; + } + } + std::unique_ptr delay_generator = + std::make_unique(); + return std::make_unique(fullpath, std::move(delay_generator)); + } +} + +Result ArchiveFactory_ExtSaveData::FormatAsExtData(const Path& path, + const FileSys::ArchiveFormatInfo& format_info, + u8 unknown, u64 program_id, u64 total_size, + std::span icon) { + if (IsUsingArtic()) { + ExtSaveDataArchivePath path_data; + std::memcpy(&path_data, path.AsBinary().data(), sizeof(path_data)); + + Service::FS::ExtSaveDataInfo artic_extdata_path; + + artic_extdata_path.media_type = static_cast(path_data.media_type); + artic_extdata_path.unknown = unknown; + artic_extdata_path.save_id_low = path_data.save_low; + artic_extdata_path.save_id_high = path_data.save_high; + + auto req = artic_client->NewRequest("FSUSER_CreateExtSaveData"); + + req.AddParameterBuffer(&artic_extdata_path, sizeof(artic_extdata_path)); + req.AddParameterU32(format_info.number_directories); + req.AddParameterU32(format_info.number_files); + req.AddParameterU64(total_size); + req.AddParameterBuffer(icon.data(), icon.size()); + + return ArticArchive::RespResult(artic_client->Send(req)); + } else { + auto corrected_path = GetCorrectedPath(path); + + // These folders are always created with the ExtSaveData + std::string user_path = GetExtSaveDataPath(mount_point, corrected_path) + "user/"; + std::string boss_path = GetExtSaveDataPath(mount_point, corrected_path) + "boss/"; + FileUtil::CreateFullPath(user_path); + FileUtil::CreateFullPath(boss_path); + + // Write the format metadata + std::string metadata_path = GetExtSaveDataPath(mount_point, corrected_path) + "metadata"; + FileUtil::IOFile file(metadata_path, "wb"); + + if (!file.IsOpen()) { + // TODO(Subv): Find the correct error code + return ResultUnknown; + } + + file.WriteBytes(&format_info, sizeof(format_info)); + + FileUtil::IOFile icon_file(FileSys::GetExtSaveDataPath(GetMountPoint(), path) + "icon", + "wb"); + icon_file.WriteBytes(icon.data(), icon.size()); + + return ResultSuccess; + } +} + +Result ArchiveFactory_ExtSaveData::DeleteExtData(Service::FS::MediaType media_type, u8 unknown, + u32 high, u32 low) { + if (IsUsingArtic()) { + Service::FS::ExtSaveDataInfo artic_extdata_path; + + artic_extdata_path.media_type = static_cast(media_type); + artic_extdata_path.unknown = unknown; + artic_extdata_path.save_id_low = low; + artic_extdata_path.save_id_high = high; + + auto req = artic_client->NewRequest("FSUSER_DeleteExtSaveData"); + + req.AddParameterBuffer(&artic_extdata_path, sizeof(artic_extdata_path)); + + return ArticArchive::RespResult(artic_client->Send(req)); + } else { + // Construct the binary path to the archive first + FileSys::Path path = + FileSys::ConstructExtDataBinaryPath(static_cast(media_type), high, low); + + std::string media_type_directory; + if (media_type == Service::FS::MediaType::NAND) { + media_type_directory = FileUtil::GetUserPath(FileUtil::UserPath::NANDDir); + } else if (media_type == Service::FS::MediaType::SDMC) { + media_type_directory = FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir); + } else { + LOG_ERROR(Service_FS, "Unsupported media type {}", media_type); + return ResultUnknown; // TODO(Subv): Find the right error code + } + + // Delete all directories (/user, /boss) and the icon file. + std::string base_path = FileSys::GetExtDataContainerPath( + media_type_directory, media_type == Service::FS::MediaType::NAND); + std::string extsavedata_path = FileSys::GetExtSaveDataPath(base_path, path); + if (FileUtil::Exists(extsavedata_path) && !FileUtil::DeleteDirRecursively(extsavedata_path)) + return ResultUnknown; // TODO(Subv): Find the right error code + return ResultSuccess; + } } ResultVal ArchiveFactory_ExtSaveData::GetFormatInfo(const Path& path, u64 program_id) const { - std::string metadata_path = GetExtSaveDataPath(mount_point, path) + "metadata"; - FileUtil::IOFile file(metadata_path, "rb"); + if (IsUsingArtic()) { + auto req = artic_client->NewRequest("FSUSER_GetFormatInfo"); - if (!file.IsOpen()) { - LOG_ERROR(Service_FS, "Could not open metadata information for archive"); - // TODO(Subv): Verify error code - return ResultNotFormatted; + req.AddParameterS32(static_cast(ExtSaveDataTypeToArchiveID(type))); + auto path_artic = ArticArchive::BuildFSPath(path); + req.AddParameterBuffer(path_artic.data(), path_artic.size()); + + auto resp = artic_client->Send(req); + Result res = ArticArchive::RespResult(resp); + if (R_FAILED(res)) { + return res; + } + + auto info_buf = resp->GetResponseBuffer(0); + if (!info_buf.has_value() || info_buf->second != sizeof(ArchiveFormatInfo)) { + return ResultUnknown; + } + + ArchiveFormatInfo info; + memcpy(&info, info_buf->first, sizeof(info)); + return info; + } else { + std::string metadata_path = GetExtSaveDataPath(mount_point, path) + "metadata"; + FileUtil::IOFile file(metadata_path, "rb"); + + if (!file.IsOpen()) { + LOG_ERROR(Service_FS, "Could not open metadata information for archive"); + // TODO(Subv): Verify error code + return ResultNotFormatted; + } + + ArchiveFormatInfo info = {}; + file.ReadBytes(&info, sizeof(info)); + return info; } - - ArchiveFormatInfo info = {}; - file.ReadBytes(&info, sizeof(info)); - return info; } - -void ArchiveFactory_ExtSaveData::WriteIcon(const Path& path, std::span icon) { - std::string game_path = FileSys::GetExtSaveDataPath(GetMountPoint(), path); - FileUtil::IOFile icon_file(game_path + "icon", "wb"); - icon_file.WriteBytes(icon.data(), icon.size()); -} - } // namespace FileSys SERIALIZE_EXPORT_IMPL(FileSys::ExtSaveDataDelayGenerator) diff --git a/src/core/file_sys/archive_extsavedata.h b/src/core/file_sys/archive_extsavedata.h index 5093ecdc1..e89909ec3 100644 --- a/src/core/file_sys/archive_extsavedata.h +++ b/src/core/file_sys/archive_extsavedata.h @@ -11,7 +11,10 @@ #include #include "common/common_types.h" #include "core/file_sys/archive_backend.h" +#include "core/file_sys/artic_cache.h" #include "core/hle/result.h" +#include "core/hle/service/fs/archive.h" +#include "network/artic_base/artic_base_client.h" namespace FileSys { @@ -22,7 +25,7 @@ enum class ExtSaveDataType { }; /// File system interface to the ExtSaveData archive -class ArchiveFactory_ExtSaveData final : public ArchiveFactory { +class ArchiveFactory_ExtSaveData final : public ArchiveFactory, public ArticCacheProvider { public: ArchiveFactory_ExtSaveData(const std::string& mount_point, ExtSaveDataType type_); @@ -31,21 +34,34 @@ public: } ResultVal> Open(const Path& path, u64 program_id) override; - Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, - u64 program_id) override; + ResultVal GetFormatInfo(const Path& path, u64 program_id) const override; + bool IsSlow() override { + return IsUsingArtic(); + } + const std::string& GetMountPoint() const { return mount_point; } - /** - * Writes the SMDH icon of the ExtSaveData to file - * @param path Path of this ExtSaveData - * @param icon_data Binary data of the icon - * @param icon_size Size of the icon data - */ - void WriteIcon(const Path& path, std::span icon); + Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, u64 program_id, + u32 directory_buckets, u32 file_buckets) override { + return UnimplementedFunction(ErrorModule::FS); + }; + + Result FormatAsExtData(const Path& path, const FileSys::ArchiveFormatInfo& format_info, + u8 unknown, u64 program_id, u64 total_size, std::span icon); + + Result DeleteExtData(Service::FS::MediaType media_type, u8 unknown, u32 high, u32 low); + + void RegisterArtic(std::shared_ptr& client) { + artic_client = client; + } + + bool IsUsingArtic() const { + return artic_client.get() != nullptr; + } private: /// Type of ext save data archive being accessed. @@ -61,10 +77,13 @@ private: /// Returns a path with the correct SaveIdHigh value for Shared extdata paths. Path GetCorrectedPath(const Path& path); + std::shared_ptr artic_client = nullptr; + ArchiveFactory_ExtSaveData() = default; template void serialize(Archive& ar, const unsigned int) { ar& boost::serialization::base_object(*this); + ar& boost::serialization::base_object(*this); ar& type; ar& mount_point; } diff --git a/src/core/file_sys/archive_ncch.cpp b/src/core/file_sys/archive_ncch.cpp index 4b3a478e2..54d4639c5 100644 --- a/src/core/file_sys/archive_ncch.cpp +++ b/src/core/file_sys/archive_ncch.cpp @@ -15,6 +15,7 @@ #include "common/string_util.h" #include "common/swap.h" #include "core/core.h" +#include "core/file_sys/archive_artic.h" #include "core/file_sys/archive_ncch.h" #include "core/file_sys/errors.h" #include "core/file_sys/ivfc_archive.h" @@ -69,8 +70,9 @@ Path MakeNCCHFilePath(NCCHFileOpenType open_type, u32 content_index, NCCHFilePat return FileSys::Path(std::move(file)); } -ResultVal> NCCHArchive::OpenFile(const Path& path, - const Mode& mode) const { +ResultVal> NCCHArchive::OpenFile(const Path& path, const Mode& mode, + u32 attributes) { + if (path.GetType() != LowPathType::Binary) { LOG_ERROR(Service_FS, "Path need to be Binary"); return ResultInvalidPath; @@ -207,14 +209,14 @@ Result NCCHArchive::DeleteDirectoryRecursively(const Path& path) const { return ResultUnknown; } -Result NCCHArchive::CreateFile(const Path& path, u64 size) const { +Result NCCHArchive::CreateFile(const Path& path, u64 size, u32 attributes) const { LOG_CRITICAL(Service_FS, "Attempted to create a file in an NCCH archive ({}).", GetName()); // TODO: Verify error code return Result(ErrorDescription::NotAuthorized, ErrorModule::FS, ErrorSummary::NotSupported, ErrorLevel::Permanent); } -Result NCCHArchive::CreateDirectory(const Path& path) const { +Result NCCHArchive::CreateDirectory(const Path& path, u32 attributes) const { LOG_CRITICAL(Service_FS, "Attempted to create a directory in an NCCH archive ({}).", GetName()); // TODO(wwylele): Use correct error code return ResultUnknown; @@ -226,7 +228,7 @@ Result NCCHArchive::RenameDirectory(const Path& src_path, const Path& dest_path) return ResultUnknown; } -ResultVal> NCCHArchive::OpenDirectory(const Path& path) const { +ResultVal> NCCHArchive::OpenDirectory(const Path& path) { LOG_CRITICAL(Service_FS, "Attempted to open a directory within an NCCH archive ({}).", GetName().c_str()); // TODO(shinyquagsire23): Use correct error code @@ -255,7 +257,7 @@ ResultVal NCCHFile::Read(const u64 offset, const std::size_t length } ResultVal NCCHFile::Write(const u64 offset, const std::size_t length, const bool flush, - const u8* buffer) { + const bool update_timestamp, const u8* buffer) { LOG_ERROR(Service_FS, "Attempted to write to NCCH file"); // TODO(shinyquagsire23): Find error code return 0ULL; @@ -274,6 +276,13 @@ ArchiveFactory_NCCH::ArchiveFactory_NCCH() {} ResultVal> ArchiveFactory_NCCH::Open(const Path& path, u64 program_id) { + + if (IsUsingArtic()) { + EnsureCacheCreated(); + return ArticArchive::Open(artic_client, Service::FS::ArchiveIdCode::NCCH, path, + Core::PerfStats::PerfArticEventBits::NONE, *this, false); + } + if (path.GetType() != LowPathType::Binary) { LOG_ERROR(Service_FS, "Path need to be Binary"); return ResultInvalidPath; @@ -293,7 +302,7 @@ ResultVal> ArchiveFactory_NCCH::Open(const Path& } Result ArchiveFactory_NCCH::Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, - u64 program_id) { + u64 program_id, u32 directory_buckets, u32 file_buckets) { LOG_ERROR(Service_FS, "Attempted to format a NCCH archive."); // TODO: Verify error code return Result(ErrorDescription::NotAuthorized, ErrorModule::FS, ErrorSummary::NotSupported, diff --git a/src/core/file_sys/archive_ncch.h b/src/core/file_sys/archive_ncch.h index 3e22ef02a..725752f19 100644 --- a/src/core/file_sys/archive_ncch.h +++ b/src/core/file_sys/archive_ncch.h @@ -11,8 +11,10 @@ #include #include #include "core/file_sys/archive_backend.h" +#include "core/file_sys/artic_cache.h" #include "core/file_sys/file_backend.h" #include "core/hle/result.h" +#include "network/artic_base/artic_base_client.h" namespace Service::FS { enum class MediaType : u32; @@ -48,16 +50,16 @@ public: return "NCCHArchive"; } - ResultVal> OpenFile(const Path& path, - const Mode& mode) const override; + ResultVal> OpenFile(const Path& path, const Mode& mode, + u32 attributes) override; Result DeleteFile(const Path& path) const override; Result RenameFile(const Path& src_path, const Path& dest_path) const override; Result DeleteDirectory(const Path& path) const override; Result DeleteDirectoryRecursively(const Path& path) const override; - Result CreateFile(const Path& path, u64 size) const override; - Result CreateDirectory(const Path& path) const override; + Result CreateFile(const Path& path, u64 size, u32 attributes) const override; + Result CreateDirectory(const Path& path, u32 attributes) const override; Result RenameDirectory(const Path& src_path, const Path& dest_path) const override; - ResultVal> OpenDirectory(const Path& path) const override; + ResultVal> OpenDirectory(const Path& path) override; u64 GetFreeBytes() const override; protected: @@ -82,11 +84,11 @@ public: explicit NCCHFile(std::vector buffer, std::unique_ptr delay_generator_); ResultVal Read(u64 offset, std::size_t length, u8* buffer) const override; - ResultVal Write(u64 offset, std::size_t length, bool flush, + ResultVal Write(u64 offset, std::size_t length, bool flush, bool update_timestamp, const u8* buffer) override; u64 GetSize() const override; bool SetSize(u64 size) const override; - bool Close() const override { + bool Close() override { return false; } void Flush() const override {} @@ -105,7 +107,7 @@ private: }; /// File system interface to the NCCH archive -class ArchiveFactory_NCCH final : public ArchiveFactory { +class ArchiveFactory_NCCH final : public ArchiveFactory, public ArticCacheProvider { public: explicit ArchiveFactory_NCCH(); @@ -114,14 +116,29 @@ public: } ResultVal> Open(const Path& path, u64 program_id) override; - Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, - u64 program_id) override; + Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, u64 program_id, + u32 directory_buckets, u32 file_buckets) override; ResultVal GetFormatInfo(const Path& path, u64 program_id) const override; + bool IsSlow() override { + return IsUsingArtic(); + } + + void RegisterArtic(std::shared_ptr& client) { + artic_client = client; + } + + bool IsUsingArtic() const { + return artic_client.get() != nullptr; + } + private: + std::shared_ptr artic_client = nullptr; + template void serialize(Archive& ar, const unsigned int) { ar& boost::serialization::base_object(*this); + ar& boost::serialization::base_object(*this); } friend class boost::serialization::access; }; diff --git a/src/core/file_sys/archive_other_savedata.cpp b/src/core/file_sys/archive_other_savedata.cpp index 3944fce6f..c6aaaff0c 100644 --- a/src/core/file_sys/archive_other_savedata.cpp +++ b/src/core/file_sys/archive_other_savedata.cpp @@ -75,12 +75,14 @@ ResultVal> ArchiveFactory_OtherSaveDataPermitted return ResultGamecardNotInserted; } - return sd_savedata_source->Open(program_id); + return sd_savedata_source->Open(Service::FS::ArchiveIdCode::OtherSaveDataPermitted, path, + program_id); } Result ArchiveFactory_OtherSaveDataPermitted::Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, - u64 program_id) { + u64 program_id, u32 directory_buckets, + u32 file_buckets) { LOG_ERROR(Service_FS, "Attempted to format a OtherSaveDataPermitted archive."); return ResultInvalidPath; } @@ -96,7 +98,8 @@ ResultVal ArchiveFactory_OtherSaveDataPermitted::GetFormatInf return ResultGamecardNotInserted; } - return sd_savedata_source->GetFormatInfo(program_id); + return sd_savedata_source->GetFormatInfo( + program_id, Service::FS::ArchiveIdCode::OtherSaveDataPermitted, path); } ArchiveFactory_OtherSaveDataGeneral::ArchiveFactory_OtherSaveDataGeneral( @@ -114,12 +117,14 @@ ResultVal> ArchiveFactory_OtherSaveDataGeneral:: return ResultGamecardNotInserted; } - return sd_savedata_source->Open(program_id); + return sd_savedata_source->Open(Service::FS::ArchiveIdCode::OtherSaveDataGeneral, path, + program_id); } Result ArchiveFactory_OtherSaveDataGeneral::Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, - u64 /*client_program_id*/) { + u64 /*client_program_id*/, u32 directory_buckets, + u32 file_buckets) { MediaType media_type; u64 program_id; CASCADE_RESULT(std::tie(media_type, program_id), ParsePathGeneral(path)); @@ -129,7 +134,9 @@ Result ArchiveFactory_OtherSaveDataGeneral::Format(const Path& path, return ResultGamecardNotInserted; } - return sd_savedata_source->Format(program_id, format_info); + return sd_savedata_source->Format(program_id, format_info, + Service::FS::ArchiveIdCode::OtherSaveDataPermitted, path, + directory_buckets, file_buckets); } ResultVal ArchiveFactory_OtherSaveDataGeneral::GetFormatInfo( @@ -143,7 +150,8 @@ ResultVal ArchiveFactory_OtherSaveDataGeneral::GetFormatInfo( return ResultGamecardNotInserted; } - return sd_savedata_source->GetFormatInfo(program_id); + return sd_savedata_source->GetFormatInfo( + program_id, Service::FS::ArchiveIdCode::OtherSaveDataPermitted, path); } } // namespace FileSys diff --git a/src/core/file_sys/archive_other_savedata.h b/src/core/file_sys/archive_other_savedata.h index c9e720a8d..fffcb5481 100644 --- a/src/core/file_sys/archive_other_savedata.h +++ b/src/core/file_sys/archive_other_savedata.h @@ -22,10 +22,14 @@ public: } ResultVal> Open(const Path& path, u64 program_id) override; - Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, - u64 program_id) override; + Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, u64 program_id, + u32 directory_buckets, u32 file_buckets) override; ResultVal GetFormatInfo(const Path& path, u64 program_id) const override; + bool IsSlow() override { + return sd_savedata_source->IsUsingArtic(); + } + private: std::shared_ptr sd_savedata_source; @@ -49,8 +53,8 @@ public: } ResultVal> Open(const Path& path, u64 program_id) override; - Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, - u64 program_id) override; + Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, u64 program_id, + u32 directory_buckets, u32 file_buckets) override; ResultVal GetFormatInfo(const Path& path, u64 program_id) const override; private: diff --git a/src/core/file_sys/archive_savedata.cpp b/src/core/file_sys/archive_savedata.cpp index a5e767b14..aa06f9544 100644 --- a/src/core/file_sys/archive_savedata.cpp +++ b/src/core/file_sys/archive_savedata.cpp @@ -18,18 +18,20 @@ ArchiveFactory_SaveData::ArchiveFactory_SaveData( ResultVal> ArchiveFactory_SaveData::Open(const Path& path, u64 program_id) { - return sd_savedata_source->Open(program_id); + return sd_savedata_source->Open(Service::FS::ArchiveIdCode::SaveData, path, program_id); } Result ArchiveFactory_SaveData::Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, - u64 program_id) { - return sd_savedata_source->Format(program_id, format_info); + u64 program_id, u32 directory_buckets, u32 file_buckets) { + return sd_savedata_source->Format(program_id, format_info, Service::FS::ArchiveIdCode::SaveData, + path, directory_buckets, file_buckets); } ResultVal ArchiveFactory_SaveData::GetFormatInfo(const Path& path, u64 program_id) const { - return sd_savedata_source->GetFormatInfo(program_id); + return sd_savedata_source->GetFormatInfo(program_id, Service::FS::ArchiveIdCode::SaveData, + path); } } // namespace FileSys diff --git a/src/core/file_sys/archive_savedata.h b/src/core/file_sys/archive_savedata.h index dba6d5cdb..4b018f214 100644 --- a/src/core/file_sys/archive_savedata.h +++ b/src/core/file_sys/archive_savedata.h @@ -20,11 +20,15 @@ public: } ResultVal> Open(const Path& path, u64 program_id) override; - Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, - u64 program_id) override; + Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, u64 program_id, + u32 directory_buckets, u32 file_buckets) override; ResultVal GetFormatInfo(const Path& path, u64 program_id) const override; + bool IsSlow() override { + return sd_savedata_source->IsUsingArtic(); + } + private: std::shared_ptr sd_savedata_source; diff --git a/src/core/file_sys/archive_sdmc.cpp b/src/core/file_sys/archive_sdmc.cpp index 8ba01e7b8..91b1fa680 100644 --- a/src/core/file_sys/archive_sdmc.cpp +++ b/src/core/file_sys/archive_sdmc.cpp @@ -43,8 +43,8 @@ public: SERIALIZE_DELAY_GENERATOR }; -ResultVal> SDMCArchive::OpenFile(const Path& path, - const Mode& mode) const { +ResultVal> SDMCArchive::OpenFile(const Path& path, const Mode& mode, + u32 attributes) { Mode modified_mode; modified_mode.hex = mode.hex; @@ -222,7 +222,7 @@ Result SDMCArchive::DeleteDirectoryRecursively(const Path& path) const { path, mount_point, [](const std::string& p) { return FileUtil::DeleteDirRecursively(p); }); } -Result SDMCArchive::CreateFile(const FileSys::Path& path, u64 size) const { +Result SDMCArchive::CreateFile(const FileSys::Path& path, u64 size, u32 attributes) const { const PathParser path_parser(path); if (!path_parser.IsValid()) { @@ -267,7 +267,7 @@ Result SDMCArchive::CreateFile(const FileSys::Path& path, u64 size) const { ErrorLevel::Info); } -Result SDMCArchive::CreateDirectory(const Path& path) const { +Result SDMCArchive::CreateDirectory(const Path& path, u32 attributes) const { const PathParser path_parser(path); if (!path_parser.IsValid()) { @@ -331,7 +331,7 @@ Result SDMCArchive::RenameDirectory(const Path& src_path, const Path& dest_path) ErrorSummary::NothingHappened, ErrorLevel::Status); } -ResultVal> SDMCArchive::OpenDirectory(const Path& path) const { +ResultVal> SDMCArchive::OpenDirectory(const Path& path) { const PathParser path_parser(path); if (!path_parser.IsValid()) { @@ -392,7 +392,7 @@ ResultVal> ArchiveFactory_SDMC::Open(const Path& } Result ArchiveFactory_SDMC::Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, - u64 program_id) { + u64 program_id, u32 directory_buckets, u32 file_buckets) { // This is kind of an undesirable operation, so let's just ignore it. :) return ResultSuccess; } diff --git a/src/core/file_sys/archive_sdmc.h b/src/core/file_sys/archive_sdmc.h index 267b1dfdf..2da56cd2e 100644 --- a/src/core/file_sys/archive_sdmc.h +++ b/src/core/file_sys/archive_sdmc.h @@ -27,16 +27,16 @@ public: return "SDMCArchive: " + mount_point; } - ResultVal> OpenFile(const Path& path, - const Mode& mode) const override; + ResultVal> OpenFile(const Path& path, const Mode& mode, + u32 attributes) override; Result DeleteFile(const Path& path) const override; Result RenameFile(const Path& src_path, const Path& dest_path) const override; Result DeleteDirectory(const Path& path) const override; Result DeleteDirectoryRecursively(const Path& path) const override; - Result CreateFile(const Path& path, u64 size) const override; - Result CreateDirectory(const Path& path) const override; + Result CreateFile(const Path& path, u64 size, u32 attributes) const override; + Result CreateDirectory(const Path& path, u32 attributes) const override; Result RenameDirectory(const Path& src_path, const Path& dest_path) const override; - ResultVal> OpenDirectory(const Path& path) const override; + ResultVal> OpenDirectory(const Path& path) override; u64 GetFreeBytes() const override; protected: @@ -68,8 +68,8 @@ public: } ResultVal> Open(const Path& path, u64 program_id) override; - Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, - u64 program_id) override; + Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, u64 program_id, + u32 directory_buckets, u32 file_buckets) override; ResultVal GetFormatInfo(const Path& path, u64 program_id) const override; private: diff --git a/src/core/file_sys/archive_sdmcwriteonly.cpp b/src/core/file_sys/archive_sdmcwriteonly.cpp index 31c27c2d2..fc8191964 100644 --- a/src/core/file_sys/archive_sdmcwriteonly.cpp +++ b/src/core/file_sys/archive_sdmcwriteonly.cpp @@ -41,7 +41,8 @@ public: }; ResultVal> SDMCWriteOnlyArchive::OpenFile(const Path& path, - const Mode& mode) const { + const Mode& mode, + u32 attributes) { if (mode.read_flag) { LOG_ERROR(Service_FS, "Read flag is not supported"); return ResultInvalidReadFlag; @@ -49,8 +50,7 @@ ResultVal> SDMCWriteOnlyArchive::OpenFile(const Pat return SDMCArchive::OpenFileBase(path, mode); } -ResultVal> SDMCWriteOnlyArchive::OpenDirectory( - const Path& path) const { +ResultVal> SDMCWriteOnlyArchive::OpenDirectory(const Path& path) { LOG_ERROR(Service_FS, "Not supported"); return ResultUnsupportedOpenFlags; } @@ -83,7 +83,8 @@ ResultVal> ArchiveFactory_SDMCWriteOnly::Open(co Result ArchiveFactory_SDMCWriteOnly::Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, - u64 program_id) { + u64 program_id, u32 directory_buckets, + u32 file_buckets) { // TODO(wwylele): hwtest this LOG_ERROR(Service_FS, "Attempted to format a SDMC write-only archive."); return ResultUnknown; diff --git a/src/core/file_sys/archive_sdmcwriteonly.h b/src/core/file_sys/archive_sdmcwriteonly.h index c05f408d9..c60723a04 100644 --- a/src/core/file_sys/archive_sdmcwriteonly.h +++ b/src/core/file_sys/archive_sdmcwriteonly.h @@ -24,10 +24,10 @@ public: return "SDMCWriteOnlyArchive: " + mount_point; } - ResultVal> OpenFile(const Path& path, - const Mode& mode) const override; + ResultVal> OpenFile(const Path& path, const Mode& mode, + u32 attributes) override; - ResultVal> OpenDirectory(const Path& path) const override; + ResultVal> OpenDirectory(const Path& path) override; private: SDMCWriteOnlyArchive() = default; @@ -54,8 +54,8 @@ public: } ResultVal> Open(const Path& path, u64 program_id) override; - Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, - u64 program_id) override; + Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, u64 program_id, + u32 directory_buckets, u32 file_buckets) override; ResultVal GetFormatInfo(const Path& path, u64 program_id) const override; private: diff --git a/src/core/file_sys/archive_selfncch.cpp b/src/core/file_sys/archive_selfncch.cpp index 60454d674..5472eda01 100644 --- a/src/core/file_sys/archive_selfncch.cpp +++ b/src/core/file_sys/archive_selfncch.cpp @@ -51,7 +51,7 @@ public: return data->size(); } - ResultVal Write(u64 offset, std::size_t length, bool flush, + ResultVal Write(u64 offset, std::size_t length, bool flush, bool update_timestamp, const u8* buffer) override { LOG_ERROR(Service_FS, "The file is read-only!"); return ResultUnsupportedOpenFlags; @@ -65,7 +65,7 @@ public: return false; } - bool Close() const override { + bool Close() override { return true; } @@ -94,7 +94,8 @@ public: return "SelfNCCHArchive"; } - ResultVal> OpenFile(const Path& path, const Mode&) const override { + ResultVal> OpenFile(const Path& path, const Mode&, + u32 attributes) override { // Note: SelfNCCHArchive doesn't check the open mode. if (path.GetType() != LowPathType::Binary) { @@ -154,12 +155,12 @@ public: return ResultUnsupportedOpenFlags; } - Result CreateFile(const Path& path, u64 size) const override { + Result CreateFile(const Path& path, u64 size, u32 attributes) const override { LOG_ERROR(Service_FS, "Unsupported"); return ResultUnsupportedOpenFlags; } - Result CreateDirectory(const Path& path) const override { + Result CreateDirectory(const Path& path, u32 attributes) const override { LOG_ERROR(Service_FS, "Unsupported"); return ResultUnsupportedOpenFlags; } @@ -169,7 +170,7 @@ public: return ResultUnsupportedOpenFlags; } - ResultVal> OpenDirectory(const Path& path) const override { + ResultVal> OpenDirectory(const Path& path) override { LOG_ERROR(Service_FS, "Unsupported"); return ResultUnsupportedOpenFlags; } @@ -297,7 +298,7 @@ ResultVal> ArchiveFactory_SelfNCCH::Open(const P } Result ArchiveFactory_SelfNCCH::Format(const Path&, const FileSys::ArchiveFormatInfo&, - u64 program_id) { + u64 program_id, u32 directory_buckets, u32 file_buckets) { LOG_ERROR(Service_FS, "Attempted to format a SelfNCCH archive."); return ResultInvalidPath; } diff --git a/src/core/file_sys/archive_selfncch.h b/src/core/file_sys/archive_selfncch.h index 0643faf63..e25526105 100644 --- a/src/core/file_sys/archive_selfncch.h +++ b/src/core/file_sys/archive_selfncch.h @@ -50,8 +50,8 @@ public: return "SelfNCCH"; } ResultVal> Open(const Path& path, u64 program_id) override; - Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, - u64 program_id) override; + Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, u64 program_id, + u32 directory_buckets, u32 file_buckets) override; ResultVal GetFormatInfo(const Path& path, u64 program_id) const override; private: diff --git a/src/core/file_sys/archive_source_sd_savedata.cpp b/src/core/file_sys/archive_source_sd_savedata.cpp index 2f4cdcb54..9ab16be0a 100644 --- a/src/core/file_sys/archive_source_sd_savedata.cpp +++ b/src/core/file_sys/archive_source_sd_savedata.cpp @@ -6,6 +6,7 @@ #include "common/archives.h" #include "common/file_util.h" #include "common/logging/log.h" +#include "core/file_sys/archive_artic.h" #include "core/file_sys/archive_source_sd_savedata.h" #include "core/file_sys/errors.h" #include "core/file_sys/savedata_archive.h" @@ -40,49 +41,101 @@ ArchiveSource_SDSaveData::ArchiveSource_SDSaveData(const std::string& sdmc_direc LOG_DEBUG(Service_FS, "Directory {} set as SaveData.", mount_point); } -ResultVal> ArchiveSource_SDSaveData::Open(u64 program_id) { - std::string concrete_mount_point = GetSaveDataPath(mount_point, program_id); - if (!FileUtil::Exists(concrete_mount_point)) { - // When a SaveData archive is created for the first time, it is not yet formatted and the - // save file/directory structure expected by the game has not yet been initialized. - // Returning the NotFormatted error code will signal the game to provision the SaveData - // archive with the files and folders that it expects. - return ResultNotFormatted; - } +ResultVal> ArchiveSource_SDSaveData::Open( + Service::FS::ArchiveIdCode archive_id, const Path& path, u64 program_id) { + if (IsUsingArtic()) { + EnsureCacheCreated(); + return ArticArchive::Open(artic_client, archive_id, path, + Core::PerfStats::PerfArticEventBits::ARTIC_SAVE_DATA, *this, + archive_id != Service::FS::ArchiveIdCode::SaveData); + } else { + std::string concrete_mount_point = GetSaveDataPath(mount_point, program_id); + if (!FileUtil::Exists(concrete_mount_point)) { + // When a SaveData archive is created for the first time, it is not yet formatted and + // the save file/directory structure expected by the game has not yet been initialized. + // Returning the NotFormatted error code will signal the game to provision the SaveData + // archive with the files and folders that it expects. + return ResultNotFormatted; + } - return std::make_unique(std::move(concrete_mount_point)); + return std::make_unique(std::move(concrete_mount_point)); + } } Result ArchiveSource_SDSaveData::Format(u64 program_id, - const FileSys::ArchiveFormatInfo& format_info) { - std::string concrete_mount_point = GetSaveDataPath(mount_point, program_id); - FileUtil::DeleteDirRecursively(concrete_mount_point); - FileUtil::CreateFullPath(concrete_mount_point); + const FileSys::ArchiveFormatInfo& format_info, + Service::FS::ArchiveIdCode archive_id, const Path& path, + u32 directory_buckets, u32 file_buckets) { + if (IsUsingArtic()) { + ClearAllCache(); + auto req = artic_client->NewRequest("FSUSER_FormatSaveData"); - // Write the format metadata - std::string metadata_path = GetSaveDataMetadataPath(mount_point, program_id); - FileUtil::IOFile file(metadata_path, "wb"); + req.AddParameterS32(static_cast(archive_id)); + auto artic_path = ArticArchive::BuildFSPath(path); + req.AddParameterBuffer(artic_path.data(), artic_path.size()); + req.AddParameterU32(format_info.total_size / 512); + req.AddParameterU32(format_info.number_directories); + req.AddParameterU32(format_info.number_files); + req.AddParameterU32(directory_buckets); + req.AddParameterU32(file_buckets); + req.AddParameterU8(format_info.duplicate_data); - if (file.IsOpen()) { - file.WriteBytes(&format_info, sizeof(format_info)); + auto resp = artic_client->Send(req); + return ArticArchive::RespResult(resp); + } else { + std::string concrete_mount_point = GetSaveDataPath(mount_point, program_id); + FileUtil::DeleteDirRecursively(concrete_mount_point); + FileUtil::CreateFullPath(concrete_mount_point); + + // Write the format metadata + std::string metadata_path = GetSaveDataMetadataPath(mount_point, program_id); + FileUtil::IOFile file(metadata_path, "wb"); + + if (file.IsOpen()) { + file.WriteBytes(&format_info, sizeof(format_info)); + return ResultSuccess; + } return ResultSuccess; } - return ResultSuccess; } -ResultVal ArchiveSource_SDSaveData::GetFormatInfo(u64 program_id) const { - std::string metadata_path = GetSaveDataMetadataPath(mount_point, program_id); - FileUtil::IOFile file(metadata_path, "rb"); +ResultVal ArchiveSource_SDSaveData::GetFormatInfo( + u64 program_id, Service::FS::ArchiveIdCode archive_id, const Path& path) const { + if (IsUsingArtic()) { + auto req = artic_client->NewRequest("FSUSER_GetFormatInfo"); - if (!file.IsOpen()) { - LOG_ERROR(Service_FS, "Could not open metadata information for archive"); - // TODO(Subv): Verify error code - return ResultNotFormatted; + req.AddParameterS32(static_cast(archive_id)); + auto path_artic = ArticArchive::BuildFSPath(path); + req.AddParameterBuffer(path_artic.data(), path_artic.size()); + + auto resp = artic_client->Send(req); + Result res = ArticArchive::RespResult(resp); + if (R_FAILED(res)) { + return res; + } + + auto info_buf = resp->GetResponseBuffer(0); + if (!info_buf.has_value() || info_buf->second != sizeof(ArchiveFormatInfo)) { + return ResultUnknown; + } + + ArchiveFormatInfo info; + memcpy(&info, info_buf->first, sizeof(info)); + return info; + } else { + std::string metadata_path = GetSaveDataMetadataPath(mount_point, program_id); + FileUtil::IOFile file(metadata_path, "rb"); + + if (!file.IsOpen()) { + LOG_ERROR(Service_FS, "Could not open metadata information for archive"); + // TODO(Subv): Verify error code + return ResultNotFormatted; + } + + ArchiveFormatInfo info = {}; + file.ReadBytes(&info, sizeof(info)); + return info; } - - ArchiveFormatInfo info = {}; - file.ReadBytes(&info, sizeof(info)); - return info; } std::string ArchiveSource_SDSaveData::GetSaveDataPathFor(const std::string& mount_point, diff --git a/src/core/file_sys/archive_source_sd_savedata.h b/src/core/file_sys/archive_source_sd_savedata.h index 07832c3ae..56fdb2c5b 100644 --- a/src/core/file_sys/archive_source_sd_savedata.h +++ b/src/core/file_sys/archive_source_sd_savedata.h @@ -9,27 +9,48 @@ #include #include #include "core/file_sys/archive_backend.h" +#include "core/file_sys/artic_cache.h" #include "core/hle/result.h" +#include "network/artic_base/artic_base_client.h" + +namespace Service::FS { +enum class ArchiveIdCode : u32; +} // namespace Service::FS namespace FileSys { /// A common source of SD save data archive -class ArchiveSource_SDSaveData { +class ArchiveSource_SDSaveData : public ArticCacheProvider { public: explicit ArchiveSource_SDSaveData(const std::string& mount_point); - ResultVal> Open(u64 program_id); - Result Format(u64 program_id, const FileSys::ArchiveFormatInfo& format_info); - ResultVal GetFormatInfo(u64 program_id) const; + ResultVal> Open(Service::FS::ArchiveIdCode archive_id, + const Path& path, u64 program_id); + Result Format(u64 program_id, const FileSys::ArchiveFormatInfo& format_info, + Service::FS::ArchiveIdCode archive_id, const Path& path, u32 directory_buckets, + u32 file_buckets); + ResultVal GetFormatInfo(u64 program_id, + Service::FS::ArchiveIdCode archive_id, + const Path& path) const; static std::string GetSaveDataPathFor(const std::string& mount_point, u64 program_id); + void RegisterArtic(std::shared_ptr& client) { + artic_client = client; + } + + bool IsUsingArtic() const { + return artic_client.get() != nullptr; + } + private: std::string mount_point; + std::shared_ptr artic_client = nullptr; ArchiveSource_SDSaveData() = default; template void serialize(Archive& ar, const unsigned int) { + ar& boost::serialization::base_object(*this); ar& mount_point; } friend class boost::serialization::access; diff --git a/src/core/file_sys/archive_systemsavedata.cpp b/src/core/file_sys/archive_systemsavedata.cpp index 62734fca6..79ae33cd7 100644 --- a/src/core/file_sys/archive_systemsavedata.cpp +++ b/src/core/file_sys/archive_systemsavedata.cpp @@ -64,7 +64,8 @@ ResultVal> ArchiveFactory_SystemSaveData::Open(c Result ArchiveFactory_SystemSaveData::Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, - u64 program_id) { + u64 program_id, u32 directory_buckets, + u32 file_buckets) { std::string fullpath = GetSystemSaveDataPath(base_path, path); FileUtil::DeleteDirRecursively(fullpath); FileUtil::CreateFullPath(fullpath); diff --git a/src/core/file_sys/archive_systemsavedata.h b/src/core/file_sys/archive_systemsavedata.h index af7c341e2..5cb109122 100644 --- a/src/core/file_sys/archive_systemsavedata.h +++ b/src/core/file_sys/archive_systemsavedata.h @@ -20,8 +20,8 @@ public: explicit ArchiveFactory_SystemSaveData(const std::string& mount_point); ResultVal> Open(const Path& path, u64 program_id) override; - Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, - u64 program_id) override; + Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, u64 program_id, + u32 directory_buckets, u32 file_buckets) override; ResultVal GetFormatInfo(const Path& path, u64 program_id) const override; std::string GetName() const override { diff --git a/src/core/file_sys/artic_cache.cpp b/src/core/file_sys/artic_cache.cpp new file mode 100644 index 000000000..b5c963495 --- /dev/null +++ b/src/core/file_sys/artic_cache.cpp @@ -0,0 +1,235 @@ +// Copyright 2024 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "artic_cache.h" + +namespace FileSys { +ResultVal ArticCache::Read(s32 file_handle, std::size_t offset, std::size_t length, + u8* buffer) { + if (length == 0) + return size_t(); + + const auto segments = BreakupRead(offset, length); + std::size_t read_progress = 0; + + // Skip cache if the read is too big + if (segments.size() == 1 && segments[0].second > cache_line_size) { + if (segments[0].second < big_cache_skip) { + std::unique_lock big_read_guard(big_cache_mutex); + auto big_cache_entry = big_cache.request(std::make_pair(offset, length)); + if (!big_cache_entry.first) { + LOG_TRACE(Service_FS, "ArticCache BMISS: offset={}, length={}", offset, length); + big_cache_entry.second.clear(); + big_cache_entry.second.resize(length); + auto res = + ReadFromArtic(file_handle, reinterpret_cast(big_cache_entry.second.data()), + length, offset); + if (res.Failed()) + return res; + length = res.Unwrap(); + } else { + LOG_TRACE(Service_FS, "ArticCache BHIT: offset={}, length={}", offset, length); + } + memcpy(buffer, big_cache_entry.second.data(), length); + } else { + if (segments[0].second < very_big_cache_skip) { + std::unique_lock very_big_read_guard(very_big_cache_mutex); + auto very_big_cache_entry = very_big_cache.request(std::make_pair(offset, length)); + if (!very_big_cache_entry.first) { + LOG_TRACE(Service_FS, "ArticCache VBMISS: offset={}, length={}", offset, + length); + very_big_cache_entry.second.clear(); + very_big_cache_entry.second.resize(length); + auto res = ReadFromArtic( + file_handle, reinterpret_cast(very_big_cache_entry.second.data()), + length, offset); + if (res.Failed()) + return res; + length = res.Unwrap(); + } else { + LOG_TRACE(Service_FS, "ArticCache VBHIT: offset={}, length={}", offset, length); + } + memcpy(buffer, very_big_cache_entry.second.data(), length); + } else { + LOG_TRACE(Service_FS, "ArticCache SKIP: offset={}, length={}", offset, length); + + auto res = ReadFromArtic(file_handle, buffer, length, offset); + if (res.Failed()) + return res; + length = res.Unwrap(); + } + } + return length; + } + + // TODO(PabloMK7): Make cache thread safe, read the comment in CacheReady function. + std::unique_lock read_guard(cache_mutex); + for (const auto& seg : segments) { + std::size_t read_size = cache_line_size; + std::size_t page = OffsetToPage(seg.first); + // Check if segment is in cache + auto cache_entry = cache.request(page); + if (!cache_entry.first) { + // If not found, read from artic and cache the data + auto res = ReadFromArtic(file_handle, cache_entry.second.data(), read_size, page); + if (res.Failed()) + return res; + read_size = res.Unwrap(); + LOG_TRACE(Service_FS, "ArticCache MISS: page={}, length={}, into={}", page, seg.second, + (seg.first - page)); + } else { + LOG_TRACE(Service_FS, "ArticCache HIT: page={}, length={}, into={}", page, seg.second, + (seg.first - page)); + } + std::size_t copy_amount = + (read_size > (seg.first - page)) + ? std::min((seg.first - page) + seg.second, read_size) - (seg.first - page) + : 0; + std::memcpy(buffer + read_progress, cache_entry.second.data() + (seg.first - page), + copy_amount); + read_progress += copy_amount; + } + return read_progress; +} + +bool ArticCache::CacheReady(std::size_t file_offset, std::size_t length) { + auto segments = BreakupRead(file_offset, length); + if (segments.size() == 1 && segments[0].second > cache_line_size) { + return false; + } else { + std::shared_lock read_guard(cache_mutex); + for (auto it = segments.begin(); it != segments.end(); it++) { + if (!cache.contains(OffsetToPage(it->first))) + return false; + } + return true; + } +} + +void ArticCache::Clear() { + std::unique_lock l1(cache_mutex), l2(big_cache_mutex), l3(very_big_cache_mutex); + cache.clear(); + big_cache.clear(); + very_big_cache.clear(); + data_size = std::nullopt; +} + +ResultVal ArticCache::Write(s32 file_handle, std::size_t offset, std::size_t length, + const u8* buffer, u32 flags) { + // Can probably do better, but write operations are usually done at the end, so it doesn't + // matter much + Clear(); + + size_t written_amount = 0; + while (written_amount != length) { + size_t to_write = + std::min(client->GetServerRequestMaxSize() - 0x100, length - written_amount); + + auto req = client->NewRequest("FSFILE_Write"); + req.AddParameterS32(file_handle); + req.AddParameterS64(static_cast(offset + written_amount)); + req.AddParameterS32(static_cast(to_write)); + req.AddParameterS32(static_cast(flags)); + req.AddParameterBuffer(buffer + written_amount, to_write); + auto resp = client->Send(req); + if (!resp.has_value() || !resp->Succeeded()) + return Result(-1); + + auto res = Result(static_cast(resp->GetMethodResult())); + if (res.IsError()) + return res; + + auto actually_written_opt = resp->GetResponseS32(0); + if (!actually_written_opt.has_value()) + return Result(-1); + + size_t actually_written = static_cast(actually_written_opt.value()); + + written_amount += actually_written; + if (actually_written != to_write) + break; + } + return written_amount; +} + +ResultVal ArticCache::GetSize(s32 file_handle) { + std::unique_lock l1(cache_mutex); + + if (data_size.has_value()) + return data_size.value(); + + auto req = client->NewRequest("FSFILE_GetSize"); + + req.AddParameterS32(file_handle); + + auto resp = client->Send(req); + if (!resp.has_value() || !resp->Succeeded()) + return Result(-1); + + auto res = Result(static_cast(resp->GetMethodResult())); + if (res.IsError()) + return res; + + auto size_buf = resp->GetResponseS64(0); + if (!size_buf) { + return Result(-1); + } + + data_size = static_cast(*size_buf); + return data_size.value(); +} + +ResultVal ArticCache::ReadFromArtic(s32 file_handle, u8* buffer, size_t len, + size_t offset) { + size_t read_amount = 0; + while (read_amount != len) { + size_t to_read = + std::min(client->GetServerRequestMaxSize() - 0x100, len - read_amount); + + auto req = client->NewRequest("FSFILE_Read"); + req.AddParameterS32(file_handle); + req.AddParameterS64(static_cast(offset + read_amount)); + req.AddParameterS32(static_cast(to_read)); + auto resp = client->Send(req); + if (!resp.has_value() || !resp->Succeeded()) + return Result(-1); + + auto res = Result(static_cast(resp->GetMethodResult())); + if (res.IsError()) + return res; + + auto read_buff = resp->GetResponseBuffer(0); + if (!read_buff.has_value()) + return Result(-1); + size_t actually_read = read_buff->second; + + memcpy(buffer + read_amount, read_buff->first, actually_read); + read_amount += actually_read; + if (actually_read != to_read) + break; + } + return read_amount; +} + +std::vector> ArticCache::BreakupRead(std::size_t offset, + std::size_t length) { + std::vector> ret; + + // Reads bigger than the cache line size will probably never hit again + if (length > max_breakup_size) { + ret.push_back(std::make_pair(offset, length)); + return ret; + } + + std::size_t curr_offset = offset; + while (length) { + std::size_t next_page = OffsetToPage(curr_offset + cache_line_size); + std::size_t curr_page_len = std::min(length, next_page - curr_offset); + ret.push_back(std::make_pair(curr_offset, curr_page_len)); + curr_offset = next_page; + length -= curr_page_len; + } + return ret; +} +} // namespace FileSys diff --git a/src/core/file_sys/artic_cache.h b/src/core/file_sys/artic_cache.h new file mode 100644 index 000000000..7e8ed591c --- /dev/null +++ b/src/core/file_sys/artic_cache.h @@ -0,0 +1,154 @@ +// Copyright 2024 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include "vector" + +#include +#include +#include +#include "common/alignment.h" +#include "common/common_types.h" +#include "common/static_lru_cache.h" +#include "core/file_sys/archive_backend.h" +#include "core/hle/result.h" +#include "network/artic_base/artic_base_client.h" + +namespace FileSys { +class ArticCache { +public: + ArticCache() = default; + + ArticCache(const std::shared_ptr& cli) : client(cli) {} + + ResultVal Read(s32 file_handle, std::size_t offset, std::size_t length, + u8* buffer); + + bool CacheReady(std::size_t file_offset, std::size_t length); + + void Clear(); + + ResultVal Write(s32 file_handle, std::size_t offset, std::size_t length, + const u8* buffer, u32 flags); + + ResultVal GetSize(s32 file_handle); + + void ForceSetSize(const std::optional& size) { + data_size = size; + } + +private: + std::shared_ptr client; + std::optional data_size; + + // Total cache size: 32MB small, 512MB big (worst case), 160MB very big (worst case). + // The worst case values are unrealistic, they will never happen in any real game. + static constexpr std::size_t cache_line_size = 4 * 1024; + static constexpr std::size_t cache_line_count = 256; + static constexpr std::size_t max_breakup_size = 8 * 1024; + + static constexpr std::size_t big_cache_skip = 1 * 1024 * 1024; + static constexpr std::size_t big_cache_lines = 1024; + + static constexpr std::size_t very_big_cache_skip = 10 * 1024 * 1024; + static constexpr std::size_t very_big_cache_lines = 24; + + Common::StaticLRUCache, cache_line_count> cache; + std::shared_mutex cache_mutex; + + struct NoInitChar { + u8 value; + NoInitChar() noexcept { + // do nothing + static_assert(sizeof *this == sizeof value, "invalid size"); + } + }; + Common::StaticLRUCache, std::vector, + big_cache_lines> + big_cache; + std::shared_mutex big_cache_mutex; + Common::StaticLRUCache, std::vector, + very_big_cache_lines> + very_big_cache; + std::shared_mutex very_big_cache_mutex; + + ResultVal ReadFromArtic(s32 file_handle, u8* buffer, size_t len, size_t offset); + + std::size_t OffsetToPage(std::size_t offset) { + return Common::AlignDown(offset, cache_line_size); + } + + std::vector> BreakupRead(std::size_t offset, + std::size_t length); + +protected: + template + void serialize(Archive& ar, const unsigned int) {} + friend class boost::serialization::access; +}; + +class ArticCacheProvider { +public: + virtual ~ArticCacheProvider() {} + + std::vector PathsToVector(const Path& archive_path, const Path& file_path) { + auto archive_path_binary = archive_path.AsBinary(); + auto file_path_binary = file_path.AsBinary(); + + std::vector ret; + ret.push_back(static_cast(file_path.GetType())); + ret.insert(ret.end(), archive_path_binary.begin(), archive_path_binary.end()); + ret.push_back(static_cast(archive_path.GetType())); + ret.insert(ret.end(), file_path_binary.begin(), file_path_binary.end()); + return ret; + } + + virtual std::shared_ptr ProvideCache( + const std::shared_ptr& cli, const std::vector& path, + bool create) { + if (file_caches == nullptr) + return nullptr; + + auto it = file_caches->find(path); + if (it == file_caches->end()) { + if (!create) { + return nullptr; + } + auto res = std::make_shared(cli); + file_caches->insert({path, res}); + return res; + } + return it->second; + } + + virtual void ClearAllCache() { + if (file_caches != nullptr) { + file_caches->clear(); + } + } + + virtual void EnsureCacheCreated() { + if (file_caches == nullptr) { + file_caches = + std::make_unique, std::shared_ptr>>(); + } + } + +protected: + template + void serialize(Archive& ar, const unsigned int) {} + friend class boost::serialization::access; + +private: + std::unique_ptr, std::shared_ptr>> file_caches = nullptr; + std::shared_ptr client; +}; + +} // namespace FileSys + +BOOST_CLASS_EXPORT_KEY(FileSys::ArticCache) +BOOST_CLASS_EXPORT_KEY(FileSys::ArticCacheProvider) \ No newline at end of file diff --git a/src/core/file_sys/directory_backend.h b/src/core/file_sys/directory_backend.h index b5a2617bb..e88363ae6 100644 --- a/src/core/file_sys/directory_backend.h +++ b/src/core/file_sys/directory_backend.h @@ -49,7 +49,11 @@ public: * Close the directory * @return true if the directory closed correctly */ - virtual bool Close() const = 0; + virtual bool Close() = 0; + + virtual bool IsSlow() { + return false; + } private: template diff --git a/src/core/file_sys/disk_archive.cpp b/src/core/file_sys/disk_archive.cpp index 12ea8932a..a7ae5e92e 100644 --- a/src/core/file_sys/disk_archive.cpp +++ b/src/core/file_sys/disk_archive.cpp @@ -26,7 +26,7 @@ ResultVal DiskFile::Read(const u64 offset, const std::size_t length } ResultVal DiskFile::Write(const u64 offset, const std::size_t length, const bool flush, - const u8* buffer) { + const bool update_timestamp, const u8* buffer) { if (!mode.write_flag) return ResultInvalidOpenFlags; @@ -47,7 +47,7 @@ bool DiskFile::SetSize(const u64 size) const { return true; } -bool DiskFile::Close() const { +bool DiskFile::Close() { return file->Close(); } diff --git a/src/core/file_sys/disk_archive.h b/src/core/file_sys/disk_archive.h index 5843a37d5..0af741be7 100644 --- a/src/core/file_sys/disk_archive.h +++ b/src/core/file_sys/disk_archive.h @@ -30,11 +30,11 @@ public: } ResultVal Read(u64 offset, std::size_t length, u8* buffer) const override; - ResultVal Write(u64 offset, std::size_t length, bool flush, + ResultVal Write(u64 offset, std::size_t length, bool flush, bool update_timestamp, const u8* buffer) override; u64 GetSize() const override; bool SetSize(u64 size) const override; - bool Close() const override; + bool Close() override; void Flush() const override { file->Flush(); @@ -66,7 +66,7 @@ public: u32 Read(u32 count, Entry* entries) override; - bool Close() const override { + bool Close() override { return true; } diff --git a/src/core/file_sys/file_backend.h b/src/core/file_sys/file_backend.h index bf0a1b493..e491ae0e8 100644 --- a/src/core/file_sys/file_backend.h +++ b/src/core/file_sys/file_backend.h @@ -37,7 +37,7 @@ public: * @return Number of bytes written, or error code */ virtual ResultVal Write(u64 offset, std::size_t length, bool flush, - const u8* buffer) = 0; + bool update_timestamp, const u8* buffer) = 0; /** * Get the amount of time a 3ds needs to read those data @@ -79,7 +79,7 @@ public: * Close the file * @return true if the file closed correctly */ - virtual bool Close() const = 0; + virtual bool Close() = 0; /** * Flushes the file diff --git a/src/core/file_sys/ivfc_archive.cpp b/src/core/file_sys/ivfc_archive.cpp index de71883a7..e58c78d22 100644 --- a/src/core/file_sys/ivfc_archive.cpp +++ b/src/core/file_sys/ivfc_archive.cpp @@ -28,8 +28,8 @@ std::string IVFCArchive::GetName() const { return "IVFC"; } -ResultVal> IVFCArchive::OpenFile(const Path& path, - const Mode& mode) const { +ResultVal> IVFCArchive::OpenFile(const Path& path, const Mode& mode, + u32 attributes) { std::unique_ptr delay_generator = std::make_unique(); return std::make_unique(romfs_file, std::move(delay_generator)); } @@ -61,14 +61,14 @@ Result IVFCArchive::DeleteDirectoryRecursively(const Path& path) const { return ResultUnknown; } -Result IVFCArchive::CreateFile(const Path& path, u64 size) const { +Result IVFCArchive::CreateFile(const Path& path, u64 size, u32 attributes) const { LOG_CRITICAL(Service_FS, "Attempted to create a file in an IVFC archive ({}).", GetName()); // TODO: Verify error code return Result(ErrorDescription::NotAuthorized, ErrorModule::FS, ErrorSummary::NotSupported, ErrorLevel::Permanent); } -Result IVFCArchive::CreateDirectory(const Path& path) const { +Result IVFCArchive::CreateDirectory(const Path& path, u32 attributes) const { LOG_CRITICAL(Service_FS, "Attempted to create a directory in an IVFC archive ({}).", GetName()); // TODO(wwylele): Use correct error code return ResultUnknown; @@ -80,7 +80,7 @@ Result IVFCArchive::RenameDirectory(const Path& src_path, const Path& dest_path) return ResultUnknown; } -ResultVal> IVFCArchive::OpenDirectory(const Path& path) const { +ResultVal> IVFCArchive::OpenDirectory(const Path& path) { return std::make_unique(); } @@ -102,7 +102,7 @@ ResultVal IVFCFile::Read(const u64 offset, const std::size_t length } ResultVal IVFCFile::Write(const u64 offset, const std::size_t length, const bool flush, - const u8* buffer) { + const bool update_timestamp, const u8* buffer) { LOG_ERROR(Service_FS, "Attempted to write to IVFC file"); // TODO(Subv): Find error code return 0ULL; @@ -133,7 +133,8 @@ ResultVal IVFCFileInMemory::Read(const u64 offset, const std::size_ } ResultVal IVFCFileInMemory::Write(const u64 offset, const std::size_t length, - const bool flush, const u8* buffer) { + const bool flush, const bool update_timestamp, + const u8* buffer) { LOG_ERROR(Service_FS, "Attempted to write to IVFC file"); // TODO(Subv): Find error code return 0ULL; diff --git a/src/core/file_sys/ivfc_archive.h b/src/core/file_sys/ivfc_archive.h index 765e41444..632d643ec 100644 --- a/src/core/file_sys/ivfc_archive.h +++ b/src/core/file_sys/ivfc_archive.h @@ -101,16 +101,16 @@ public: std::string GetName() const override; - ResultVal> OpenFile(const Path& path, - const Mode& mode) const override; + ResultVal> OpenFile(const Path& path, const Mode& mode, + u32 attributes) override; Result DeleteFile(const Path& path) const override; Result RenameFile(const Path& src_path, const Path& dest_path) const override; Result DeleteDirectory(const Path& path) const override; Result DeleteDirectoryRecursively(const Path& path) const override; - Result CreateFile(const Path& path, u64 size) const override; - Result CreateDirectory(const Path& path) const override; + Result CreateFile(const Path& path, u64 size, u32 attributes) const override; + Result CreateDirectory(const Path& path, u32 attributes) const override; Result RenameDirectory(const Path& src_path, const Path& dest_path) const override; - ResultVal> OpenDirectory(const Path& path) const override; + ResultVal> OpenDirectory(const Path& path) override; u64 GetFreeBytes() const override; protected: @@ -122,11 +122,11 @@ public: IVFCFile(std::shared_ptr file, std::unique_ptr delay_generator_); ResultVal Read(u64 offset, std::size_t length, u8* buffer) const override; - ResultVal Write(u64 offset, std::size_t length, bool flush, + ResultVal Write(u64 offset, std::size_t length, bool flush, bool update_timestamp, const u8* buffer) override; u64 GetSize() const override; bool SetSize(u64 size) const override; - bool Close() const override { + bool Close() override { return false; } void Flush() const override {} @@ -157,7 +157,7 @@ public: u32 Read(const u32 count, Entry* entries) override { return 0; } - bool Close() const override { + bool Close() override { return false; } }; @@ -168,11 +168,11 @@ public: std::unique_ptr delay_generator_); ResultVal Read(u64 offset, std::size_t length, u8* buffer) const override; - ResultVal Write(u64 offset, std::size_t length, bool flush, + ResultVal Write(u64 offset, std::size_t length, bool flush, bool update_timestamp, const u8* buffer) override; u64 GetSize() const override; bool SetSize(u64 size) const override; - bool Close() const override { + bool Close() override { return false; } void Flush() const override {} diff --git a/src/core/file_sys/romfs_reader.cpp b/src/core/file_sys/romfs_reader.cpp index 40823d833..2cff2825b 100644 --- a/src/core/file_sys/romfs_reader.cpp +++ b/src/core/file_sys/romfs_reader.cpp @@ -4,7 +4,11 @@ #include #include "common/archives.h" #include "common/logging/log.h" +#include "core/file_sys/archive_artic.h" +#include "core/file_sys/archive_backend.h" #include "core/file_sys/romfs_reader.h" +#include "core/hle/service/fs/fs_user.h" +#include "core/loader/loader.h" SERIALIZE_EXPORT_IMPL(FileSys::DirectRomFSReader) @@ -109,4 +113,102 @@ std::vector> DirectRomFSReader::BreakupRead( return ret; } +ArticRomFSReader::ArticRomFSReader(std::shared_ptr& cli, + bool is_update_romfs) + : client(cli), cache(cli) { + auto req = client->NewRequest("FSUSER_OpenFileDirectly"); + + FileSys::Path archive(FileSys::LowPathType::Empty, {}); + std::vector fileVec(0xC); + fileVec[0] = static_cast(is_update_romfs ? 5 : 0); + FileSys::Path file(FileSys::LowPathType::Binary, fileVec); + + req.AddParameterS32(static_cast(Service::FS::ArchiveIdCode::SelfNCCH)); + + auto archive_buf = ArticArchive::BuildFSPath(archive); + req.AddParameterBuffer(archive_buf.data(), archive_buf.size()); + auto file_buf = ArticArchive::BuildFSPath(file); + req.AddParameterBuffer(file_buf.data(), file_buf.size()); + + req.AddParameterS32(1); + req.AddParameterS32(0); + + auto resp = client->Send(req); + + if (!resp.has_value() || !resp->Succeeded()) { + load_status = Loader::ResultStatus::Error; + return; + } + if (resp->GetMethodResult() != 0) { + load_status = Loader::ResultStatus::ErrorNotUsed; + return; + } + + auto handle_buf = resp->GetResponseBuffer(0); + if (!handle_buf.has_value() || handle_buf->second != sizeof(s32)) { + load_status = Loader::ResultStatus::Error; + return; + } + + romfs_handle = *reinterpret_cast(handle_buf->first); + + req = client->NewRequest("FSFILE_GetSize"); + + req.AddParameterS32(romfs_handle); + + resp = client->Send(req); + + if (!resp.has_value() || !resp->Succeeded()) { + load_status = Loader::ResultStatus::Error; + return; + } + if (resp->GetMethodResult() != 0) { + load_status = Loader::ResultStatus::ErrorNotUsed; + return; + } + + auto size_buf = resp->GetResponseBuffer(0); + if (!size_buf.has_value() || size_buf->second != sizeof(u64)) { + load_status = Loader::ResultStatus::Error; + return; + } + + data_size = static_cast(*reinterpret_cast(size_buf->first)); + load_status = Loader::ResultStatus::Success; +} + +ArticRomFSReader::~ArticRomFSReader() { + if (romfs_handle != -1) { + auto req = client->NewRequest("FSFILE_Close"); + req.AddParameterS32(romfs_handle); + client->Send(req); + romfs_handle = -1; + } +} + +std::size_t ArticRomFSReader::ReadFile(std::size_t offset, std::size_t length, u8* buffer) { + length = std::min(length, static_cast(data_size) - offset); + auto res = cache.Read(romfs_handle, offset, length, buffer); + if (res.Failed()) + return 0; + return res.Unwrap(); +} + +bool ArticRomFSReader::AllowsCachedReads() const { + return true; +} + +bool ArticRomFSReader::CacheReady(std::size_t file_offset, std::size_t length) { + return cache.CacheReady(file_offset, length); +} + +void ArticRomFSReader::CloseFile() { + if (romfs_handle != -1) { + auto req = client->NewRequest("FSFILE_Close"); + req.AddParameterS32(romfs_handle); + client->Send(req); + romfs_handle = -1; + } +} + } // namespace FileSys diff --git a/src/core/file_sys/romfs_reader.h b/src/core/file_sys/romfs_reader.h index 128d10dbd..63b88e262 100644 --- a/src/core/file_sys/romfs_reader.h +++ b/src/core/file_sys/romfs_reader.h @@ -9,6 +9,12 @@ #include "common/common_types.h" #include "common/file_util.h" #include "common/static_lru_cache.h" +#include "core/file_sys/artic_cache.h" +#include "network/artic_base/artic_base_client.h" + +namespace Loader { +enum class ResultStatus; +} namespace FileSys { @@ -97,6 +103,53 @@ private: friend class boost::serialization::access; }; +/** + * A RomFS reader that reads from an artic base server. + */ +class ArticRomFSReader : public RomFSReader { +public: + ArticRomFSReader() = default; + ArticRomFSReader(std::shared_ptr& cli, bool is_update_romfs); + + ~ArticRomFSReader() override; + + std::size_t GetSize() const override { + return data_size; + } + + std::size_t ReadFile(std::size_t offset, std::size_t length, u8* buffer) override; + + bool AllowsCachedReads() const override; + + bool CacheReady(std::size_t file_offset, std::size_t length) override; + + Loader::ResultStatus OpenStatus() { + return load_status; + } + + void ClearCache() { + cache.Clear(); + } + + void CloseFile(); + +private: + std::shared_ptr client; + size_t data_size = 0; + s32 romfs_handle = -1; + Loader::ResultStatus load_status; + + ArticCache cache; + + template + void serialize(Archive& ar, const unsigned int) { + ar& boost::serialization::base_object(*this); + ar& data_size; + } + friend class boost::serialization::access; +}; + } // namespace FileSys BOOST_CLASS_EXPORT_KEY(FileSys::DirectRomFSReader) +BOOST_CLASS_EXPORT_KEY(FileSys::ArticRomFSReader) diff --git a/src/core/file_sys/savedata_archive.cpp b/src/core/file_sys/savedata_archive.cpp index 17380e6ac..00ee84d8f 100644 --- a/src/core/file_sys/savedata_archive.cpp +++ b/src/core/file_sys/savedata_archive.cpp @@ -36,7 +36,8 @@ public: }; ResultVal> SaveDataArchive::OpenFile(const Path& path, - const Mode& mode) const { + const Mode& mode, + u32 attributes) { LOG_DEBUG(Service_FS, "called path={} mode={:01X}", path.DebugStr(), mode.hex); const PathParser path_parser(path); @@ -203,7 +204,7 @@ Result SaveDataArchive::DeleteDirectoryRecursively(const Path& path) const { path, mount_point, [](const std::string& p) { return FileUtil::DeleteDirRecursively(p); }); } -Result SaveDataArchive::CreateFile(const FileSys::Path& path, u64 size) const { +Result SaveDataArchive::CreateFile(const FileSys::Path& path, u64 size, u32 attributes) const { const PathParser path_parser(path); if (!path_parser.IsValid()) { @@ -253,7 +254,7 @@ Result SaveDataArchive::CreateFile(const FileSys::Path& path, u64 size) const { ErrorLevel::Info); } -Result SaveDataArchive::CreateDirectory(const Path& path) const { +Result SaveDataArchive::CreateDirectory(const Path& path, u32 attributes) const { const PathParser path_parser(path); if (!path_parser.IsValid()) { @@ -319,8 +320,7 @@ Result SaveDataArchive::RenameDirectory(const Path& src_path, const Path& dest_p ErrorSummary::NothingHappened, ErrorLevel::Status); } -ResultVal> SaveDataArchive::OpenDirectory( - const Path& path) const { +ResultVal> SaveDataArchive::OpenDirectory(const Path& path) { const PathParser path_parser(path); if (!path_parser.IsValid()) { diff --git a/src/core/file_sys/savedata_archive.h b/src/core/file_sys/savedata_archive.h index f72f924f1..0169d918e 100644 --- a/src/core/file_sys/savedata_archive.h +++ b/src/core/file_sys/savedata_archive.h @@ -22,16 +22,16 @@ public: return "SaveDataArchive: " + mount_point; } - ResultVal> OpenFile(const Path& path, - const Mode& mode) const override; + ResultVal> OpenFile(const Path& path, const Mode& mode, + u32 attributes) override; Result DeleteFile(const Path& path) const override; Result RenameFile(const Path& src_path, const Path& dest_path) const override; Result DeleteDirectory(const Path& path) const override; Result DeleteDirectoryRecursively(const Path& path) const override; - Result CreateFile(const Path& path, u64 size) const override; - Result CreateDirectory(const Path& path) const override; + Result CreateFile(const Path& path, u64 size, u32 attributes) const override; + Result CreateDirectory(const Path& path, u32 attributes) const override; Result RenameDirectory(const Path& src_path, const Path& dest_path) const override; - ResultVal> OpenDirectory(const Path& path) const override; + ResultVal> OpenDirectory(const Path& path) override; u64 GetFreeBytes() const override; protected: diff --git a/src/core/file_sys/secure_value_backend.cpp b/src/core/file_sys/secure_value_backend.cpp new file mode 100644 index 000000000..c0b0bcae6 --- /dev/null +++ b/src/core/file_sys/secure_value_backend.cpp @@ -0,0 +1,74 @@ +// Copyright 2024 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "common/archives.h" +#include "secure_value_backend.h" + +SERIALIZE_EXPORT_IMPL(FileSys::DefaultSecureValueBackend) + +namespace FileSys { + +Result DefaultSecureValueBackend::ObsoletedSetSaveDataSecureValue(u32 unique_id, u8 title_variation, + u32 secure_value_slot, + u64 secure_value) { + + // TODO: Generate and Save the Secure Value + + LOG_WARNING(Service_FS, + "(STUBBED) called, value=0x{:016x} secure_value_slot=0x{:08X} " + "unqiue_id=0x{:08X} title_variation=0x{:02X}", + secure_value, secure_value_slot, unique_id, title_variation); + + return ResultSuccess; +} + +ResultVal> DefaultSecureValueBackend::ObsoletedGetSaveDataSecureValue( + u32 unique_id, u8 title_variation, u32 secure_value_slot) { + + // TODO: Implement Secure Value Lookup & Generation + + LOG_WARNING(Service_FS, + "(STUBBED) called, secure_value_slot=0x{:08X} " + "unqiue_id=0x{:08X} title_variation=0x{:02X}", + secure_value_slot, unique_id, title_variation); + + return std::make_tuple(false, 0); +} + +Result DefaultSecureValueBackend::ControlSecureSave(u32 action, u8* input, size_t input_size, + u8* output, size_t output_size) { + + LOG_WARNING(Service_FS, + "(STUBBED) called, action=0x{:08X} " + "input_size=0x{:016X} output_size=0x{:016X}", + action, input_size, output_size); + + return ResultSuccess; +} + +Result DefaultSecureValueBackend::SetThisSaveDataSecureValue(u32 secure_value_slot, + u64 secure_value) { + // TODO: Generate and Save the Secure Value + + LOG_WARNING(Service_FS, "(STUBBED) called, secure_value=0x{:016x} secure_value_slot=0x{:08X}", + secure_value, secure_value_slot); + + return ResultSuccess; +} + +ResultVal> DefaultSecureValueBackend::GetThisSaveDataSecureValue( + u32 secure_value_slot) { + + // TODO: Implement Secure Value Lookup & Generation + + LOG_WARNING(Service_FS, "(STUBBED) called secure_value_slot=0x{:08X}", secure_value_slot); + + return std::make_tuple(false, true, 0); +} + +template +void FileSys::DefaultSecureValueBackend::serialize(Archive& ar, const unsigned int) { + ar& boost::serialization::base_object(*this); +} +} // namespace FileSys \ No newline at end of file diff --git a/src/core/file_sys/secure_value_backend.h b/src/core/file_sys/secure_value_backend.h new file mode 100644 index 000000000..e9d3d3da2 --- /dev/null +++ b/src/core/file_sys/secure_value_backend.h @@ -0,0 +1,65 @@ +// Copyright 2024 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include "tuple" + +#include "common/common_types.h" +#include "core/hle/result.h" +#include "core/hle/service/fs/archive.h" + +namespace FileSys { +class SecureValueBackend : NonCopyable { +public: + virtual ~SecureValueBackend(){}; + + virtual Result ObsoletedSetSaveDataSecureValue(u32 unique_id, u8 title_variation, + u32 secure_value_slot, u64 secure_value) = 0; + + virtual ResultVal> ObsoletedGetSaveDataSecureValue( + u32 unique_id, u8 title_variation, u32 secure_value_slot) = 0; + + virtual Result ControlSecureSave(u32 action, u8* input, size_t input_size, u8* output, + size_t output_size) = 0; + + virtual Result SetThisSaveDataSecureValue(u32 secure_value_slot, u64 secure_value) = 0; + + virtual ResultVal> GetThisSaveDataSecureValue( + u32 secure_value_slot) = 0; + + virtual bool BackendIsSlow() { + return false; + } + +protected: + template + void serialize(Archive& ar, const unsigned int) {} + friend class boost::serialization::access; +}; + +class DefaultSecureValueBackend : public SecureValueBackend { +public: + Result ObsoletedSetSaveDataSecureValue(u32 unique_id, u8 title_variation, u32 secure_value_slot, + u64 secure_value) override; + + ResultVal> ObsoletedGetSaveDataSecureValue( + u32 unique_id, u8 title_variation, u32 secure_value_slot) override; + + Result ControlSecureSave(u32 action, u8* input, size_t input_size, u8* output, + size_t output_size) override; + + Result SetThisSaveDataSecureValue(u32 secure_value_slot, u64 secure_value) override; + + ResultVal> GetThisSaveDataSecureValue( + u32 secure_value_slot) override; + +protected: + template + void serialize(Archive& ar, const unsigned int); + friend class boost::serialization::access; +}; +} // namespace FileSys + +BOOST_CLASS_EXPORT_KEY(FileSys::DefaultSecureValueBackend) \ No newline at end of file diff --git a/src/core/file_sys/secure_value_backend_artic.cpp b/src/core/file_sys/secure_value_backend_artic.cpp new file mode 100644 index 000000000..11f0449ab --- /dev/null +++ b/src/core/file_sys/secure_value_backend_artic.cpp @@ -0,0 +1,119 @@ +// Copyright 2024 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "common/archives.h" +#include "core/file_sys/archive_artic.h" +#include "core/file_sys/secure_value_backend_artic.h" + +SERIALIZE_EXPORT_IMPL(FileSys::ArticSecureValueBackend) + +namespace FileSys { +Result ArticSecureValueBackend::ObsoletedSetSaveDataSecureValue(u32 unique_id, u8 title_variation, + u32 secure_value_slot, + u64 secure_value) { + auto req = client->NewRequest("FSUSER_ObsSetSaveDataSecureVal"); + + req.AddParameterU64(secure_value); + req.AddParameterU32(secure_value_slot); + req.AddParameterU32(unique_id); + req.AddParameterU8(title_variation); + + return ArticArchive::RespResult(client->Send(req)); +} + +ResultVal> ArticSecureValueBackend::ObsoletedGetSaveDataSecureValue( + u32 unique_id, u8 title_variation, u32 secure_value_slot) { + + auto req = client->NewRequest("FSUSER_ObsGetSaveDataSecureVal"); + + req.AddParameterU32(secure_value_slot); + req.AddParameterU32(unique_id); + req.AddParameterU8(title_variation); + + auto resp = client->Send(req); + auto res = ArticArchive::RespResult(resp); + if (res.IsError()) + return res; + + struct { + bool exists; + u64 secure_value; + } secure_value_result; + static_assert(sizeof(secure_value_result) == 0x10); + + auto output_buf = resp->GetResponseBuffer(0); + if (!output_buf.has_value()) + return res; + + if (output_buf->second != sizeof(secure_value_result)) + return ResultUnknown; + + memcpy(&secure_value_result, output_buf->first, output_buf->second); + return std::make_tuple(secure_value_result.exists, secure_value_result.secure_value); +} + +Result ArticSecureValueBackend::ControlSecureSave(u32 action, u8* input, size_t input_size, + u8* output, size_t output_size) { + auto req = client->NewRequest("FSUSER_ControlSecureSave"); + + req.AddParameterU32(action); + req.AddParameterBuffer(input, input_size); + req.AddParameterU32(static_cast(output_size)); + + auto resp = client->Send(req); + auto res = ArticArchive::RespResult(resp); + if (res.IsError()) + return res; + + auto output_buf = resp->GetResponseBuffer(0); + if (!output_buf.has_value()) + return res; + + if (output_buf->second != output_size) + return ResultUnknown; + + memcpy(output, output_buf->first, output_buf->second); + return res; +} + +Result ArticSecureValueBackend::SetThisSaveDataSecureValue(u32 secure_value_slot, + u64 secure_value) { + auto req = client->NewRequest("FSUSER_SetThisSaveDataSecVal"); + + req.AddParameterU32(secure_value_slot); + req.AddParameterU64(secure_value); + + return ArticArchive::RespResult(client->Send(req)); +} + +ResultVal> ArticSecureValueBackend::GetThisSaveDataSecureValue( + u32 secure_value_slot) { + auto req = client->NewRequest("FSUSER_GetThisSaveDataSecVal"); + + req.AddParameterU32(secure_value_slot); + + auto resp = client->Send(req); + auto res = ArticArchive::RespResult(resp); + if (res.IsError()) + return res; + + struct { + bool exists; + bool isGamecard; + u64 secure_value; + } secure_value_result; + static_assert(sizeof(secure_value_result) == 0x10); + + auto output_buf = resp->GetResponseBuffer(0); + if (!output_buf.has_value()) + return res; + + if (output_buf->second != sizeof(secure_value_result)) + return ResultUnknown; + + memcpy(&secure_value_result, output_buf->first, output_buf->second); + return std::make_tuple(secure_value_result.exists, secure_value_result.isGamecard, + secure_value_result.secure_value); +} +} // namespace FileSys \ No newline at end of file diff --git a/src/core/file_sys/secure_value_backend_artic.h b/src/core/file_sys/secure_value_backend_artic.h new file mode 100644 index 000000000..23d6fae1b --- /dev/null +++ b/src/core/file_sys/secure_value_backend_artic.h @@ -0,0 +1,53 @@ +// Copyright 2024 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include "tuple" + +#include "common/common_types.h" +#include "core/file_sys/secure_value_backend.h" +#include "core/hle/result.h" +#include "core/hle/service/fs/archive.h" +#include "network/artic_base/artic_base_client.h" + +namespace FileSys { +class ArticSecureValueBackend : public SecureValueBackend { +public: + ArticSecureValueBackend(const std::shared_ptr& _client) + : client(_client) {} + + Result ObsoletedSetSaveDataSecureValue(u32 unique_id, u8 title_variation, u32 secure_value_slot, + u64 secure_value) override; + + ResultVal> ObsoletedGetSaveDataSecureValue( + u32 unique_id, u8 title_variation, u32 secure_value_slot) override; + + Result ControlSecureSave(u32 action, u8* input, size_t input_size, u8* output, + size_t output_size) override; + + Result SetThisSaveDataSecureValue(u32 secure_value_slot, u64 secure_value) override; + + ResultVal> GetThisSaveDataSecureValue( + u32 secure_value_slot) override; + + bool BackendIsSlow() override { + return true; + } + +protected: + ArticSecureValueBackend() = default; + + template + void serialize(Archive& ar, const unsigned int) { + ar& boost::serialization::base_object(*this); + } + friend class boost::serialization::access; + +private: + std::shared_ptr client; +}; +} // namespace FileSys + +BOOST_CLASS_EXPORT_KEY(FileSys::ArticSecureValueBackend) \ No newline at end of file diff --git a/src/core/hle/ipc_helpers.h b/src/core/hle/ipc_helpers.h index 9b7dbe73a..46c87514e 100644 --- a/src/core/hle/ipc_helpers.h +++ b/src/core/hle/ipc_helpers.h @@ -58,6 +58,11 @@ public: : RequestBuilder( context, Header{MakeHeader(command_id, normal_params_size, translate_params_size)}) {} + RequestBuilder(Kernel::HLERequestContext& context, unsigned normal_params_size, + unsigned translate_params_size) + : RequestBuilder(context, Header{MakeHeader(context.CommandID(), normal_params_size, + translate_params_size)}) {} + // Validate on destruction, as there shouldn't be any case where we don't want it ~RequestBuilder() { ValidateHeader(); diff --git a/src/core/hle/kernel/hle_ipc.h b/src/core/hle/kernel/hle_ipc.h index 5b6ab88b5..64a6ed949 100644 --- a/src/core/hle/kernel/hle_ipc.h +++ b/src/core/hle/kernel/hle_ipc.h @@ -206,6 +206,11 @@ public: return {cmd_buf[0]}; } + /// Returns the Command ID from the IPC command buffer. + u16 CommandID() const { + return static_cast(CommandHeader().command_id.Value()); + } + /** * Returns the session through which this request was made. This can be used as a map key to * access per-client data on services. diff --git a/src/core/hle/service/am/am.cpp b/src/core/hle/service/am/am.cpp index 1807fc185..cd3f49651 100644 --- a/src/core/hle/service/am/am.cpp +++ b/src/core/hle/service/am/am.cpp @@ -266,7 +266,7 @@ ResultVal CIAFile::WriteContentData(u64 offset, std::size_t length, } ResultVal CIAFile::Write(u64 offset, std::size_t length, bool flush, - const u8* buffer) { + bool update_timestamp, const u8* buffer) { written += length; // TODO(shinyquagsire23): Can we assume that things will only be written in sequence? @@ -347,7 +347,7 @@ bool CIAFile::SetSize(u64 size) const { return false; } -bool CIAFile::Close() const { +bool CIAFile::Close() { bool complete = install_state >= CIAInstallState::TMDLoaded && content_written.size() == container.GetTitleMetadata().GetContentCount() && @@ -419,7 +419,7 @@ ResultVal TicketFile::Read(u64 offset, std::size_t length, u8* buff } ResultVal TicketFile::Write(u64 offset, std::size_t length, bool flush, - const u8* buffer) { + bool update_timestamp, const u8* buffer) { written += length; data.resize(written); std::memcpy(data.data() + offset, buffer, length); @@ -434,7 +434,7 @@ bool TicketFile::SetSize(u64 size) const { return false; } -bool TicketFile::Close() const { +bool TicketFile::Close() { FileSys::Ticket ticket; if (ticket.Load(data, 0) == Loader::ResultStatus::Success) { LOG_WARNING(Service_AM, "Discarding ticket for {:#016X}.", ticket.GetTitleID()); @@ -480,7 +480,7 @@ InstallStatus InstallCIA(const std::string& path, while (total_bytes_read != file_size) { std::size_t bytes_read = file.ReadBytes(buffer.data(), buffer.size()); auto result = installFile.Write(static_cast(total_bytes_read), bytes_read, true, - static_cast(buffer.data())); + false, static_cast(buffer.data())); if (update_callback) { update_callback(total_bytes_read, file_size); @@ -590,7 +590,8 @@ InstallStatus InstallFromNus(u64 title_id, int version) { const u64 offset = Common::AlignUp(current_offset + data.size(), FileSys::CIA_SECTION_ALIGNMENT); data.resize(offset - current_offset, 0); - const auto result = install_file.Write(current_offset, data.size(), true, data.data()); + const auto result = + install_file.Write(current_offset, data.size(), true, false, data.data()); if (result.Failed()) { LOG_ERROR(Service_AM, "CIA file installation aborted with error code {:08x}", result.Code().raw); @@ -1464,9 +1465,9 @@ public: return file->backend->Read(offset + file_offset, length, buffer); } - ResultVal Write(u64 offset, std::size_t length, bool flush, + ResultVal Write(u64 offset, std::size_t length, bool flush, bool update_timestamp, const u8* buffer) override { - return file->backend->Write(offset + file_offset, length, flush, buffer); + return file->backend->Write(offset + file_offset, length, flush, update_timestamp, buffer); } u64 GetSize() const override { @@ -1475,7 +1476,7 @@ public: bool SetSize(u64 size) const override { return false; } - bool Close() const override { + bool Close() override { return false; } void Flush() const override {} diff --git a/src/core/hle/service/am/am.h b/src/core/hle/service/am/am.h index 26b9b1056..7ef96f985 100644 --- a/src/core/hle/service/am/am.h +++ b/src/core/hle/service/am/am.h @@ -111,11 +111,11 @@ public: Result WriteTicket(); Result WriteTitleMetadata(); ResultVal WriteContentData(u64 offset, std::size_t length, const u8* buffer); - ResultVal Write(u64 offset, std::size_t length, bool flush, + ResultVal Write(u64 offset, std::size_t length, bool flush, bool update_timestamp, const u8* buffer) override; u64 GetSize() const override; bool SetSize(u64 size) const override; - bool Close() const override; + bool Close() override; void Flush() const override; private: @@ -146,11 +146,11 @@ public: ~TicketFile(); ResultVal Read(u64 offset, std::size_t length, u8* buffer) const override; - ResultVal Write(u64 offset, std::size_t length, bool flush, + ResultVal Write(u64 offset, std::size_t length, bool flush, bool update_timestamp, const u8* buffer) override; u64 GetSize() const override; bool SetSize(u64 size) const override; - bool Close() const override; + bool Close() override; void Flush() const override; private: diff --git a/src/core/hle/service/apt/apt.cpp b/src/core/hle/service/apt/apt.cpp index 3e552dfc4..58cac93e3 100644 --- a/src/core/hle/service/apt/apt.cpp +++ b/src/core/hle/service/apt/apt.cpp @@ -217,7 +217,7 @@ bool Module::LoadSharedFont() { const FileSys::Path file_path(std::vector(20, 0)); FileSys::Mode open_mode = {}; open_mode.read_flag.Assign(1); - auto file_result = archive.OpenFile(file_path, open_mode); + auto file_result = archive.OpenFile(file_path, open_mode, 0); if (file_result.Failed()) return false; diff --git a/src/core/hle/service/boss/online_service.cpp b/src/core/hle/service/boss/online_service.cpp index d289c910b..cd587aa51 100644 --- a/src/core/hle/service/boss/online_service.cpp +++ b/src/core/hle/service/boss/online_service.cpp @@ -75,7 +75,7 @@ Result OnlineService::InitializeSession(u64 init_program_id) { boss_system_save_data_archive = std::move(archive_result).Unwrap(); } else if (archive_result.Code() == FileSys::ResultNotFound) { // If the archive didn't exist, create the files inside - systemsavedata_factory.Format(archive_path, FileSys::ArchiveFormatInfo(), 0); + systemsavedata_factory.Format(archive_path, FileSys::ArchiveFormatInfo(), 0, 0, 0); // Open it again to get a valid archive now that the folder exists auto create_archive_result = systemsavedata_factory.Open(archive_path, 0); diff --git a/src/core/hle/service/cecd/cecd.cpp b/src/core/hle/service/cecd/cecd.cpp index bbd4cc0a2..112e766af 100644 --- a/src/core/hle/service/cecd/cecd.cpp +++ b/src/core/hle/service/cecd/cecd.cpp @@ -116,7 +116,7 @@ void Module::Interface::Open(Kernel::HLERequestContext& ctx) { std::vector program_id(8); u64_le le_program_id = cecd->system.Kernel().GetCurrentProcess()->codeset->program_id; std::memcpy(program_id.data(), &le_program_id, sizeof(u64)); - session_data->file->Write(0, sizeof(u64), true, program_id.data()); + session_data->file->Write(0, sizeof(u64), true, false, program_id.data()); session_data->file->Close(); } } @@ -373,7 +373,7 @@ void Module::Interface::Write(Kernel::HLERequestContext& ctx) { } [[maybe_unused]] const u32 bytes_written = static_cast( - session_data->file->Write(0, buffer.size(), true, buffer.data()).Unwrap()); + session_data->file->Write(0, buffer.size(), true, false, buffer.data()).Unwrap()); session_data->file->Close(); rb.Push(ResultSuccess); @@ -435,7 +435,7 @@ void Module::Interface::WriteMessage(Kernel::HLERequestContext& ctx) { msg_header.forward_count, msg_header.user_data); [[maybe_unused]] const u32 bytes_written = - static_cast(message->Write(0, buffer_size, true, buffer.data()).Unwrap()); + static_cast(message->Write(0, buffer_size, true, false, buffer.data()).Unwrap()); message->Close(); rb.Push(ResultSuccess); @@ -522,7 +522,7 @@ void Module::Interface::WriteMessageWithHMAC(Kernel::HLERequestContext& ctx) { std::memcpy(buffer.data() + hmac_offset, hmac_digest.data(), hmac_size); [[maybe_unused]] const u32 bytes_written = - static_cast(message->Write(0, buffer_size, true, buffer.data()).Unwrap()); + static_cast(message->Write(0, buffer_size, true, false, buffer.data()).Unwrap()); message->Close(); rb.Push(ResultSuccess); @@ -607,7 +607,7 @@ void Module::Interface::SetData(Kernel::HLERequestContext& ctx) { cecd->CheckAndUpdateFile(CecDataPathType::OutboxIndex, ncch_program_id, buffer); - file->Write(0, buffer.size(), true, buffer.data()); + file->Write(0, buffer.size(), true, false, buffer.data()); file->Close(); } } @@ -764,8 +764,8 @@ void Module::Interface::OpenAndWrite(Kernel::HLERequestContext& ctx) { cecd->CheckAndUpdateFile(path_type, ncch_program_id, buffer); } - [[maybe_unused]] const u32 bytes_written = - static_cast(file->Write(0, buffer.size(), true, buffer.data()).Unwrap()); + [[maybe_unused]] const u32 bytes_written = static_cast( + file->Write(0, buffer.size(), true, false, buffer.data()).Unwrap()); file->Close(); rb.Push(ResultSuccess); @@ -1409,7 +1409,7 @@ Module::Module(Core::System& system) : system(system) { cecd_system_save_data_archive = std::move(archive_result).Unwrap(); } else { // Format the archive to create the directories - systemsavedata_factory.Format(archive_path, FileSys::ArchiveFormatInfo(), 0); + systemsavedata_factory.Format(archive_path, FileSys::ArchiveFormatInfo(), 0, 0, 0); // Open it again to get a valid archive now that the folder exists cecd_system_save_data_archive = systemsavedata_factory.Open(archive_path, 0).Unwrap(); @@ -1442,7 +1442,7 @@ Module::Module(Core::System& system) : system(system) { eventlog_buffer[1] = 0x41; eventlog_buffer[2] = 0x12; - eventlog->Write(0, eventlog_size, true, eventlog_buffer.data()); + eventlog->Write(0, eventlog_size, true, false, eventlog_buffer.data()); eventlog->Close(); /// MBoxList____ resides within the root CEC/ directory. @@ -1464,7 +1464,7 @@ Module::Module(Core::System& system) : system(system) { // mboxlist_buffer[2-3] are already zeroed mboxlist_buffer[4] = 0x01; - mboxlist->Write(0, mboxlist_size, true, mboxlist_buffer.data()); + mboxlist->Write(0, mboxlist_size, true, false, mboxlist_buffer.data()); mboxlist->Close(); } } diff --git a/src/core/hle/service/cfg/cfg.cpp b/src/core/hle/service/cfg/cfg.cpp index d118250ba..864369215 100644 --- a/src/core/hle/service/cfg/cfg.cpp +++ b/src/core/hle/service/cfg/cfg.cpp @@ -565,7 +565,7 @@ Result Module::UpdateConfigNANDSavegame() { ASSERT_MSG(config_result.Succeeded(), "could not open file"); auto config = std::move(config_result).Unwrap(); - config->Write(0, CONFIG_SAVEFILE_SIZE, 1, cfg_config_file_buffer.data()); + config->Write(0, CONFIG_SAVEFILE_SIZE, true, false, cfg_config_file_buffer.data()); return ResultSuccess; } @@ -625,7 +625,7 @@ Result Module::LoadConfigNANDSaveFile() { // If the archive didn't exist, create the files inside if (archive_result.Code() == FileSys::ResultNotFound) { // Format the archive to create the directories - systemsavedata_factory.Format(archive_path, FileSys::ArchiveFormatInfo(), 0); + systemsavedata_factory.Format(archive_path, FileSys::ArchiveFormatInfo(), 0, 0, 0); // Open it again to get a valid archive now that the folder exists cfg_system_save_data_archive = systemsavedata_factory.Open(archive_path, 0).Unwrap(); diff --git a/src/core/hle/service/fs/archive.cpp b/src/core/hle/service/fs/archive.cpp index 44e303129..f70f03c7e 100644 --- a/src/core/hle/service/fs/archive.cpp +++ b/src/core/hle/service/fs/archive.cpp @@ -67,10 +67,24 @@ ResultVal ArchiveManager::OpenArchive(ArchiveIdCode id_code, } Result ArchiveManager::CloseArchive(ArchiveHandle handle) { - if (handle_map.erase(handle) == 0) + auto itr = handle_map.find(handle); + if (itr != handle_map.end()) { + itr->second->Close(); + } else { return FileSys::ResultInvalidArchiveHandle; - else - return ResultSuccess; + } + handle_map.erase(itr); + return ResultSuccess; +} + +Result ArchiveManager::ControlArchive(ArchiveHandle handle, u32 action, u8* input, + size_t input_size, u8* output, size_t output_size) { + auto itr = handle_map.find(handle); + if (itr != handle_map.end()) { + return itr->second->Control(action, input, input_size, output, output_size); + } else { + return FileSys::ResultInvalidArchiveHandle; + } } // TODO(yuriks): This might be what the fs:REG service is for. See the Register/Unregister calls in @@ -90,14 +104,14 @@ Result ArchiveManager::RegisterArchiveType(std::unique_ptr>, std::chrono::nanoseconds> ArchiveManager::OpenFileFromArchive(ArchiveHandle archive_handle, const FileSys::Path& path, - const FileSys::Mode mode) { + const FileSys::Mode mode, u32 attributes) { ArchiveBackend* archive = GetArchive(archive_handle); if (archive == nullptr) { return std::make_pair(FileSys::ResultInvalidArchiveHandle, std::chrono::nanoseconds{0}); } const std::chrono::nanoseconds open_timeout_ns{archive->GetOpenDelayNs()}; - auto backend = archive->OpenFile(path, mode); + auto backend = archive->OpenFile(path, mode, attributes); if (backend.Failed()) { return std::make_pair(backend.Code(), open_timeout_ns); } @@ -151,21 +165,21 @@ Result ArchiveManager::DeleteDirectoryRecursivelyFromArchive(ArchiveHandle archi } Result ArchiveManager::CreateFileInArchive(ArchiveHandle archive_handle, const FileSys::Path& path, - u64 file_size) { + u64 file_size, u32 attributes) { ArchiveBackend* archive = GetArchive(archive_handle); if (archive == nullptr) return FileSys::ResultInvalidArchiveHandle; - return archive->CreateFile(path, file_size); + return archive->CreateFile(path, file_size, attributes); } Result ArchiveManager::CreateDirectoryFromArchive(ArchiveHandle archive_handle, - const FileSys::Path& path) { + const FileSys::Path& path, u32 attributes) { ArchiveBackend* archive = GetArchive(archive_handle); if (archive == nullptr) return FileSys::ResultInvalidArchiveHandle; - return archive->CreateDirectory(path); + return archive->CreateDirectory(path, attributes); } Result ArchiveManager::RenameDirectoryBetweenArchives(ArchiveHandle src_archive_handle, @@ -210,13 +224,15 @@ ResultVal ArchiveManager::GetFreeBytesInArchive(ArchiveHandle archive_handl Result ArchiveManager::FormatArchive(ArchiveIdCode id_code, const FileSys::ArchiveFormatInfo& format_info, - const FileSys::Path& path, u64 program_id) { + const FileSys::Path& path, u64 program_id, + u32 directory_buckets, u32 file_buckets) { auto archive_itr = id_code_map.find(id_code); if (archive_itr == id_code_map.end()) { return UnimplementedFunction(ErrorModule::FS); // TODO(Subv): Find the right error } - return archive_itr->second->Format(path, format_info, program_id); + return archive_itr->second->Format(path, format_info, program_id, directory_buckets, + file_buckets); } ResultVal ArchiveManager::GetArchiveFormatInfo( @@ -229,10 +245,10 @@ ResultVal ArchiveManager::GetArchiveFormatInfo( return archive->second->GetFormatInfo(archive_path, program_id); } -Result ArchiveManager::CreateExtSaveData(MediaType media_type, u32 high, u32 low, +Result ArchiveManager::CreateExtSaveData(MediaType media_type, u8 unknown, u32 high, u32 low, std::span smdh_icon, const FileSys::ArchiveFormatInfo& format_info, - u64 program_id) { + u64 program_id, u64 total_size) { // Construct the binary path to the archive first FileSys::Path path = FileSys::ConstructExtDataBinaryPath(static_cast(media_type), high, low); @@ -246,37 +262,26 @@ Result ArchiveManager::CreateExtSaveData(MediaType media_type, u32 high, u32 low auto ext_savedata = static_cast(archive->second.get()); - Result result = ext_savedata->Format(path, format_info, program_id); + Result result = ext_savedata->FormatAsExtData(path, format_info, unknown, program_id, + total_size, smdh_icon); if (result.IsError()) { return result; } - ext_savedata->WriteIcon(path, smdh_icon); return ResultSuccess; } -Result ArchiveManager::DeleteExtSaveData(MediaType media_type, u32 high, u32 low) { - // Construct the binary path to the archive first - FileSys::Path path = - FileSys::ConstructExtDataBinaryPath(static_cast(media_type), high, low); +Result ArchiveManager::DeleteExtSaveData(MediaType media_type, u8 unknown, u32 high, u32 low) { + auto archive = id_code_map.find(media_type == MediaType::NAND ? ArchiveIdCode::SharedExtSaveData + : ArchiveIdCode::ExtSaveData); - std::string media_type_directory; - if (media_type == MediaType::NAND) { - media_type_directory = FileUtil::GetUserPath(FileUtil::UserPath::NANDDir); - } else if (media_type == MediaType::SDMC) { - media_type_directory = FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir); - } else { - LOG_ERROR(Service_FS, "Unsupported media type {}", media_type); - return ResultUnknown; // TODO(Subv): Find the right error code + if (archive == id_code_map.end()) { + return UnimplementedFunction(ErrorModule::FS); // TODO(Subv): Find the right error } - // Delete all directories (/user, /boss) and the icon file. - std::string base_path = - FileSys::GetExtDataContainerPath(media_type_directory, media_type == MediaType::NAND); - std::string extsavedata_path = FileSys::GetExtSaveDataPath(base_path, path); - if (FileUtil::Exists(extsavedata_path) && !FileUtil::DeleteDirRecursively(extsavedata_path)) - return ResultUnknown; // TODO(Subv): Find the right error code - return ResultSuccess; + auto ext_savedata = static_cast(archive->second.get()); + + return ext_savedata->DeleteExtData(media_type, unknown, high, low); } Result ArchiveManager::DeleteSystemSaveData(u32 high, u32 low) { @@ -317,6 +322,24 @@ ResultVal ArchiveManager::GetArchiveResource(MediaType media_ty return resource; } +Result ArchiveManager::SetSaveDataSecureValue(ArchiveHandle archive_handle, u32 secure_value_slot, + u64 secure_value, bool flush) { + ArchiveBackend* archive = GetArchive(archive_handle); + if (archive == nullptr) { + return FileSys::ResultInvalidArchiveHandle; + } + return archive->SetSaveDataSecureValue(secure_value_slot, secure_value, flush); +} + +ResultVal> ArchiveManager::GetSaveDataSecureValue( + ArchiveHandle archive_handle, u32 secure_value_slot) { + ArchiveBackend* archive = GetArchive(archive_handle); + if (archive == nullptr) { + return FileSys::ResultInvalidArchiveHandle; + } + return archive->GetSaveDataSecureValue(secure_value_slot); +} + void ArchiveManager::RegisterArchiveTypes() { // TODO(Subv): Add the other archive types (see here for the known types: // http://3dbrew.org/wiki/FS:OpenArchive#Archive_idcodes). @@ -337,7 +360,7 @@ void ArchiveManager::RegisterArchiveTypes() { sdmc_directory); // Create the SaveData archive - auto sd_savedata_source = std::make_shared(sdmc_directory); + sd_savedata_source = std::make_shared(sdmc_directory); auto savedata_factory = std::make_unique(sd_savedata_source); RegisterArchiveType(std::move(savedata_factory), ArchiveIdCode::SaveData); auto other_savedata_permitted_factory = @@ -373,6 +396,23 @@ void ArchiveManager::RegisterArchiveTypes() { RegisterArchiveType(std::move(selfncch_factory), ArchiveIdCode::SelfNCCH); } +bool ArchiveManager::ArchiveIsSlow(ArchiveIdCode archive_id) { + auto itr = id_code_map.find(archive_id); + if (itr == id_code_map.end() || itr->second.get() == nullptr) { + return false; + } + + return itr->second->IsSlow(); +} + +bool ArchiveManager::ArchiveIsSlow(ArchiveHandle archive_handle) { + ArchiveBackend* archive = GetArchive(archive_handle); + if (archive == nullptr) { + return false; + } + return archive->IsSlow(); +} + void ArchiveManager::RegisterSelfNCCH(Loader::AppLoader& app_loader) { auto itr = id_code_map.find(ArchiveIdCode::SelfNCCH); if (itr == id_code_map.end()) { @@ -385,6 +425,35 @@ void ArchiveManager::RegisterSelfNCCH(Loader::AppLoader& app_loader) { factory->Register(app_loader); } +void ArchiveManager::RegisterArticSaveDataSource( + std::shared_ptr& client) { + if (!sd_savedata_source.get()) { + LOG_ERROR(Service_FS, "Could not register artic save data source."); + return; + } + sd_savedata_source->RegisterArtic(client); +} + +void ArchiveManager::RegisterArticExtData(std::shared_ptr& client) { + for (auto it : {ArchiveIdCode::ExtSaveData, ArchiveIdCode::SharedExtSaveData, + ArchiveIdCode::BossExtSaveData}) { + auto itr = id_code_map.find(it); + if (itr == id_code_map.end() || itr->second.get() == nullptr) { + continue; + } + reinterpret_cast(itr->second.get()) + ->RegisterArtic(client); + } +} + +void ArchiveManager::RegisterArticNCCH(std::shared_ptr& client) { + auto itr = id_code_map.find(ArchiveIdCode::NCCH); + if (itr == id_code_map.end() || itr->second.get() == nullptr) { + return; + } + reinterpret_cast(itr->second.get())->RegisterArtic(client); +} + ArchiveManager::ArchiveManager(Core::System& system) : system(system) { RegisterArchiveTypes(); } diff --git a/src/core/hle/service/fs/archive.h b/src/core/hle/service/fs/archive.h index 245e929f9..a20713066 100644 --- a/src/core/hle/service/fs/archive.h +++ b/src/core/hle/service/fs/archive.h @@ -12,9 +12,11 @@ #include #include "common/common_types.h" #include "core/file_sys/archive_backend.h" +#include "core/file_sys/archive_source_sd_savedata.h" #include "core/hle/result.h" #include "core/hle/service/fs/directory.h" #include "core/hle/service/fs/file.h" +#include "network/artic_base/artic_base_client.h" /// The unique system identifier hash, also known as ID0 static constexpr char SYSTEM_ID[]{"00000000000000000000000000000000"}; @@ -67,6 +69,19 @@ struct ArchiveResource { }; static_assert(sizeof(ArchiveResource) == 0x10, "ArchiveResource has incorrect size"); +struct ExtSaveDataInfo { + u8 media_type; + u8 unknown; + u16 reserved1; + u32 save_id_low; + u32 save_id_high; + u32 reserved2; +}; +static_assert(sizeof(ExtSaveDataInfo) == 0x10, "ExtSaveDataInfo struct has incorrect size"); +static_assert(std::is_trivial(), "ExtSaveDataInfo should be trivial"); +static_assert(std::is_trivially_copyable(), + "ExtSaveDataInfo should be trivially copyable"); + using FileSys::ArchiveBackend; using FileSys::ArchiveFactory; @@ -90,6 +105,9 @@ public: */ Result CloseArchive(ArchiveHandle handle); + Result ControlArchive(ArchiveHandle handle, u32 action, u8* input, size_t input_size, + u8* output, size_t output_size); + /** * Open a File from an Archive * @param archive_handle Handle to an open Archive object @@ -98,7 +116,8 @@ public: * @return Pair containing the opened File object and the open delay */ std::pair>, std::chrono::nanoseconds> OpenFileFromArchive( - ArchiveHandle archive_handle, const FileSys::Path& path, FileSys::Mode mode); + ArchiveHandle archive_handle, const FileSys::Path& path, FileSys::Mode mode, + u32 attributes); /** * Delete a File from an Archive @@ -146,7 +165,7 @@ public: * @return File creation result code */ Result CreateFileInArchive(ArchiveHandle archive_handle, const FileSys::Path& path, - u64 file_size); + u64 file_size, u32 attributes); /** * Create a Directory from an Archive @@ -154,7 +173,8 @@ public: * @param path Path to the Directory inside of the Archive * @return Whether creation of directory succeeded */ - Result CreateDirectoryFromArchive(ArchiveHandle archive_handle, const FileSys::Path& path); + Result CreateDirectoryFromArchive(ArchiveHandle archive_handle, const FileSys::Path& path, + u32 attributes); /** * Rename a Directory between two Archives @@ -195,7 +215,8 @@ public: * @return Result 0 on success or the corresponding code on error */ Result FormatArchive(ArchiveIdCode id_code, const FileSys::ArchiveFormatInfo& format_info, - const FileSys::Path& path, u64 program_id); + const FileSys::Path& path, u64 program_id, u32 directory_buckets, + u32 file_buckets); /** * Retrieves the format info about the archive of the specified type and path. @@ -219,8 +240,10 @@ public: * @param program_id the program ID of the client that requests the operation * @return Result 0 on success or the corresponding code on error */ - Result CreateExtSaveData(MediaType media_type, u32 high, u32 low, std::span smdh_icon, - const FileSys::ArchiveFormatInfo& format_info, u64 program_id); + Result CreateExtSaveData(MediaType media_type, u8 unknown, u32 high, u32 low, + std::span smdh_icon, + const FileSys::ArchiveFormatInfo& format_info, u64 program_id, + u64 total_size); /** * Deletes the SharedExtSaveData archive for the specified extdata ID @@ -229,7 +252,7 @@ public: * @param low The low word of the extdata id to delete * @return Result 0 on success or the corresponding code on error */ - Result DeleteExtSaveData(MediaType media_type, u32 high, u32 low); + Result DeleteExtSaveData(MediaType media_type, u8 unknown, u32 high, u32 low); /** * Deletes the SystemSaveData archive folder for the specified save data id @@ -254,9 +277,25 @@ public: */ ResultVal GetArchiveResource(MediaType media_type) const; + Result SetSaveDataSecureValue(ArchiveHandle archive_handle, u32 secure_value_slot, + u64 secure_value, bool flush); + + ResultVal> GetSaveDataSecureValue(ArchiveHandle archive_handle, + u32 secure_value_slot); + + bool ArchiveIsSlow(ArchiveIdCode archive_id); + + bool ArchiveIsSlow(ArchiveHandle archive_handle); + /// Registers a new NCCH file with the SelfNCCH archive factory void RegisterSelfNCCH(Loader::AppLoader& app_loader); + void RegisterArticSaveDataSource(std::shared_ptr& client); + + void RegisterArticExtData(std::shared_ptr& client); + + void RegisterArticNCCH(std::shared_ptr& client); + private: Core::System& system; @@ -285,11 +324,17 @@ private: std::unordered_map> handle_map; ArchiveHandle next_handle = 1; + /** + * Savedata source + */ + std::shared_ptr sd_savedata_source; + template void serialize(Archive& ar, const unsigned int) { ar& id_code_map; ar& handle_map; ar& next_handle; + ar& sd_savedata_source; } friend class boost::serialization::access; }; diff --git a/src/core/hle/service/fs/file.cpp b/src/core/hle/service/fs/file.cpp index f9134a848..347aac3ae 100644 --- a/src/core/hle/service/fs/file.cpp +++ b/src/core/hle/service/fs/file.cpp @@ -169,10 +169,9 @@ void File::Write(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); u64 offset = rp.Pop(); u32 length = rp.Pop(); - u32 flush = rp.Pop(); - auto& buffer = rp.PopMappedBuffer(); - LOG_TRACE(Service_FS, "Write {}: offset=0x{:x} length={}, flush=0x{:x}", GetName(), offset, - length, flush); + u32 flags = rp.Pop(); + LOG_TRACE(Service_FS, "Write {}: offset=0x{:x} length={}, flags=0x{:x}", GetName(), offset, + length, flags); IPC::RequestBuilder rb = rp.MakeBuilder(2, 2); @@ -182,25 +181,75 @@ void File::Write(Kernel::HLERequestContext& ctx) { if (file->subfile) { rb.Push(FileSys::ResultUnsupportedOpenFlags); rb.Push(0); + rb.PushMappedBuffer(rp.PopMappedBuffer()); + return; + } + bool flush = (flags & 0xFF) != 0, update_timestamp = (flags & 0xFF00) != 0; + + if (!backend->AllowsCachedReads()) { + std::vector data(length); + auto& buffer = rp.PopMappedBuffer(); + buffer.Read(data.data(), 0, data.size()); + ResultVal written = + backend->Write(offset, data.size(), flush, update_timestamp, data.data()); + + // Update file size + file->size = backend->GetSize(); + + if (written.Failed()) { + rb.Push(written.Code()); + rb.Push(0); + } else { + rb.Push(ResultSuccess); + rb.Push(static_cast(*written)); + } rb.PushMappedBuffer(buffer); return; } - std::vector data(length); - buffer.Read(data.data(), 0, data.size()); - ResultVal written = backend->Write(offset, data.size(), flush != 0, data.data()); + struct AsyncData { + // Input + u32 length; + u64 offset; + bool flush; + bool update_timestamp; + Kernel::MappedBuffer* buffer; + FileSessionSlot* file; - // Update file size - file->size = backend->GetSize(); + // Output + ResultVal written; + }; + auto async_data = std::make_shared(); + async_data->length = length; + async_data->offset = offset; + async_data->flush = flush; + async_data->update_timestamp = update_timestamp; + async_data->buffer = &rp.PopMappedBuffer(); + async_data->file = file; - if (written.Failed()) { - rb.Push(written.Code()); - rb.Push(0); - } else { - rb.Push(ResultSuccess); - rb.Push(static_cast(*written)); - } - rb.PushMappedBuffer(buffer); + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + std::vector data(async_data->length); + async_data->buffer->Read(data.data(), 0, data.size()); + async_data->written = backend->Write(async_data->offset, data.size(), async_data->flush, + async_data->update_timestamp, data.data()); + + // Update file size + async_data->file->size = backend->GetSize(); + return 0; + }, + [async_data](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 2, 2); + if (async_data->written.Failed()) { + rb.Push(async_data->written.Code()); + rb.Push(0); + } else { + rb.Push(ResultSuccess); + rb.Push(static_cast(*async_data->written)); + } + rb.PushMappedBuffer(*async_data->buffer); + }, + true); } void File::GetSize(Kernel::HLERequestContext& ctx) { @@ -219,17 +268,32 @@ void File::SetSize(Kernel::HLERequestContext& ctx) { FileSessionSlot* file = GetSessionData(ctx.Session()); - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - // SetSize can not be called on subfiles. if (file->subfile) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(FileSys::ResultUnsupportedOpenFlags); return; } - file->size = size; - backend->SetSize(size); - rb.Push(ResultSuccess); + if (!backend->AllowsCachedReads()) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + file->size = size; + backend->SetSize(size); + rb.Push(ResultSuccess); + return; + } + + ctx.RunAsync( + [file, size, this](Kernel::HLERequestContext& ctx) { + file->size = size; + backend->SetSize(size); + return 0; + }, + [](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 1, 0); + rb.Push(ResultSuccess); + }, + true); } void File::Close(Kernel::HLERequestContext& ctx) { @@ -240,26 +304,53 @@ void File::Close(Kernel::HLERequestContext& ctx) { LOG_WARNING(Service_FS, "Closing File backend but {} clients still connected", connected_sessions.size()); - backend->Close(); - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(ResultSuccess); + if (!backend->AllowsCachedReads()) { + backend->Close(); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultSuccess); + return; + } + + ctx.RunAsync( + [this](Kernel::HLERequestContext& ctx) { + backend->Close(); + return 0; + }, + [](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 1, 0); + rb.Push(ResultSuccess); + }, + true); } void File::Flush(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - const FileSessionSlot* file = GetSessionData(ctx.Session()); // Subfiles can not be flushed. if (file->subfile) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(FileSys::ResultUnsupportedOpenFlags); return; } - backend->Flush(); - rb.Push(ResultSuccess); + if (!backend->AllowsCachedReads()) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + backend->Flush(); + rb.Push(ResultSuccess); + } + + ctx.RunAsync( + [this](Kernel::HLERequestContext& ctx) { + backend->Flush(); + return 0; + }, + [](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 1, 0); + rb.Push(ResultSuccess); + }, + true); } void File::SetPriority(Kernel::HLERequestContext& ctx) { diff --git a/src/core/hle/service/fs/fs_user.cpp b/src/core/hle/service/fs/fs_user.cpp index f893c8df3..e5b4cb240 100644 --- a/src/core/hle/service/fs/fs_user.cpp +++ b/src/core/hle/service/fs/fs_user.cpp @@ -54,26 +54,72 @@ void FS_USER::OpenFile(Kernel::HLERequestContext& ctx) { const auto filename_type = rp.PopEnum(); const auto filename_size = rp.Pop(); const FileSys::Mode mode{rp.Pop()}; - const auto attributes = rp.Pop(); // TODO(Link Mauve): do something with those attributes. + const auto attributes = rp.Pop(); std::vector filename = rp.PopStaticBuffer(); ASSERT(filename.size() == filename_size); const FileSys::Path file_path(filename_type, std::move(filename)); LOG_DEBUG(Service_FS, "path={}, mode={} attrs={}", file_path.DebugStr(), mode.hex, attributes); - const auto [file_res, open_timeout_ns] = - archives.OpenFileFromArchive(archive_handle, file_path, mode); - IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); - rb.Push(file_res.Code()); - if (file_res.Succeeded()) { - std::shared_ptr file = *file_res; - rb.PushMoveObjects(file->Connect()); - } else { - rb.PushMoveObjects(nullptr); - LOG_DEBUG(Service_FS, "failed to get a handle for file {}", file_path.DebugStr()); + if (!archives.ArchiveIsSlow(archive_handle)) { + const auto [file_res, open_timeout_ns] = + archives.OpenFileFromArchive(archive_handle, file_path, mode, attributes); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); + rb.Push(file_res.Code()); + if (file_res.Succeeded()) { + std::shared_ptr file = *file_res; + rb.PushMoveObjects(file->Connect()); + } else { + rb.PushMoveObjects(nullptr); + LOG_DEBUG(Service_FS, "failed to get a handle for file {}", file_path.DebugStr()); + } + + ctx.SleepClientThread("fs_user::open", open_timeout_ns, nullptr); + return; } - ctx.SleepClientThread("fs_user::open", open_timeout_ns, nullptr); + struct AsyncData { + ArchiveHandle archive_handle; + FileSys::Path file_path; + FileSys::Mode mode; + u32 attributes; + std::chrono::steady_clock::time_point pre_timer; + + std::pair>, std::chrono::nanoseconds> file; + }; + auto async_data = std::make_shared(); + async_data->archive_handle = archive_handle; + async_data->file_path = file_path; + async_data->mode = mode; + async_data->attributes = attributes; + async_data->pre_timer = std::chrono::steady_clock::now(); + + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + async_data->file = + archives.OpenFileFromArchive(async_data->archive_handle, async_data->file_path, + async_data->mode, async_data->attributes); + const auto time_took = std::chrono::duration_cast( + std::chrono::steady_clock::now() - async_data->pre_timer); + return static_cast(((async_data->file.second > time_took) + ? (async_data->file.second - time_took) + : std::chrono::nanoseconds()) + .count()); + }, + [async_data](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 1, 2); + + rb.Push(async_data->file.first.Code()); + if (async_data->file.first.Succeeded()) { + std::shared_ptr file = *async_data->file.first; + rb.PushMoveObjects(file->Connect()); + } else { + rb.PushMoveObjects(nullptr); + LOG_DEBUG(Service_FS, "failed to get a handle for file {}", + async_data->file_path.DebugStr()); + } + }, + true); } void FS_USER::OpenFileDirectly(Kernel::HLERequestContext& ctx) { @@ -97,35 +143,100 @@ void FS_USER::OpenFileDirectly(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_FS, "archive_id=0x{:08X} archive_path={} file_path={}, mode={} attributes={}", archive_id, archive_path.DebugStr(), file_path.DebugStr(), mode.hex, attributes); - IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); + u64 program_id = GetSessionData(ctx.Session())->program_id; - ClientSlot* slot = GetSessionData(ctx.Session()); + if (!archives.ArchiveIsSlow(archive_id)) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); - ResultVal archive_handle = - archives.OpenArchive(archive_id, archive_path, slot->program_id); - if (archive_handle.Failed()) { - LOG_ERROR(Service_FS, - "Failed to get a handle for archive archive_id=0x{:08X} archive_path={}", - archive_id, archive_path.DebugStr()); - rb.Push(archive_handle.Code()); - rb.PushMoveObjects(nullptr); + ResultVal archive_handle = + archives.OpenArchive(archive_id, archive_path, program_id); + if (archive_handle.Failed()) { + LOG_ERROR(Service_FS, + "Failed to get a handle for archive archive_id=0x{:08X} archive_path={}", + archive_id, archive_path.DebugStr()); + rb.Push(archive_handle.Code()); + rb.PushMoveObjects(nullptr); + return; + } + SCOPE_EXIT({ archives.CloseArchive(*archive_handle); }); + + const auto [file_res, open_timeout_ns] = + archives.OpenFileFromArchive(*archive_handle, file_path, mode, attributes); + rb.Push(file_res.Code()); + if (file_res.Succeeded()) { + std::shared_ptr file = *file_res; + rb.PushMoveObjects(file->Connect()); + } else { + rb.PushMoveObjects(nullptr); + LOG_DEBUG(Service_FS, "failed to get a handle for file {} mode={} attributes={}", + file_path.DebugStr(), mode.hex, attributes); + } + + ctx.SleepClientThread("fs_user::open_directly", open_timeout_ns, nullptr); return; } - SCOPE_EXIT({ archives.CloseArchive(*archive_handle); }); - const auto [file_res, open_timeout_ns] = - archives.OpenFileFromArchive(*archive_handle, file_path, mode); - rb.Push(file_res.Code()); - if (file_res.Succeeded()) { - std::shared_ptr file = *file_res; - rb.PushMoveObjects(file->Connect()); - } else { - rb.PushMoveObjects(nullptr); - LOG_DEBUG(Service_FS, "failed to get a handle for file {} mode={} attributes={}", - file_path.DebugStr(), mode.hex, attributes); - } + struct AsyncData { + ArchiveIdCode archive_id; + FileSys::Path archive_path; + FileSys::Path file_path; + u64 program_id; + FileSys::Mode mode; + u32 attributes; + std::chrono::steady_clock::time_point pre_timer; - ctx.SleepClientThread("fs_user::open_directly", open_timeout_ns, nullptr); + ResultVal archive_handle; + std::pair>, std::chrono::nanoseconds> file; + }; + auto async_data = std::make_shared(); + async_data->archive_id = archive_id; + async_data->archive_path = archive_path; + async_data->file_path = file_path; + async_data->program_id = program_id; + async_data->mode = mode; + async_data->attributes = attributes; + async_data->pre_timer = std::chrono::steady_clock::now(); + + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + async_data->archive_handle = archives.OpenArchive( + async_data->archive_id, async_data->archive_path, async_data->program_id); + if (async_data->archive_handle.Failed()) { + LOG_ERROR(Service_FS, + "Failed to get a handle for archive archive_id=0x{:08X} archive_path={}", + async_data->archive_id, async_data->archive_path.DebugStr()); + return s64(); + } + async_data->file = + archives.OpenFileFromArchive(*async_data->archive_handle, async_data->file_path, + async_data->mode, async_data->attributes); + archives.CloseArchive(*async_data->archive_handle); + const auto time_took = std::chrono::duration_cast( + std::chrono::steady_clock::now() - async_data->pre_timer); + return static_cast(((async_data->file.second > time_took) + ? (async_data->file.second - time_took) + : std::chrono::nanoseconds()) + .count()); + }, + [async_data](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 1, 2); + + if (async_data->archive_handle.Failed()) { + rb.Push(async_data->archive_handle.Code()); + rb.PushMoveObjects(nullptr); + } + + rb.Push(async_data->file.first.Code()); + if (async_data->file.first.Succeeded()) { + std::shared_ptr file = *async_data->file.first; + rb.PushMoveObjects(file->Connect()); + } else { + rb.PushMoveObjects(nullptr); + LOG_DEBUG(Service_FS, "failed to get a handle for file {}", + async_data->file_path.DebugStr()); + } + }, + true); } void FS_USER::DeleteFile(Kernel::HLERequestContext& ctx) { @@ -142,8 +253,33 @@ void FS_USER::DeleteFile(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_FS, "type={} size={} data={}", filename_type, filename_size, file_path.DebugStr()); - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(archives.DeleteFileFromArchive(archive_handle, file_path)); + if (!archives.ArchiveIsSlow(archive_handle)) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(archives.DeleteFileFromArchive(archive_handle, file_path)); + return; + } + + struct AsyncData { + ArchiveHandle archive_handle; + FileSys::Path file_path; + + Result res{0}; + }; + auto async_data = std::make_shared(); + async_data->archive_handle = archive_handle; + async_data->file_path = file_path; + + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + async_data->res = + archives.DeleteFileFromArchive(async_data->archive_handle, async_data->file_path); + return 0; + }, + [async_data](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 1, 0); + rb.Push(async_data->res); + }, + true); } void FS_USER::RenameFile(Kernel::HLERequestContext& ctx) { @@ -169,9 +305,40 @@ void FS_USER::RenameFile(Kernel::HLERequestContext& ctx) { src_filename_type, src_filename_size, src_file_path.DebugStr(), dest_filename_type, dest_filename_size, dest_file_path.DebugStr()); - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(archives.RenameFileBetweenArchives(src_archive_handle, src_file_path, - dest_archive_handle, dest_file_path)); + if (!archives.ArchiveIsSlow(src_archive_handle) && + !archives.ArchiveIsSlow(dest_archive_handle)) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(archives.RenameFileBetweenArchives(src_archive_handle, src_file_path, + dest_archive_handle, dest_file_path)); + return; + } + + struct AsyncData { + ArchiveHandle src_archive_handle; + FileSys::Path src_file_path; + ArchiveHandle dest_archive_handle; + FileSys::Path dest_file_path; + + Result res{0}; + }; + auto async_data = std::make_shared(); + async_data->src_archive_handle = src_archive_handle; + async_data->src_file_path = src_file_path; + async_data->dest_archive_handle = dest_archive_handle; + async_data->dest_file_path = dest_file_path; + + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + async_data->res = archives.RenameFileBetweenArchives( + async_data->src_archive_handle, async_data->src_file_path, + async_data->dest_archive_handle, async_data->dest_file_path); + return 0; + }, + [async_data](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 1, 0); + rb.Push(async_data->res); + }, + true); } void FS_USER::DeleteDirectory(Kernel::HLERequestContext& ctx) { @@ -189,8 +356,33 @@ void FS_USER::DeleteDirectory(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_FS, "type={} size={} data={}", dirname_type, dirname_size, dir_path.DebugStr()); - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(archives.DeleteDirectoryFromArchive(archive_handle, dir_path)); + if (!archives.ArchiveIsSlow(archive_handle)) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(archives.DeleteDirectoryFromArchive(archive_handle, dir_path)); + return; + } + + struct AsyncData { + ArchiveHandle archive_handle; + FileSys::Path dir_path; + + Result res{0}; + }; + auto async_data = std::make_shared(); + async_data->archive_handle = archive_handle; + async_data->dir_path = dir_path; + + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + async_data->res = archives.DeleteDirectoryFromArchive(async_data->archive_handle, + async_data->dir_path); + return 0; + }, + [async_data](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 1, 0); + rb.Push(async_data->res); + }, + true); } void FS_USER::DeleteDirectoryRecursively(Kernel::HLERequestContext& ctx) { @@ -208,8 +400,33 @@ void FS_USER::DeleteDirectoryRecursively(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_FS, "type={} size={} data={}", dirname_type, dirname_size, dir_path.DebugStr()); - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(archives.DeleteDirectoryRecursivelyFromArchive(archive_handle, dir_path)); + if (!archives.ArchiveIsSlow(archive_handle)) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(archives.DeleteDirectoryRecursivelyFromArchive(archive_handle, dir_path)); + return; + } + + struct AsyncData { + ArchiveHandle archive_handle; + FileSys::Path dir_path; + + Result res{0}; + }; + auto async_data = std::make_shared(); + async_data->archive_handle = archive_handle; + async_data->dir_path = dir_path; + + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + async_data->res = archives.DeleteDirectoryRecursivelyFromArchive( + async_data->archive_handle, async_data->dir_path); + return 0; + }, + [async_data](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 1, 0); + rb.Push(async_data->res); + }, + true); } void FS_USER::CreateFile(Kernel::HLERequestContext& ctx) { @@ -229,8 +446,38 @@ void FS_USER::CreateFile(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_FS, "type={} attributes={} size={:x} data={}", filename_type, attributes, file_size, file_path.DebugStr()); - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(archives.CreateFileInArchive(archive_handle, file_path, file_size)); + if (!archives.ArchiveIsSlow(archive_handle)) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(archives.CreateFileInArchive(archive_handle, file_path, file_size, attributes)); + return; + } + + struct AsyncData { + ArchiveHandle archive_handle; + FileSys::Path file_path; + u64 file_size; + u32 attributes; + + Result res{0}; + }; + auto async_data = std::make_shared(); + async_data->archive_handle = archive_handle; + async_data->file_path = file_path; + async_data->file_size = file_size; + async_data->attributes = attributes; + + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + async_data->res = + archives.CreateFileInArchive(async_data->archive_handle, async_data->file_path, + async_data->file_size, async_data->attributes); + return 0; + }, + [async_data](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 1, 0); + rb.Push(async_data->res); + }, + true); } void FS_USER::CreateDirectory(Kernel::HLERequestContext& ctx) { @@ -239,7 +486,7 @@ void FS_USER::CreateDirectory(Kernel::HLERequestContext& ctx) { const auto archive_handle = rp.PopRaw(); const auto dirname_type = rp.PopEnum(); const auto dirname_size = rp.Pop(); - [[maybe_unused]] const auto attributes = rp.Pop(); + const auto attributes = rp.Pop(); std::vector dirname = rp.PopStaticBuffer(); ASSERT(dirname.size() == dirname_size); const FileSys::Path dir_path(dirname_type, std::move(dirname)); @@ -247,8 +494,35 @@ void FS_USER::CreateDirectory(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_FS, "type={} size={} data={}", dirname_type, dirname_size, dir_path.DebugStr()); - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(archives.CreateDirectoryFromArchive(archive_handle, dir_path)); + if (!archives.ArchiveIsSlow(archive_handle)) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(archives.CreateDirectoryFromArchive(archive_handle, dir_path, attributes)); + return; + } + + struct AsyncData { + ArchiveHandle archive_handle; + FileSys::Path dir_path; + u32 attributes; + + Result res{0}; + }; + auto async_data = std::make_shared(); + async_data->archive_handle = archive_handle; + async_data->dir_path = dir_path; + async_data->attributes = attributes; + + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + async_data->res = archives.CreateDirectoryFromArchive( + async_data->archive_handle, async_data->dir_path, async_data->attributes); + return 0; + }, + [async_data](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 1, 0); + rb.Push(async_data->res); + }, + true); } void FS_USER::RenameDirectory(Kernel::HLERequestContext& ctx) { @@ -273,9 +547,40 @@ void FS_USER::RenameDirectory(Kernel::HLERequestContext& ctx) { src_dirname_type, src_dirname_size, src_dir_path.DebugStr(), dest_dirname_type, dest_dirname_size, dest_dir_path.DebugStr()); - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(archives.RenameDirectoryBetweenArchives(src_archive_handle, src_dir_path, - dest_archive_handle, dest_dir_path)); + if (!archives.ArchiveIsSlow(src_archive_handle) && + !archives.ArchiveIsSlow(dest_archive_handle)) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(archives.RenameDirectoryBetweenArchives(src_archive_handle, src_dir_path, + dest_archive_handle, dest_dir_path)); + return; + } + + struct AsyncData { + ArchiveHandle src_archive_handle; + FileSys::Path src_dir_path; + ArchiveHandle dest_archive_handle; + FileSys::Path dest_dir_path; + + Result res{0}; + }; + auto async_data = std::make_shared(); + async_data->src_archive_handle = src_archive_handle; + async_data->src_dir_path = src_dir_path; + async_data->dest_archive_handle = dest_archive_handle; + async_data->dest_dir_path = dest_dir_path; + + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + async_data->res = archives.RenameDirectoryBetweenArchives( + async_data->src_archive_handle, async_data->src_dir_path, + async_data->dest_archive_handle, async_data->dest_dir_path); + return 0; + }, + [async_data](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 1, 0); + rb.Push(async_data->res); + }, + true); } void FS_USER::OpenDirectory(Kernel::HLERequestContext& ctx) { @@ -291,20 +596,56 @@ void FS_USER::OpenDirectory(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_FS, "type={} size={} data={}", dirname_type, dirname_size, dir_path.DebugStr()); - IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); - ResultVal> dir_res = - archives.OpenDirectoryFromArchive(archive_handle, dir_path); - rb.Push(dir_res.Code()); - if (dir_res.Succeeded()) { - std::shared_ptr directory = *dir_res; - auto [server, client] = system.Kernel().CreateSessionPair(directory->GetName()); - directory->ClientConnected(server); - rb.PushMoveObjects(client); - } else { - LOG_DEBUG(Service_FS, "failed to get a handle for directory type={} size={} data={}", - dirname_type, dirname_size, dir_path.DebugStr()); - rb.PushMoveObjects(nullptr); + if (!archives.ArchiveIsSlow(archive_handle)) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); + ResultVal> dir_res = + archives.OpenDirectoryFromArchive(archive_handle, dir_path); + rb.Push(dir_res.Code()); + if (dir_res.Succeeded()) { + std::shared_ptr directory = *dir_res; + auto [server, client] = system.Kernel().CreateSessionPair(directory->GetName()); + directory->ClientConnected(server); + rb.PushMoveObjects(client); + } else { + LOG_DEBUG(Service_FS, "failed to get a handle for directory type={} size={} data={}", + dirname_type, dirname_size, dir_path.DebugStr()); + rb.PushMoveObjects(nullptr); + } + return; } + + struct AsyncData { + ArchiveHandle archive_handle; + FileSys::Path dir_path; + + ResultVal> dir_res; + }; + auto async_data = std::make_shared(); + async_data->archive_handle = archive_handle; + async_data->dir_path = dir_path; + + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + async_data->dir_res = + archives.OpenDirectoryFromArchive(async_data->archive_handle, async_data->dir_path); + return 0; + }, + [this, async_data](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 1, 2); + + rb.Push(async_data->dir_res.Code()); + if (async_data->dir_res.Succeeded()) { + std::shared_ptr directory = *async_data->dir_res; + auto [server, client] = system.Kernel().CreateSessionPair(directory->GetName()); + directory->ClientConnected(server); + rb.PushMoveObjects(client); + } else { + LOG_DEBUG(Service_FS, "failed to get a handle for directory path={}", + async_data->dir_path.DebugStr()); + rb.PushMoveObjects(nullptr); + } + }, + true); } void FS_USER::OpenArchive(Kernel::HLERequestContext& ctx) { @@ -318,20 +659,59 @@ void FS_USER::OpenArchive(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_FS, "archive_id=0x{:08X} archive_path={}", archive_id, archive_path.DebugStr()); - - IPC::RequestBuilder rb = rp.MakeBuilder(3, 0); ClientSlot* slot = GetSessionData(ctx.Session()); - const ResultVal handle = - archives.OpenArchive(archive_id, archive_path, slot->program_id); - rb.Push(handle.Code()); - if (handle.Succeeded()) { - rb.PushRaw(*handle); - } else { - rb.Push(0); - LOG_ERROR(Service_FS, - "failed to get a handle for archive archive_id=0x{:08X} archive_path={}", - archive_id, archive_path.DebugStr()); + u64 program_id = slot->program_id; + + // Conventional opening + if (!archives.ArchiveIsSlow(archive_id)) { + IPC::RequestBuilder rb = rp.MakeBuilder(3, 0); + const ResultVal handle = + archives.OpenArchive(archive_id, archive_path, program_id); + rb.Push(handle.Code()); + if (handle.Succeeded()) { + rb.PushRaw(*handle); + } else { + rb.Push(0); + LOG_ERROR(Service_FS, + "failed to get a handle for archive archive_id=0x{:08X} archive_path={}", + archive_id, archive_path.DebugStr()); + } + return; } + + struct AsyncData { + // Input + ArchiveIdCode archive_id; + FileSys::Path archive_path; + u64 program_id; + + // Output + ResultVal handle; + }; + auto async_data = std::make_shared(); + async_data->archive_id = archive_id; + async_data->archive_path = archive_path; + async_data->program_id = program_id; + + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + async_data->handle = archives.OpenArchive( + async_data->archive_id, async_data->archive_path, async_data->program_id); + return 0; + }, + [async_data](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 3, 0); + rb.Push(async_data->handle.Code()); + if (async_data->handle.Succeeded()) { + rb.PushRaw(*async_data->handle); + } else { + rb.Push(0); + LOG_ERROR(Service_FS, + "failed to get a handle for archive archive_id=0x{:08X} archive_path={}", + async_data->archive_id, async_data->archive_path.DebugStr()); + } + }, + true); } void FS_USER::ControlArchive(Kernel::HLERequestContext& ctx) { @@ -340,24 +720,96 @@ void FS_USER::ControlArchive(Kernel::HLERequestContext& ctx) { const auto action = rp.Pop(); const auto input_size = rp.Pop(); const auto output_size = rp.Pop(); - [[maybe_unused]] const auto input = rp.PopMappedBuffer(); - [[maybe_unused]] const auto output = rp.PopMappedBuffer(); - LOG_WARNING(Service_FS, - "(STUBBED) called, archive_handle={:016X}, action={:08X}, input_size={:08X}, " - "output_size={:08X}", - archive_handle, action, input_size, output_size); + if (!archives.ArchiveIsSlow(archive_handle)) { + auto input = rp.PopMappedBuffer(); + auto output = rp.PopMappedBuffer(); + std::vector in_data(input_size); + input.Read(in_data.data(), 0, in_data.size()); + std::vector out_data(output_size); - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(ResultSuccess); + const Result res = + archives.ControlArchive(archive_handle, action, in_data.data(), in_data.size(), + out_data.data(), out_data.size()); + + if (res.IsSuccess() && output_size != 0) { + output.Write(out_data.data(), 0, out_data.size()); + } + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(res); + return; + } + + struct AsyncData { + ArchiveHandle handle; + u32 action; + Kernel::MappedBuffer* in_buffer; + u32 in_size; + u32 out_size; + + Result res{0}; + std::vector out_data; + Kernel::MappedBuffer* out_buffer; + }; + + auto async_data = std::make_shared(); + async_data->handle = archive_handle; + async_data->action = action; + async_data->in_size = input_size; + async_data->out_size = output_size; + async_data->in_buffer = &rp.PopMappedBuffer(); + async_data->out_buffer = &rp.PopMappedBuffer(); + + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + std::vector in_data(async_data->in_size); + async_data->in_buffer->Read(in_data.data(), 0, in_data.size()); + async_data->out_data.resize(async_data->out_size); + + async_data->res = archives.ControlArchive( + async_data->handle, async_data->action, in_data.data(), in_data.size(), + async_data->out_data.data(), async_data->out_data.size()); + return 0; + }, + [async_data](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 1, 0); + if (async_data->res.IsSuccess() && async_data->out_size != 0) { + async_data->out_buffer->Write(async_data->out_data.data(), 0, + async_data->out_data.size()); + } + rb.Push(async_data->res); + }, + true); } void FS_USER::CloseArchive(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); const auto archive_handle = rp.PopRaw(); - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(archives.CloseArchive(archive_handle)); + if (!archives.ArchiveIsSlow(archive_handle)) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(archives.CloseArchive(archive_handle)); + return; + } + + struct AsyncData { + ArchiveHandle handle; + + Result res{0}; + }; + auto async_data = std::make_shared(); + async_data->handle = archive_handle; + + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + async_data->res = archives.CloseArchive(async_data->handle); + return 0; + }, + [async_data](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 1, 0); + rb.Push(async_data->res); + }, + true); } void FS_USER::IsSdmcDetected(Kernel::HLERequestContext& ctx) { @@ -377,8 +829,6 @@ void FS_USER::IsSdmcWriteable(Kernel::HLERequestContext& ctx) { } void FS_USER::FormatSaveData(Kernel::HLERequestContext& ctx) { - LOG_WARNING(Service_FS, "(STUBBED)"); - IPC::RequestParser rp(ctx); const auto archive_id = rp.PopEnum(); const auto archivename_type = rp.PopEnum(); @@ -386,8 +836,8 @@ void FS_USER::FormatSaveData(Kernel::HLERequestContext& ctx) { const auto block_size = rp.Pop(); const auto number_directories = rp.Pop(); const auto number_files = rp.Pop(); - [[maybe_unused]] const auto directory_buckets = rp.Pop(); - [[maybe_unused]] const auto file_buckets = rp.Pop(); + const auto directory_buckets = rp.Pop(); + const auto file_buckets = rp.Pop(); const bool duplicate_data = rp.Pop(); std::vector archivename = rp.PopStaticBuffer(); ASSERT(archivename.size() == archivename_size); @@ -417,7 +867,7 @@ void FS_USER::FormatSaveData(Kernel::HLERequestContext& ctx) { ClientSlot* slot = GetSessionData(ctx.Session()); rb.Push(archives.FormatArchive(ArchiveIdCode::SaveData, format_info, archive_path, - slot->program_id)); + slot->program_id, directory_buckets, file_buckets)); } void FS_USER::FormatThisUserSaveData(Kernel::HLERequestContext& ctx) { @@ -425,8 +875,8 @@ void FS_USER::FormatThisUserSaveData(Kernel::HLERequestContext& ctx) { const auto block_size = rp.Pop(); const auto number_directories = rp.Pop(); const auto number_files = rp.Pop(); - [[maybe_unused]] const auto directory_buckets = rp.Pop(); - [[maybe_unused]] const auto file_buckets = rp.Pop(); + const auto directory_buckets = rp.Pop(); + const auto file_buckets = rp.Pop(); const auto duplicate_data = rp.Pop(); FileSys::ArchiveFormatInfo format_info; @@ -438,7 +888,7 @@ void FS_USER::FormatThisUserSaveData(Kernel::HLERequestContext& ctx) { IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); ClientSlot* slot = GetSessionData(ctx.Session()); rb.Push(archives.FormatArchive(ArchiveIdCode::SaveData, format_info, FileSys::Path(), - slot->program_id)); + slot->program_id, directory_buckets, file_buckets)); LOG_TRACE(Service_FS, "called"); } @@ -495,10 +945,7 @@ void FS_USER::GetNandArchiveResource(Kernel::HLERequestContext& ctx) { void FS_USER::CreateExtSaveData(Kernel::HLERequestContext& ctx) { // TODO(Subv): Figure out the other parameters. IPC::RequestParser rp(ctx); - MediaType media_type = static_cast(rp.Pop()); // the other bytes are unknown - u32 save_low = rp.Pop(); - u32 save_high = rp.Pop(); - u32 unknown = rp.Pop(); + auto ext_data_info = rp.PopRaw(); u32 directories = rp.Pop(); u32 files = rp.Pop(); u64 size_limit = rp.Pop(); @@ -512,33 +959,34 @@ void FS_USER::CreateExtSaveData(Kernel::HLERequestContext& ctx) { format_info.number_directories = directories; format_info.number_files = files; format_info.duplicate_data = false; - format_info.total_size = 0; + format_info.total_size = static_cast(size_limit); IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); ClientSlot* slot = GetSessionData(ctx.Session()); - rb.Push(archives.CreateExtSaveData(media_type, save_high, save_low, icon, format_info, - slot->program_id)); + rb.Push(archives.CreateExtSaveData(static_cast(ext_data_info.media_type), + ext_data_info.unknown, ext_data_info.save_id_high, + ext_data_info.save_id_low, icon, format_info, + slot->program_id, size_limit)); rb.PushMappedBuffer(icon_buffer); LOG_DEBUG(Service_FS, "called, savedata_high={:08X} savedata_low={:08X} unknown={:08X} " "files={:08X} directories={:08X} size_limit={:016x} icon_size={:08X}", - save_high, save_low, unknown, directories, files, size_limit, icon_size); + ext_data_info.save_id_high, ext_data_info.save_id_low, ext_data_info.unknown, + directories, files, size_limit, icon_size); } void FS_USER::DeleteExtSaveData(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); - MediaType media_type = static_cast(rp.Pop()); // the other bytes are unknown - u32 save_low = rp.Pop(); - u32 save_high = rp.Pop(); - u32 unknown = rp.Pop(); // TODO(Subv): Figure out what this is + ExtSaveDataInfo info = rp.PopRaw(); IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(archives.DeleteExtSaveData(media_type, save_high, save_low)); + rb.Push(archives.DeleteExtSaveData(static_cast(info.media_type), info.unknown, + info.save_id_high, info.save_id_low)); LOG_DEBUG(Service_FS, - "called, save_low={:08X} save_high={:08X} media_type={:08X} unknown={:08X}", save_low, - save_high, media_type, unknown); + "called, save_low={:08X} save_high={:08X} media_type={:08X} unknown={:08X}", + info.save_id_low, info.save_id_high, info.media_type, info.unknown); } void FS_USER::CardSlotIsInserted(Kernel::HLERequestContext& ctx) { @@ -757,13 +1205,12 @@ void FS_USER::ObsoletedCreateExtSaveData(Kernel::HLERequestContext& ctx) { FileSys::ArchiveFormatInfo format_info; format_info.number_directories = directories; format_info.number_files = files; - format_info.duplicate_data = false; - format_info.total_size = 0; + format_info.total_size = -1; IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); ClientSlot* slot = GetSessionData(ctx.Session()); - rb.Push(archives.CreateExtSaveData(media_type, save_high, save_low, icon, format_info, - slot->program_id)); + rb.Push(archives.CreateExtSaveData(media_type, 0, save_high, save_low, icon, format_info, + slot->program_id, -1)); rb.PushMappedBuffer(icon_buffer); LOG_DEBUG(Service_FS, @@ -778,7 +1225,7 @@ void FS_USER::ObsoletedDeleteExtSaveData(Kernel::HLERequestContext& ctx) { u32 save_low = rp.Pop(); IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(archives.DeleteExtSaveData(media_type, 0, save_low)); + rb.Push(archives.DeleteExtSaveData(media_type, 0, 0, save_low)); LOG_DEBUG(Service_FS, "called, save_low={:08X} media_type={:08X}", save_low, media_type); } @@ -832,16 +1279,39 @@ void FS_USER::ObsoletedSetSaveDataSecureValue(Kernel::HLERequestContext& ctx) { const u32 unique_id = rp.Pop(); const u8 title_variation = rp.Pop(); - // TODO: Generate and Save the Secure Value + if (!secure_value_backend->BackendIsSlow()) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(secure_value_backend->ObsoletedSetSaveDataSecureValue(unique_id, title_variation, + secure_value_slot, value)); + return; + } - LOG_WARNING(Service_FS, - "(STUBBED) called, value=0x{:016x} secure_value_slot=0x{:08X} " - "unqiue_id=0x{:08X} title_variation=0x{:02X}", - value, secure_value_slot, unique_id, title_variation); + struct AsyncData { + u64 value; + u32 secure_value_slot; + u32 unique_id; + u8 title_variation; - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + Result res{0}; + }; + auto async_data = std::make_shared(); + async_data->value = value; + async_data->secure_value_slot = secure_value_slot; + async_data->unique_id = unique_id; + async_data->title_variation = title_variation; - rb.Push(ResultSuccess); + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + async_data->res = secure_value_backend->ObsoletedSetSaveDataSecureValue( + async_data->unique_id, async_data->title_variation, async_data->secure_value_slot, + async_data->value); + return 0; + }, + [async_data](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 1, 0); + rb.Push(async_data->res); + }, + true); } void FS_USER::ObsoletedGetSaveDataSecureValue(Kernel::HLERequestContext& ctx) { @@ -850,19 +1320,74 @@ void FS_USER::ObsoletedGetSaveDataSecureValue(Kernel::HLERequestContext& ctx) { const u32 unique_id = rp.Pop(); const u8 title_variation = rp.Pop(); - LOG_WARNING( - Service_FS, - "(STUBBED) called secure_value_slot=0x{:08X} unqiue_id=0x{:08X} title_variation=0x{:02X}", - secure_value_slot, unique_id, title_variation); + if (!secure_value_backend->BackendIsSlow()) { + auto res = secure_value_backend->ObsoletedGetSaveDataSecureValue(unique_id, title_variation, + secure_value_slot); + if (res.Failed()) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(res.Code()); + } else { + IPC::RequestBuilder rb = rp.MakeBuilder(4, 0); + rb.Push(res.Code()); + rb.Push(std::get<0>(*res)); // indicates if the secure value exists + rb.Push(std::get<1>(*res)); // the secure value + } + return; + } - IPC::RequestBuilder rb = rp.MakeBuilder(4, 0); + struct AsyncData { + u32 secure_value_slot; + u32 unique_id; + u8 title_variation; - rb.Push(ResultSuccess); + ResultVal> res; + }; + auto async_data = std::make_shared(); + async_data->secure_value_slot = secure_value_slot; + async_data->unique_id = unique_id; + async_data->title_variation = title_variation; - // TODO: Implement Secure Value Lookup & Generation + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + async_data->res = secure_value_backend->ObsoletedGetSaveDataSecureValue( + async_data->unique_id, async_data->title_variation, async_data->secure_value_slot); + return 0; + }, + [async_data](Kernel::HLERequestContext& ctx) { + if (async_data->res.Failed()) { + IPC::RequestBuilder rb(ctx, 1, 0); + rb.Push(async_data->res.Code()); + } else { + IPC::RequestBuilder rb(ctx, 4, 0); + rb.Push(async_data->res.Code()); + rb.Push( + std::get<0>(*async_data->res)); // indicates if the secure value exists + rb.Push(std::get<1>(*async_data->res)); // the secure value + } + }, + true); +} - rb.Push(false); // indicates that the secure value doesn't exist - rb.Push(0); // the secure value +void FS_USER::ControlSecureSave(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + const auto action = rp.Pop(); + const auto input_size = rp.Pop(); + const auto output_size = rp.Pop(); + auto input = rp.PopMappedBuffer(); + auto output = rp.PopMappedBuffer(); + + std::vector in_data(input_size); + input.Read(in_data.data(), 0, in_data.size()); + std::vector out_data(output_size); + + Result res = secure_value_backend->ControlSecureSave(action, in_data.data(), in_data.size(), + out_data.data(), out_data.size()); + + if (res.IsSuccess() && output_size != 0) { + output.Write(out_data.data(), 0, out_data.size()); + } + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(res); } void FS_USER::SetThisSaveDataSecureValue(Kernel::HLERequestContext& ctx) { @@ -870,31 +1395,85 @@ void FS_USER::SetThisSaveDataSecureValue(Kernel::HLERequestContext& ctx) { const u32 secure_value_slot = rp.Pop(); const u64 value = rp.Pop(); - // TODO: Generate and Save the Secure Value + if (!secure_value_backend->BackendIsSlow()) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(secure_value_backend->SetThisSaveDataSecureValue(secure_value_slot, value)); + return; + } - LOG_WARNING(Service_FS, "(STUBBED) called, value=0x{:016x} secure_value_slot=0x{:08X}", value, - secure_value_slot); + struct AsyncData { + u64 value; + u32 secure_value_slot; - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + Result res{0}; + }; + auto async_data = std::make_shared(); + async_data->value = value; + async_data->secure_value_slot = secure_value_slot; - rb.Push(ResultSuccess); + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + async_data->res = secure_value_backend->SetThisSaveDataSecureValue( + async_data->secure_value_slot, async_data->value); + return 0; + }, + [async_data](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 1, 0); + rb.Push(async_data->res); + }, + true); } void FS_USER::GetThisSaveDataSecureValue(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); const u32 secure_value_slot = rp.Pop(); - LOG_WARNING(Service_FS, "(STUBBED) called secure_value_slot=0x{:08X}", secure_value_slot); + if (!secure_value_backend->BackendIsSlow()) { + auto res = secure_value_backend->GetThisSaveDataSecureValue(secure_value_slot); - IPC::RequestBuilder rb = rp.MakeBuilder(5, 0); + if (res.Failed()) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(res.Code()); + } else { + IPC::RequestBuilder rb = rp.MakeBuilder(5, 0); + rb.Push(res.Code()); + rb.Push(std::get<0>(*res)); // indicates if the secure value exists + rb.Push(std::get<1>( + *res)); // indicates if the requesting process is a gamecard, overriding the check + rb.Push(std::get<2>(*res)); // the secure value + } + return; + } - rb.Push(ResultSuccess); + struct AsyncData { + u32 secure_value_slot; - // TODO: Implement Secure Value Lookup & Generation + ResultVal> res; + }; + auto async_data = std::make_shared(); + async_data->secure_value_slot = secure_value_slot; - rb.Push(false); // indicates that the secure value doesn't exist - rb.Push(true); // indicates the requesting process is a gamecard, overriding the check - rb.Push(0); // the secure value + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + async_data->res = + secure_value_backend->GetThisSaveDataSecureValue(async_data->secure_value_slot); + return 0; + }, + [async_data](Kernel::HLERequestContext& ctx) { + if (async_data->res.Failed()) { + IPC::RequestBuilder rb(ctx, 1, 0); + rb.Push(async_data->res.Code()); + } else { + IPC::RequestBuilder rb(ctx, 5, 0); + rb.Push(async_data->res.Code()); + rb.Push( + std::get<0>(*async_data->res)); // indicates if the secure value exists + rb.Push(std::get<1>(*async_data->res)); // indicates if the requesting process + // is a gamecard, overriding the check + rb.Push(std::get<2>(*async_data->res)); // the secure value + } + }, + true); } void FS_USER::SetSaveDataSecureValue(Kernel::HLERequestContext& ctx) { @@ -904,16 +1483,39 @@ void FS_USER::SetSaveDataSecureValue(Kernel::HLERequestContext& ctx) { const u64 value = rp.Pop(); const bool flush = rp.Pop(); - // TODO: Generate and Save the Secure Value + if (!archives.ArchiveIsSlow(archive_handle)) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - LOG_WARNING(Service_FS, - "(STUBBED) called, value=0x{:016x} secure_value_slot=0x{:04X} " - "archive_handle=0x{:08X} flush={}", - value, secure_value_slot, archive_handle, flush); + rb.Push(archives.SetSaveDataSecureValue(archive_handle, secure_value_slot, value, flush)); + return; + } - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + struct AsyncData { + ArchiveHandle archive_handle; + u64 value; + u32 secure_value_slot; + bool flush; - rb.Push(ResultSuccess); + Result res{0}; + }; + auto async_data = std::make_shared(); + async_data->archive_handle = archive_handle; + async_data->value = value; + async_data->secure_value_slot = secure_value_slot; + async_data->flush = flush; + + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + async_data->res = archives.SetSaveDataSecureValue(async_data->archive_handle, + async_data->secure_value_slot, + async_data->value, async_data->flush); + return 0; + }, + [async_data](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 1, 0); + rb.Push(async_data->res); + }, + true); } void FS_USER::GetSaveDataSecureValue(Kernel::HLERequestContext& ctx) { @@ -921,18 +1523,53 @@ void FS_USER::GetSaveDataSecureValue(Kernel::HLERequestContext& ctx) { const auto archive_handle = rp.PopRaw(); const u32 secure_value_slot = rp.Pop(); - LOG_WARNING(Service_FS, "(STUBBED) called secure_value_slot=0x{:08X} archive_handle=0x{:08X}", - secure_value_slot, archive_handle); + if (!archives.ArchiveIsSlow(archive_handle)) { + auto res = archives.GetSaveDataSecureValue(archive_handle, secure_value_slot); + if (res.Failed()) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(res.Code()); + } else { + IPC::RequestBuilder rb = rp.MakeBuilder(5, 0); + rb.Push(res.Code()); + rb.Push(std::get<0>(*res)); // indicates if the secure value exists + rb.Push(std::get<1>( + *res)); // indicates if the requesting process is a gamecard, overriding the check + rb.Push(std::get<2>(*res)); // the secure value + } + return; + } - IPC::RequestBuilder rb = rp.MakeBuilder(5, 0); + struct AsyncData { + ArchiveHandle archive_handle; + u32 secure_value_slot; - rb.Push(ResultSuccess); + ResultVal> res; + }; + auto async_data = std::make_shared(); + async_data->archive_handle = archive_handle; + async_data->secure_value_slot = secure_value_slot; - // TODO: Implement Secure Value Lookup & Generation - - rb.Push(false); // indicates that the secure value doesn't exist - rb.Push(true); // indicates the requesting process is a gamecard, overriding the check - rb.Push(0); // the secure value + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + async_data->res = archives.GetSaveDataSecureValue(async_data->archive_handle, + async_data->secure_value_slot); + return 0; + }, + [async_data](Kernel::HLERequestContext& ctx) { + if (async_data->res.Failed()) { + IPC::RequestBuilder rb(ctx, 1, 0); + rb.Push(async_data->res.Code()); + } else { + IPC::RequestBuilder rb(ctx, 5, 0); + rb.Push(async_data->res.Code()); + rb.Push( + std::get<0>(*async_data->res)); // indicates if the secure value exists + rb.Push(std::get<1>(*async_data->res)); // indicates if the requesting process + // is a gamecard, overriding the check + rb.Push(std::get<2>(*async_data->res)); // the secure value + } + }, + true); } void FS_USER::RegisterProgramInfo(u32 process_id, u64 program_id, const std::string& filepath) { @@ -1118,7 +1755,7 @@ FS_USER::FS_USER(Core::System& system) {0x0864, nullptr, "GetNandInfo"}, {0x0865, &FS_USER::ObsoletedSetSaveDataSecureValue, "SetSaveDataSecureValue"}, {0x0866, &FS_USER::ObsoletedGetSaveDataSecureValue, "GetSaveDataSecureValue"}, - {0x0867, nullptr, "ControlSecureSave"}, + {0x0867, &FS_USER::ControlSecureSave, "ControlSecureSave"}, {0x0868, nullptr, "GetMediaType"}, {0x0869, nullptr, "GetNandEraseCount"}, {0x086A, nullptr, "ReadNandReport"}, @@ -1132,6 +1769,13 @@ FS_USER::FS_USER(Core::System& system) // clang-format on }; RegisterHandlers(functions); + secure_value_backend = std::make_shared(); +} +template +void Service::FS::FS_USER::serialize(Archive& ar, const unsigned int) { + ar& boost::serialization::base_object(*this); + ar& priority; + ar& secure_value_backend; } void InstallInterfaces(Core::System& system) { diff --git a/src/core/hle/service/fs/fs_user.h b/src/core/hle/service/fs/fs_user.h index 57285995a..206a9c3b6 100644 --- a/src/core/hle/service/fs/fs_user.h +++ b/src/core/hle/service/fs/fs_user.h @@ -9,6 +9,7 @@ #include #include "common/common_types.h" #include "core/file_sys/errors.h" +#include "core/file_sys/secure_value_backend.h" #include "core/hle/service/fs/archive.h" #include "core/hle/service/service.h" @@ -77,6 +78,10 @@ public: } } + void RegisterSecureValueBackend(const std::shared_ptr& backend) { + secure_value_backend = backend; + } + private: void Initialize(Kernel::HLERequestContext& ctx); @@ -657,6 +662,21 @@ private: */ void ObsoletedGetSaveDataSecureValue(Kernel::HLERequestContext& ctx); + /** + * FS_User::ControlSecureSave service function + * Inputs: + * 1 : Action + * 2 : Input Size + * 3 : Output Size + * 4 : (Input Size << 4) | 0xA + * 5 : Input Pointer + * 6 : (Output Size << 4) | 0xC + * 7 : Output Pointer + * Outputs: + * 1 : Result of function, 0 on success, otherwise error code + */ + void ControlSecureSave(Kernel::HLERequestContext& ctx); + /** * FS_User::SetThisSaveDataSecureValue service function. * Inputs: @@ -722,11 +742,10 @@ private: Core::System& system; ArchiveManager& archives; + std::shared_ptr secure_value_backend; + template - void serialize(Archive& ar, const unsigned int) { - ar& boost::serialization::base_object(*this); - ar& priority; - } + void serialize(Archive& ar, const unsigned int); friend class boost::serialization::access; }; diff --git a/src/core/hle/service/http/http_c.cpp b/src/core/hle/service/http/http_c.cpp index 3337580a5..039a43044 100644 --- a/src/core/hle/service/http/http_c.cpp +++ b/src/core/hle/service/http/http_c.cpp @@ -1969,7 +1969,7 @@ void HTTP_C::DecryptClCertA() { FileSys::NCCHFileOpenType::NCCHData, 0, FileSys::NCCHFilePathType::RomFS, exefs_filepath); FileSys::Mode open_mode = {}; open_mode.read_flag.Assign(1); - auto file_result = archive.OpenFile(file_path, open_mode); + auto file_result = archive.OpenFile(file_path, open_mode, 0); if (file_result.Failed()) { LOG_ERROR(Service_HTTP, "ClCertA file missing"); return; diff --git a/src/core/hle/service/news/news.cpp b/src/core/hle/service/news/news.cpp index f1a32d168..919041f37 100644 --- a/src/core/hle/service/news/news.cpp +++ b/src/core/hle/service/news/news.cpp @@ -151,7 +151,7 @@ void Module::Interface::ResetNotifications(Kernel::HLERequestContext& ctx) { FileSys::Path archive_path(news_system_savedata_id); // Format the SystemSaveData archive 0x00010035 - systemsavedata_factory.Format(archive_path, FileSys::ArchiveFormatInfo(), 0); + systemsavedata_factory.Format(archive_path, FileSys::ArchiveFormatInfo(), 0, 0, 0); news->news_system_save_data_archive = systemsavedata_factory.Open(archive_path, 0).Unwrap(); @@ -655,7 +655,7 @@ Result Module::LoadNewsDBSavedata() { // If the archive didn't exist, create the files inside if (archive_result.Code() == FileSys::ResultNotFound) { // Format the archive to create the directories - systemsavedata_factory.Format(archive_path, FileSys::ArchiveFormatInfo(), 0); + systemsavedata_factory.Format(archive_path, FileSys::ArchiveFormatInfo(), 0, 0, 0); // Open it again to get a valid archive now that the folder exists news_system_save_data_archive = systemsavedata_factory.Open(archive_path, 0).Unwrap(); @@ -722,7 +722,7 @@ Result Module::SaveFileToSavedata(std::string filename, std::span buff ASSERT_MSG(result.Succeeded(), "could not open file"); auto file = std::move(result).Unwrap(); - file->Write(0, buffer.size(), 1, buffer.data()); + file->Write(0, buffer.size(), true, false, buffer.data()); file->Close(); return ResultSuccess; diff --git a/src/core/hle/service/ptm/ptm.cpp b/src/core/hle/service/ptm/ptm.cpp index 6f609182b..fb3a51b6b 100644 --- a/src/core/hle/service/ptm/ptm.cpp +++ b/src/core/hle/service/ptm/ptm.cpp @@ -158,7 +158,7 @@ static void WriteGameCoinData(GameCoin gamecoin_data) { // If the archive didn't exist, create the files inside if (archive_result.Code() == FileSys::ResultNotFormatted) { // Format the archive to create the directories - extdata_archive_factory.Format(archive_path, FileSys::ArchiveFormatInfo(), 0); + extdata_archive_factory.Format(archive_path, FileSys::ArchiveFormatInfo(), 0, 0, 0); // Open it again to get a valid archive now that the folder exists archive = extdata_archive_factory.Open(archive_path, 0).Unwrap(); // Create the game coin file @@ -174,7 +174,8 @@ static void WriteGameCoinData(GameCoin gamecoin_data) { auto gamecoin_result = archive->OpenFile(gamecoin_path, open_mode); if (gamecoin_result.Succeeded()) { auto gamecoin = std::move(gamecoin_result).Unwrap(); - gamecoin->Write(0, sizeof(GameCoin), true, reinterpret_cast(&gamecoin_data)); + gamecoin->Write(0, sizeof(GameCoin), true, false, + reinterpret_cast(&gamecoin_data)); gamecoin->Close(); } } diff --git a/src/core/hle/service/soc/soc_u.cpp b/src/core/hle/service/soc/soc_u.cpp index ae2ecfcb2..1ef35da26 100644 --- a/src/core/hle/service/soc/soc_u.cpp +++ b/src/core/hle/service/soc/soc_u.cpp @@ -19,6 +19,7 @@ #include "core/hle/kernel/shared_memory.h" #include "core/hle/result.h" #include "core/hle/service/soc/soc_u.h" +#include "network/socket_manager.h" #ifdef _WIN32 #include @@ -2221,17 +2222,12 @@ SOC_U::SOC_U() : ServiceFramework("soc:U", 18) { RegisterHandlers(functions); -#ifdef _WIN32 - WSADATA data; - WSAStartup(MAKEWORD(2, 2), &data); -#endif + Network::SocketManager::EnableSockets(); } SOC_U::~SOC_U() { CloseAndDeleteAllSockets(); -#ifdef _WIN32 - WSACleanup(); -#endif + Network::SocketManager::DisableSockets(); } std::optional SOC_U::GetDefaultInterfaceInfo() { diff --git a/src/core/loader/artic.cpp b/src/core/loader/artic.cpp new file mode 100644 index 000000000..f2e74b9a6 --- /dev/null +++ b/src/core/loader/artic.cpp @@ -0,0 +1,564 @@ +// Copyright 2024 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include +#include "common/literals.h" +#include "common/logging/log.h" +#include "common/settings.h" +#include "common/string_util.h" +#include "common/swap.h" +#include "core/core.h" +#include "core/file_sys/ncch_container.h" +#include "core/file_sys/romfs_reader.h" +#include "core/file_sys/secure_value_backend_artic.h" +#include "core/file_sys/title_metadata.h" +#include "core/hle/kernel/kernel.h" +#include "core/hle/kernel/process.h" +#include "core/hle/kernel/resource_limit.h" +#include "core/hle/service/am/am.h" +#include "core/hle/service/cfg/cfg.h" +#include "core/hle/service/fs/archive.h" +#include "core/hle/service/fs/fs_user.h" +#include "core/loader/artic.h" +#include "core/loader/smdh.h" +#include "core/memory.h" +#include "core/system_titles.h" +#include "network/network.h" + +namespace Loader { + +using namespace Common::Literals; + +Apploader_Artic::~Apploader_Artic() { + // TODO(PabloMK7) Find memory leak that prevents the romfs readers being destroyed + // when emulation stops. Looks like the mem leak comes from IVFCFile objects + // not being destroyed... + if (main_romfs_reader) { + static_cast(main_romfs_reader.get())->ClearCache(); + static_cast(main_romfs_reader.get())->CloseFile(); + main_romfs_reader.reset(); + } + if (update_romfs_reader) { + static_cast(update_romfs_reader.get())->ClearCache(); + static_cast(update_romfs_reader.get())->CloseFile(); + update_romfs_reader.reset(); + } + client->Stop(); +} + +FileType Apploader_Artic::IdentifyType(FileUtil::IOFile& file) { + return FileType::ARTIC; +} + +std::pair, ResultStatus> Apploader_Artic::LoadCoreVersion() { + if (!is_loaded) { + bool success = LoadExheader(); + if (!success) { + return std::make_pair(std::nullopt, ResultStatus::ErrorArtic); + } + } + + // Provide the core version from the exheader. + auto& ncch_caps = program_exheader.arm11_system_local_caps; + return std::make_pair(ncch_caps.core_version, ResultStatus::Success); +} + +std::pair, ResultStatus> Apploader_Artic::LoadKernelMemoryMode() { + if (!is_loaded) { + bool success = LoadExheader(); + if (!success) { + return std::make_pair(std::nullopt, ResultStatus::ErrorArtic); + } + } + + if (memory_mode_override.has_value()) { + return std::make_pair(memory_mode_override, ResultStatus::Success); + } + + // Provide the memory mode from the exheader. + auto& ncch_caps = program_exheader.arm11_system_local_caps; + auto mode = static_cast(ncch_caps.system_mode.Value()); + return std::make_pair(mode, ResultStatus::Success); +} + +std::pair, ResultStatus> +Apploader_Artic::LoadNew3dsHwCapabilities() { + if (!is_loaded) { + bool success = LoadExheader(); + if (!success) { + return std::make_pair(std::nullopt, ResultStatus::ErrorArtic); + } + } + + // Provide the capabilities from the exheader. + auto& ncch_caps = program_exheader.arm11_system_local_caps; + auto caps = Kernel::New3dsHwCapabilities{ + ncch_caps.enable_l2_cache != 0, + ncch_caps.enable_804MHz_cpu != 0, + static_cast(ncch_caps.n3ds_mode), + }; + return std::make_pair(std::move(caps), ResultStatus::Success); +} + +ResultStatus Apploader_Artic::LoadExec(std::shared_ptr& process) { + using Kernel::CodeSet; + + if (!is_loaded) + return ResultStatus::ErrorNotLoaded; + + std::vector code; + u64_le program_id; + if (ResultStatus::Success == ReadCode(code) && + ResultStatus::Success == ReadProgramId(program_id)) { + + std::string process_name = Common::StringFromFixedZeroTerminatedBuffer( + (const char*)program_exheader.codeset_info.name, 8); + + std::shared_ptr codeset = system.Kernel().CreateCodeSet(process_name, program_id); + + codeset->CodeSegment().offset = 0; + codeset->CodeSegment().addr = program_exheader.codeset_info.text.address; + codeset->CodeSegment().size = + program_exheader.codeset_info.text.num_max_pages * Memory::CITRA_PAGE_SIZE; + + codeset->RODataSegment().offset = + codeset->CodeSegment().offset + codeset->CodeSegment().size; + codeset->RODataSegment().addr = program_exheader.codeset_info.ro.address; + codeset->RODataSegment().size = + program_exheader.codeset_info.ro.num_max_pages * Memory::CITRA_PAGE_SIZE; + + // TODO(yuriks): Not sure if the bss size is added to the page-aligned .data size or just + // to the regular size. Playing it safe for now. + u32 bss_page_size = (program_exheader.codeset_info.bss_size + 0xFFF) & ~0xFFF; + code.resize(code.size() + bss_page_size, 0); + + codeset->DataSegment().offset = + codeset->RODataSegment().offset + codeset->RODataSegment().size; + codeset->DataSegment().addr = program_exheader.codeset_info.data.address; + codeset->DataSegment().size = + program_exheader.codeset_info.data.num_max_pages * Memory::CITRA_PAGE_SIZE + + bss_page_size; + + // Apply patches now that the entire codeset (including .bss) has been allocated + // const ResultStatus patch_result = overlay_ncch->ApplyCodePatch(code); + // if (patch_result != ResultStatus::Success && patch_result != ResultStatus::ErrorNotUsed) + // return patch_result; + + codeset->entrypoint = codeset->CodeSegment().addr; + codeset->memory = std::move(code); + + process = system.Kernel().CreateProcess(std::move(codeset)); + + // Attach a resource limit to the process based on the resource limit category + const auto category = static_cast( + program_exheader.arm11_system_local_caps.resource_limit_category); + process->resource_limit = system.Kernel().ResourceLimit().GetForCategory(category); + + // When running N3DS-unaware titles pm will lie about the amount of memory available. + // This means RESLIMIT_COMMIT = APPMEMALLOC doesn't correspond to the actual size of + // APPLICATION. See: + // https://github.com/LumaTeam/Luma3DS/blob/e2778a45/sysmodules/pm/source/launch.c#L237 + auto& ncch_caps = program_exheader.arm11_system_local_caps; + const auto o3ds_mode = *LoadKernelMemoryMode().first; + const auto n3ds_mode = static_cast(ncch_caps.n3ds_mode); + const bool is_new_3ds = Settings::values.is_new_3ds.GetValue(); + if (is_new_3ds && n3ds_mode == Kernel::New3dsMemoryMode::Legacy && + category == Kernel::ResourceLimitCategory::Application) { + u64 new_limit = 0; + switch (o3ds_mode) { + case Kernel::MemoryMode::Prod: + new_limit = 64_MiB; + break; + case Kernel::MemoryMode::Dev1: + new_limit = 96_MiB; + break; + case Kernel::MemoryMode::Dev2: + new_limit = 80_MiB; + break; + default: + break; + } + process->resource_limit->SetLimitValue(Kernel::ResourceLimitType::Commit, + static_cast(new_limit)); + } + + // Set the default CPU core for this process + process->ideal_processor = program_exheader.arm11_system_local_caps.ideal_processor; + + // Copy data while converting endianness + using KernelCaps = std::array; + KernelCaps kernel_caps; + std::copy_n(program_exheader.arm11_kernel_caps.descriptors, kernel_caps.size(), + begin(kernel_caps)); + process->ParseKernelCaps(kernel_caps.data(), kernel_caps.size()); + + s32 priority = program_exheader.arm11_system_local_caps.priority; + u32 stack_size = program_exheader.codeset_info.stack_size; + + // On real HW this is done with FS:Reg, but we can be lazy + auto fs_user = system.ServiceManager().GetService("fs:USER"); + fs_user->RegisterProgramInfo(process->process_id, process->codeset->program_id, + "articbase://"); + + Service::FS::FS_USER::ProductInfo product_info{}; + if (LoadProductInfo(product_info) != ResultStatus::Success) { + return ResultStatus::ErrorArtic; + } + fs_user->RegisterProductInfo(process->process_id, product_info); + + process->Run(priority, stack_size); + return ResultStatus::Success; + } + return ResultStatus::ErrorArtic; +} + +void Apploader_Artic::ParseRegionLockoutInfo(u64 program_id) { + if (Settings::values.region_value.GetValue() != Settings::REGION_VALUE_AUTO_SELECT) { + return; + } + + preferred_regions.clear(); + + std::vector smdh_buffer; + if (ReadIcon(smdh_buffer) == ResultStatus::Success && smdh_buffer.size() >= sizeof(SMDH)) { + SMDH smdh; + std::memcpy(&smdh, smdh_buffer.data(), sizeof(SMDH)); + u32 region_lockout = smdh.region_lockout; + constexpr u32 REGION_COUNT = 7; + for (u32 region = 0; region < REGION_COUNT; ++region) { + if (region_lockout & 1) { + preferred_regions.push_back(region); + } + region_lockout >>= 1; + } + } else { + const auto region = Core::GetSystemTitleRegion(program_id); + if (region.has_value()) { + preferred_regions.push_back(region.value()); + } + } +} + +bool Apploader_Artic::LoadExheader() { + if (program_exheader_loaded) + return true; + + if (!client_connected) + client_connected = client->Connect(); + if (!client_connected) + return false; + + auto req = client->NewRequest("Process_GetExheader"); + auto resp = client->Send(req); + if (!resp.has_value()) + return false; + + auto exheader_buf = resp->GetResponseBuffer(0); + if (!exheader_buf.has_value()) + return false; + + if (exheader_buf->second != sizeof(ExHeader_Header) - sizeof(ExHeader_Header::access_desc)) + return false; + + u8* prg_exh = reinterpret_cast(&program_exheader); + memcpy(prg_exh, exheader_buf->first, + sizeof(ExHeader_Header) - sizeof(ExHeader_Header::access_desc)); + memcpy(prg_exh + offsetof(ExHeader_Header, access_desc.arm11_system_local_caps), + reinterpret_cast(exheader_buf->first) + + offsetof(ExHeader_Header, arm11_system_local_caps), + offsetof(ExHeader_Header, access_desc) - + offsetof(ExHeader_Header, arm11_system_local_caps)); + program_exheader_loaded = true; + return true; +} + +ResultStatus Apploader_Artic::LoadProductInfo(Service::FS::FS_USER::ProductInfo& out_product_info) { + if (cached_product_info.has_value()) { + out_product_info = *cached_product_info; + return ResultStatus::Success; + } + + if (!client_connected) + client_connected = client->Connect(); + if (!client_connected) + return ResultStatus::ErrorArtic; + + auto req = client->NewRequest("Process_GetProductInfo"); + auto resp = client->Send(req); + if (!resp.has_value()) + return ResultStatus::ErrorArtic; + + auto pinfo_buf = resp->GetResponseBuffer(0); + if (!pinfo_buf.has_value() || pinfo_buf->second != sizeof(Service::FS::FS_USER::ProductInfo)) + return ResultStatus::ErrorArtic; + + out_product_info = *reinterpret_cast(pinfo_buf->first); + cached_product_info = out_product_info; + + return ResultStatus::Success; +} + +ResultStatus Apploader_Artic::Load(std::shared_ptr& process) { + u64_le ncch_program_id; + + if (is_loaded) + return ResultStatus::ErrorAlreadyLoaded; + + ResultStatus result = ReadProgramId(ncch_program_id); + if (result != ResultStatus::Success) { + return result; + } + + std::string program_id{fmt::format("{:016X}", ncch_program_id)}; + + LOG_INFO(Loader, "Program ID: {}", program_id); + + if (auto room_member = Network::GetRoomMember().lock()) { + Network::GameInfo game_info; + ReadTitle(game_info.name); + game_info.id = ncch_program_id; + room_member->SendGameInfo(game_info); + } + + is_loaded = true; // Set state to loaded + + result = LoadExec(process); // Load the executable into memory for booting + if (ResultStatus::Success != result) + return result; + + system.ArchiveManager().RegisterSelfNCCH(*this); + system.ArchiveManager().RegisterArticSaveDataSource(client); + system.ArchiveManager().RegisterArticExtData(client); + system.ArchiveManager().RegisterArticNCCH(client); + + auto fs_user = system.ServiceManager().GetService("fs:USER"); + fs_user->RegisterSecureValueBackend(std::make_shared(client)); + + ParseRegionLockoutInfo(ncch_program_id); + + return ResultStatus::Success; +} + +ResultStatus Apploader_Artic::IsExecutable(bool& out_executable) { + out_executable = true; + return ResultStatus::Success; +} + +ResultStatus Apploader_Artic::ReadCode(std::vector& buffer) { + // Code is only read once, there is no need to cache it. + + if (!client_connected) + client_connected = client->Connect(); + if (!client_connected) + return ResultStatus::ErrorArtic; + + size_t code_size = program_exheader.codeset_info.text.num_max_pages * Memory::CITRA_PAGE_SIZE; + code_size += program_exheader.codeset_info.ro.num_max_pages * Memory::CITRA_PAGE_SIZE; + code_size += program_exheader.codeset_info.data.num_max_pages * Memory::CITRA_PAGE_SIZE; + + size_t read_amount = 0; + buffer.clear(); + + while (read_amount != code_size) { + size_t to_read = + std::min(client->GetServerRequestMaxSize() - 0x100, code_size - read_amount); + + auto req = client->NewRequest("Process_ReadCode"); + req.AddParameterS32(static_cast(read_amount)); + req.AddParameterS32(static_cast(to_read)); + auto resp = client->Send(req); + if (!resp.has_value() || !resp->Succeeded() || resp->GetMethodResult() != 0) + return ResultStatus::ErrorArtic; + + auto code_buff = resp->GetResponseBuffer(0); + if (!code_buff.has_value() || code_buff->second != to_read) + return ResultStatus::ErrorArtic; + + buffer.resize(read_amount + to_read); + memcpy(buffer.data() + read_amount, code_buff->first, to_read); + read_amount += to_read; + } + + return ResultStatus::Success; +} + +ResultStatus Apploader_Artic::ReadIcon(std::vector& buffer) { + if (!cached_icon.empty()) { + buffer = cached_icon; + return ResultStatus::Success; + } + + if (!client_connected) + client_connected = client->Connect(); + if (!client_connected) + return ResultStatus::ErrorArtic; + + auto req = client->NewRequest("Process_ReadIcon"); + auto resp = client->Send(req); + if (!resp.has_value() || !resp->Succeeded() || resp->GetMethodResult() != 0) + return ResultStatus::ErrorArtic; + + auto icon_buf = resp->GetResponseBuffer(0); + if (!icon_buf.has_value()) + return ResultStatus::ErrorArtic; + + cached_icon.resize(icon_buf->second); + memcpy(cached_icon.data(), icon_buf->first, icon_buf->second); + buffer = cached_icon; + + return ResultStatus::Success; +} + +ResultStatus Apploader_Artic::ReadBanner(std::vector& buffer) { + if (!cached_banner.empty()) { + buffer = cached_banner; + return ResultStatus::Success; + } + + if (!client_connected) + client_connected = client->Connect(); + if (!client_connected) + return ResultStatus::ErrorArtic; + + auto req = client->NewRequest("Process_ReadBanner"); + auto resp = client->Send(req); + if (!resp.has_value() || !resp->Succeeded() || resp->GetMethodResult() != 0) + return ResultStatus::ErrorArtic; + + auto banner_buf = resp->GetResponseBuffer(0); + if (!banner_buf.has_value()) + return ResultStatus::ErrorArtic; + + cached_banner.resize(banner_buf->second); + memcpy(cached_banner.data(), banner_buf->first, banner_buf->second); + buffer = cached_banner; + + return ResultStatus::Success; +} + +ResultStatus Apploader_Artic::ReadLogo(std::vector& buffer) { + if (!cached_logo.empty()) { + buffer = cached_logo; + return ResultStatus::Success; + } + + if (!client_connected) + client_connected = client->Connect(); + if (!client_connected) + return ResultStatus::ErrorArtic; + + auto req = client->NewRequest("Process_ReadLogo"); + auto resp = client->Send(req); + if (!resp.has_value() || !resp->Succeeded() || resp->GetMethodResult() != 0) + return ResultStatus::ErrorArtic; + + auto logo_buf = resp->GetResponseBuffer(0); + if (!logo_buf.has_value()) + return ResultStatus::ErrorArtic; + + cached_logo.resize(logo_buf->second); + memcpy(cached_logo.data(), logo_buf->first, logo_buf->second); + buffer = cached_logo; + + return ResultStatus::Success; +} + +ResultStatus Apploader_Artic::ReadProgramId(u64& out_program_id) { + if (cached_title_id.has_value()) { + out_program_id = *cached_title_id; + return ResultStatus::Success; + } + + if (!client_connected) + client_connected = client->Connect(); + if (!client_connected) + return ResultStatus::ErrorArtic; + + auto req = client->NewRequest("Process_GetTitleID"); + auto resp = client->Send(req); + if (!resp.has_value()) + return ResultStatus::ErrorArtic; + + auto tid_buf = resp->GetResponseBuffer(0); + if (!tid_buf.has_value() || tid_buf->second != sizeof(u64)) + return ResultStatus::ErrorArtic; + + out_program_id = *reinterpret_cast(tid_buf->first); + cached_title_id = out_program_id; + + return ResultStatus::Success; +} + +ResultStatus Apploader_Artic::ReadExtdataId(u64& out_extdata_id) { + if (program_exheader.arm11_system_local_caps.storage_info.other_attributes >> 1) { + // Using extended save data access + // There would be multiple possible extdata IDs in this case. The best we can do for now is + // guessing that the first one would be the main save. + const std::array extdata_ids{{ + program_exheader.arm11_system_local_caps.storage_info.extdata_id0.Value(), + program_exheader.arm11_system_local_caps.storage_info.extdata_id1.Value(), + program_exheader.arm11_system_local_caps.storage_info.extdata_id2.Value(), + program_exheader.arm11_system_local_caps.storage_info.extdata_id3.Value(), + program_exheader.arm11_system_local_caps.storage_info.extdata_id4.Value(), + program_exheader.arm11_system_local_caps.storage_info.extdata_id5.Value(), + }}; + for (u64 id : extdata_ids) { + if (id) { + // Found a non-zero ID, use it + out_extdata_id = id; + return ResultStatus::Success; + } + } + + return ResultStatus::ErrorNotUsed; + } + + out_extdata_id = program_exheader.arm11_system_local_caps.storage_info.ext_save_data_id; + return Loader::ResultStatus::Success; +} + +ResultStatus Apploader_Artic::ReadRomFS(std::shared_ptr& romfs_file) { + main_romfs_reader = romfs_file = std::make_shared(client, false); + return static_cast(romfs_file.get())->OpenStatus(); +} + +ResultStatus Apploader_Artic::ReadUpdateRomFS(std::shared_ptr& romfs_file) { + update_romfs_reader = romfs_file = std::make_shared(client, true); + return static_cast(romfs_file.get())->OpenStatus(); +} + +ResultStatus Apploader_Artic::DumpRomFS(const std::string& target_path) { + return ResultStatus::ErrorNotImplemented; +} + +ResultStatus Apploader_Artic::DumpUpdateRomFS(const std::string& target_path) { + return ResultStatus::ErrorNotImplemented; +} + +ResultStatus Apploader_Artic::ReadTitle(std::string& title) { + std::vector data; + Loader::SMDH smdh; + ResultStatus result = ReadIcon(data); + if (result != ResultStatus::Success) { + return result; + } + + if (!Loader::IsValidSMDH(data)) { + return ResultStatus::ErrorInvalidFormat; + } + + std::memcpy(&smdh, data.data(), sizeof(Loader::SMDH)); + + const auto& short_title = smdh.GetShortTitle(SMDH::TitleLanguage::English); + auto title_end = std::find(short_title.begin(), short_title.end(), u'\0'); + title = Common::UTF16ToUTF8(std::u16string{short_title.begin(), title_end}); + + return ResultStatus::Success; +} + +} // namespace Loader diff --git a/src/core/loader/artic.h b/src/core/loader/artic.h new file mode 100644 index 000000000..d51202b2e --- /dev/null +++ b/src/core/loader/artic.h @@ -0,0 +1,135 @@ +// Copyright 2024 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include "common/common_types.h" +#include "common/swap.h" +#include "core/core.h" +#include "core/file_sys/ncch_container.h" +#include "core/hle/service/fs/fs_user.h" +#include "core/loader/loader.h" +#include "network/artic_base/artic_base_client.h" + +namespace Loader { + +/// Loads an NCCH file (e.g. from a CCI, or the first NCCH in a CXI) +class Apploader_Artic final : public AppLoader { +public: + Apploader_Artic(Core::System& system_, const std::string& server_addr, u16 server_port) + : AppLoader(system_, FileUtil::IOFile()) { + client = std::make_shared(server_addr, server_port); + client->SetCommunicationErrorCallback([&system_]() { + system_.SetStatus(Core::System::ResultStatus::ErrorArticDisconnected); + }); + client->SetArticReportTrafficCallback( + [&system_](u32 bytes) { system_.ReportArticTraffic(bytes); }); + client->SetReportArticEventCallback([&system_](u64 event) { + Core::PerfStats::PerfArticEventBits ev = + static_cast(event & 0xFFFFFFFF); + bool set = (event > 32) != 0; + system_.ReportPerfArticEvent(ev, set); + }); + } + + ~Apploader_Artic() override; + + /** + * Returns the type of the file + * @param file FileUtil::IOFile open file + * @return FileType found, or FileType::Error if this loader doesn't know it + */ + static FileType IdentifyType(FileUtil::IOFile& file); + + FileType GetFileType() override { + return IdentifyType(file); + } + + [[nodiscard]] std::span GetPreferredRegions() const override { + return preferred_regions; + } + + ResultStatus Load(std::shared_ptr& process) override; + + std::pair, ResultStatus> LoadCoreVersion() override; + + /** + * Loads the Exheader and returns the system mode for this application. + * @returns A pair with the optional system mode, and and the status. + */ + std::pair, ResultStatus> LoadKernelMemoryMode() override; + + std::pair, ResultStatus> LoadNew3dsHwCapabilities() + override; + + ResultStatus IsExecutable(bool& out_executable) override; + + ResultStatus ReadCode(std::vector& buffer) override; + + ResultStatus ReadIcon(std::vector& buffer) override; + + ResultStatus ReadBanner(std::vector& buffer) override; + + ResultStatus ReadLogo(std::vector& buffer) override; + + ResultStatus ReadProgramId(u64& out_program_id) override; + + ResultStatus ReadExtdataId(u64& out_extdata_id) override; + + ResultStatus ReadRomFS(std::shared_ptr& romfs_file) override; + + ResultStatus ReadUpdateRomFS(std::shared_ptr& romfs_file) override; + + ResultStatus DumpRomFS(const std::string& target_path) override; + + ResultStatus DumpUpdateRomFS(const std::string& target_path) override; + + ResultStatus ReadTitle(std::string& title) override; + + bool SupportsSaveStates() override { + return false; + } + + bool SupportsMultipleInstancesForSameFile() override { + return false; + } + +private: + /** + * Loads .code section into memory for booting + * @param process The newly created process + * @return ResultStatus result of function + */ + ResultStatus LoadExec(std::shared_ptr& process); + + /// Reads the region lockout info in the SMDH and send it to CFG service + /// If an SMDH is not present, the program ID is compared against a list + /// of known system titles to determine the region. + void ParseRegionLockoutInfo(u64 program_id); + + bool LoadExheader(); + + ResultStatus LoadProductInfo(Service::FS::FS_USER::ProductInfo& out); + + ExHeader_Header program_exheader{}; + bool program_exheader_loaded = false; + + std::optional cached_title_id = std::nullopt; + std::optional cached_product_info = std::nullopt; + std::vector cached_icon; + std::vector cached_banner; + std::vector cached_logo; + + std::vector preferred_regions; + + std::string server_address; + std::shared_ptr client; + bool client_connected = false; + + std::shared_ptr main_romfs_reader = nullptr; + std::shared_ptr update_romfs_reader = nullptr; +}; + +} // namespace Loader diff --git a/src/core/loader/loader.cpp b/src/core/loader/loader.cpp index b282d0a24..f520e4f00 100644 --- a/src/core/loader/loader.cpp +++ b/src/core/loader/loader.cpp @@ -9,6 +9,7 @@ #include "core/core.h" #include "core/hle/kernel/process.h" #include "core/loader/3dsx.h" +#include "core/loader/artic.h" #include "core/loader/elf.h" #include "core/loader/ncch.h" @@ -74,6 +75,8 @@ const char* GetFileTypeString(FileType type) { return "ELF"; case FileType::THREEDSX: return "3DSX"; + case FileType::ARTIC: + return "ARTIC"; case FileType::Error: case FileType::Unknown: break; @@ -108,12 +111,39 @@ static std::unique_ptr GetFileLoader(Core::System& system, FileUtil:: case FileType::CCI: return std::make_unique(system, std::move(file), filepath); + case FileType::ARTIC: { + auto strToUInt = [](const std::string& str) -> int { + char* pEnd = NULL; + unsigned long ul = ::strtoul(str.c_str(), &pEnd, 10); + if (*pEnd) + return -1; + return static_cast(ul); + }; + + u16 port = 5543; + std::string server_addr = filename; + auto pos = server_addr.find(":"); + if (pos != server_addr.npos) { + int newVal = strToUInt(server_addr.substr(pos + 1)); + if (newVal >= 0 && newVal <= 0xFFFF) { + port = static_cast(newVal); + server_addr = server_addr.substr(0, pos); + } + } + return std::make_unique(system, server_addr, port); + } + default: return nullptr; } } std::unique_ptr GetLoader(const std::string& filename) { + if (filename.starts_with("articbase://")) { + return GetFileLoader(Core::System::GetInstance(), FileUtil::IOFile(), FileType::ARTIC, + filename.substr(12), ""); + } + FileUtil::IOFile file(filename, "rb"); if (!file.IsOpen()) { LOG_ERROR(Loader, "Failed to load file {}", filename); diff --git a/src/core/loader/loader.h b/src/core/loader/loader.h index 737075249..a47d5ae1d 100644 --- a/src/core/loader/loader.h +++ b/src/core/loader/loader.h @@ -31,6 +31,7 @@ enum class FileType { CIA, ELF, THREEDSX, // 3DSX + ARTIC, }; /** @@ -73,6 +74,7 @@ enum class ResultStatus { ErrorMemoryAllocationFailed, ErrorEncrypted, ErrorGbaTitle, + ErrorArtic, }; constexpr u32 MakeMagic(char a, char b, char c, char d) { @@ -264,6 +266,14 @@ public: return ResultStatus::ErrorNotImplemented; } + virtual bool SupportsSaveStates() { + return true; + } + + virtual bool SupportsMultipleInstancesForSameFile() { + return true; + } + protected: Core::System& system; FileUtil::IOFile file; diff --git a/src/core/perf_stats.cpp b/src/core/perf_stats.cpp index 0d30a9187..c05cf80e5 100644 --- a/src/core/perf_stats.cpp +++ b/src/core/perf_stats.cpp @@ -101,6 +101,8 @@ PerfStats::Results PerfStats::GetAndResetStats(microseconds current_system_time_ last_stats.frametime = duration_cast(accumulated_frametime).count() / static_cast(system_frames); last_stats.emulation_speed = system_us_per_second.count() / 1'000'000.0; + last_stats.artic_transmitted = static_cast(artic_transmitted) / interval; + last_stats.artic_events.raw = artic_events.raw | prev_artic_event.raw; // Reset counters reset_point = now; @@ -108,6 +110,8 @@ PerfStats::Results PerfStats::GetAndResetStats(microseconds current_system_time_ accumulated_frametime = Clock::duration::zero(); system_frames = 0; game_frames = 0; + artic_transmitted = 0; + prev_artic_event.raw &= artic_events.raw; return last_stats; } diff --git a/src/core/perf_stats.h b/src/core/perf_stats.h index d2451dbc8..5cd69afb2 100644 --- a/src/core/perf_stats.h +++ b/src/core/perf_stats.h @@ -9,6 +9,7 @@ #include #include #include +#include "common/bit_field.h" #include "common/common_types.h" #include "common/thread.h" @@ -25,6 +26,28 @@ public: using Clock = std::chrono::high_resolution_clock; + enum class PerfArticEventBits { + NONE = 0, + ARTIC_SAVE_DATA = (1 << 0), + ARTIC_EXT_DATA = (1 << 1), + ARTIC_BOSS_EXT_DATA = (1 << 2), + ARTIC_SHARED_EXT_DATA = (1 << 3), + }; + union PerfArticEvents { + u32 raw{}; + BitField<0, 1, u32> artic_save_data; + BitField<1, 1, u32> artic_ext_data; + BitField<2, 1, u32> artic_boss_ext_data; + BitField<3, 1, u32> artic_shared_ext_data; + + void Set(PerfArticEventBits event, bool set) { + raw = (raw & ~static_cast(event)) | (set ? static_cast(event) : 0); + } + bool Get(PerfArticEventBits event) { + return (raw & static_cast(event)) != 0; + } + }; + struct Results { /// System FPS (LCD VBlanks) in Hz double system_fps; @@ -34,6 +57,10 @@ public: double frametime; /// Ratio of walltime / emulated time elapsed double emulation_speed; + /// Artic base bytes per second + double artic_transmitted = 0; + /// Artic base events + PerfArticEvents artic_events{}; }; void BeginSystemFrame(); @@ -55,6 +82,19 @@ public: */ double GetLastFrameTimeScale() const; + void AddArticBaseTraffic(u32 bytes) { + artic_transmitted += bytes; + } + + void ReportPerfArticEvent(PerfArticEventBits event, bool set) { + if (set) { + artic_events.Set(event, set); + prev_artic_event.Set(event, set); + } else { + artic_events.Set(event, set); + } + } + private: mutable std::mutex object_mutex; @@ -77,6 +117,12 @@ private: u32 system_frames = 0; /// Cumulative number of game frames (GSP frame submissions) since last reset u32 game_frames = 0; + /// Cumulative number of transmitted artic base traffic + std::atomic artic_transmitted = 0; + // System events that affect performance + PerfArticEvents artic_events; + + PerfArticEvents prev_artic_event; /// Point when the previous system frame ended Clock::time_point previous_frame_end = reset_point; diff --git a/src/core/savestate.cpp b/src/core/savestate.cpp index bc1f53684..bb750d0c9 100644 --- a/src/core/savestate.cpp +++ b/src/core/savestate.cpp @@ -13,6 +13,7 @@ #include "common/swap.h" #include "common/zstd_compression.h" #include "core/core.h" +#include "core/loader/loader.h" #include "core/movie.h" #include "core/savestate.h" #include "core/savestate_data.h" @@ -122,6 +123,12 @@ std::vector ListSaveStates(u64 program_id, u64 movie_id) { } void System::SaveState(u32 slot) const { + if (app_loader) { + if (!app_loader->SupportsSaveStates()) { + throw std::runtime_error("The current app loader doesn't support save states"); + } + } + std::ostringstream sstream{std::ios_base::binary}; // Serialize oarchive oa{sstream}; @@ -164,6 +171,11 @@ void System::SaveState(u32 slot) const { } void System::LoadState(u32 slot) { + if (app_loader) { + if (!app_loader->SupportsSaveStates()) { + throw std::runtime_error("The current app loader doesn't support save states"); + } + } if (Network::GetRoomMember().lock()->IsConnected()) { throw std::runtime_error("Unable to load while connected to multiplayer"); } diff --git a/src/network/CMakeLists.txt b/src/network/CMakeLists.txt index d8058dfd2..3feb6f479 100644 --- a/src/network/CMakeLists.txt +++ b/src/network/CMakeLists.txt @@ -1,6 +1,9 @@ add_library(network STATIC announce_multiplayer_session.cpp announce_multiplayer_session.h + artic_base/artic_base_client.cpp + artic_base/artic_base_client.h + artic_base/artic_base_common.h network.cpp network.h network_settings.cpp @@ -12,6 +15,8 @@ add_library(network STATIC room.h room_member.cpp room_member.h + socket_manager.cpp + socket_manager.h verify_user.cpp verify_user.h ) diff --git a/src/network/artic_base/artic_base_client.cpp b/src/network/artic_base/artic_base_client.cpp new file mode 100644 index 000000000..5de531662 --- /dev/null +++ b/src/network/artic_base/artic_base_client.cpp @@ -0,0 +1,735 @@ +// Copyright 2024 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "artic_base_client.h" +#include "common/assert.h" +#include "common/logging/log.h" + +#include "chrono" +#include "limits.h" +#include "memory" +#include "sstream" + +#ifdef _WIN32 +#include +#include +#else +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#endif + +#ifdef _WIN32 +#define WSAEAGAIN WSAEWOULDBLOCK +#define WSAEMULTIHOP -1 // Invalid dummy value +#define ERRNO(x) WSA##x +#define GET_ERRNO WSAGetLastError() +#define poll(x, y, z) WSAPoll(x, y, z); +#define SHUT_RD SD_RECEIVE +#define SHUT_WR SD_SEND +#define SHUT_RDWR SD_BOTH +#else +#define ERRNO(x) x +#define GET_ERRNO errno +#define closesocket(x) close(x) +#endif + +// #define DISABLE_PING_TIMEOUT + +namespace Network::ArticBase { + +using namespace std::chrono_literals; + +bool Client::Request::AddParameterS8(s8 parameter) { + if (parameters.size() >= max_param_count) { + LOG_ERROR(Network, "Too many parameters added to method: {}", method_name); + return false; + } + auto& param = parameters.emplace_back(); + param.type = ArticBaseCommon::RequestParameterType::IN_INTEGER_8; + std::memcpy(param.data, ¶meter, sizeof(s8)); + return true; +} + +bool Client::Request::AddParameterS16(s16 parameter) { + if (parameters.size() >= max_param_count) { + LOG_ERROR(Network, "Too many parameters added to method: {}", method_name); + return false; + } + auto& param = parameters.emplace_back(); + param.type = ArticBaseCommon::RequestParameterType::IN_INTEGER_16; + std::memcpy(param.data, ¶meter, sizeof(s16)); + return true; +} + +bool Client::Request::AddParameterS32(s32 parameter) { + if (parameters.size() >= max_param_count) { + LOG_ERROR(Network, "Too many parameters added to method: {}", method_name); + return false; + } + auto& param = parameters.emplace_back(); + param.type = ArticBaseCommon::RequestParameterType::IN_INTEGER_32; + std::memcpy(param.data, ¶meter, sizeof(s32)); + return true; +} + +bool Client::Request::AddParameterS64(s64 parameter) { + if (parameters.size() >= max_param_count) { + LOG_ERROR(Network, "Too many parameters added to method: {}", method_name); + return false; + } + auto& param = parameters.emplace_back(); + param.type = ArticBaseCommon::RequestParameterType::IN_INTEGER_64; + std::memcpy(param.data, ¶meter, sizeof(s64)); + return true; +} + +bool Client::Request::AddParameterBuffer(const void* buffer, size_t size) { + if (parameters.size() >= max_param_count) { + LOG_ERROR(Network, "Too many parameters added to method: {}", method_name); + return false; + } + auto& param = parameters.emplace_back(); + if (size <= sizeof(param.data)) { + param.type = ArticBaseCommon::RequestParameterType::IN_SMALL_BUFFER; + std::memcpy(param.data, buffer, size); + param.parameterSize = static_cast(size); + } else { + param.type = ArticBaseCommon::RequestParameterType::IN_BIG_BUFFER; + param.bigBufferID = static_cast(pending_big_buffers.size()); + s32 size_32 = static_cast(size); + std::memcpy(param.data, &size_32, sizeof(size_32)); + pending_big_buffers.push_back(std::make_pair(buffer, size)); + } + return true; +} + +Client::Request::Request(u32 request_id, const std::string& method, size_t max_params) { + method_name = method; + max_param_count = max_params; + request_packet.requestID = request_id; + std::memcpy(request_packet.method.data(), method.data(), + std::min(request_packet.method.size(), method.size())); +} + +Client::~Client() { + StopImpl(false); + + for (auto it = handlers.begin(); it != handlers.end(); it++) { + Handler* h = *it; + h->thread->join(); + delete h; + } + + if (ping_thread.joinable()) { + ping_thread.join(); + } + + SocketManager::DisableSockets(); +} + +bool Client::Connect() { + if (connected) + return true; + + auto str_to_int = [](const std::string& str) -> int { + char* pEnd = NULL; + unsigned long ul = ::strtoul(str.c_str(), &pEnd, 10); + if (*pEnd) + return -1; + return static_cast(ul); + }; + + struct addrinfo hints, *addrinfo; + memset(&hints, 0, sizeof(hints)); + hints.ai_socktype = SOCK_STREAM; + hints.ai_family = AF_INET; + + LOG_INFO(Network, "Starting Artic Base Client"); + + if (getaddrinfo(address.data(), NULL, &hints, &addrinfo) != 0) { + LOG_ERROR(Network, "Failed to get server address"); + SignalCommunicationError(); + return false; + } + + main_socket = ::socket(AF_INET, SOCK_STREAM, 0); + if (main_socket == -1) { + LOG_ERROR(Network, "Failed to create socket"); + SignalCommunicationError(); + return false; + } + + if (!SetNonBlock(main_socket, true)) { + shutdown(main_socket, SHUT_RDWR); + closesocket(main_socket); + LOG_ERROR(Network, "Cannot set non-blocking socket mode"); + SignalCommunicationError(); + return false; + } + + struct sockaddr_in servaddr = {0}; + servaddr.sin_family = AF_INET; + servaddr.sin_addr.s_addr = ((struct sockaddr_in*)(addrinfo->ai_addr))->sin_addr.s_addr; + servaddr.sin_port = htons(port); + freeaddrinfo(addrinfo); + + if (!ConnectWithTimeout(main_socket, &servaddr, sizeof(servaddr), 10)) { + closesocket(main_socket); + LOG_ERROR(Network, "Failed to connect"); + SignalCommunicationError(); + return false; + } + + auto version = SendSimpleRequest("VERSION"); + if (version.has_value()) { + int version_value = str_to_int(*version); + if (version_value != SERVER_VERSION) { + shutdown(main_socket, SHUT_RDWR); + closesocket(main_socket); + LOG_ERROR(Network, "Incompatible server version: {}", version_value); + SignalCommunicationError(); + return false; + } + } else { + shutdown(main_socket, SHUT_RDWR); + closesocket(main_socket); + LOG_ERROR(Network, "Couldn't fetch server version."); + SignalCommunicationError(); + return false; + } + + auto max_work_size = SendSimpleRequest("MAXSIZE"); + int max_work_size_value = -1; + if (max_work_size.has_value()) { + max_work_size_value = str_to_int(*max_work_size); + } + if (max_work_size_value < 0) { + shutdown(main_socket, SHUT_RDWR); + closesocket(main_socket); + LOG_ERROR(Network, "Couldn't fetch server work ram size"); + SignalCommunicationError(); + return false; + } + max_server_work_ram = max_work_size_value; + + auto max_params = SendSimpleRequest("MAXPARAM"); + int max_param_value = -1; + if (max_params.has_value()) { + max_param_value = str_to_int(*max_params); + } + if (max_param_value < 0) { + shutdown(main_socket, SHUT_RDWR); + closesocket(main_socket); + LOG_ERROR(Network, "Couldn't fetch server max params"); + SignalCommunicationError(); + return false; + } + max_parameter_count = max_param_value; + + auto worker_ports = SendSimpleRequest("PORTS"); + if (!worker_ports.has_value()) { + shutdown(main_socket, SHUT_RDWR); + closesocket(main_socket); + LOG_ERROR(Network, "Couldn't fetch server worker ports"); + SignalCommunicationError(); + return false; + } + std::vector ports; + std::string str_port; + std::stringstream ss_port(worker_ports.value()); + while (std::getline(ss_port, str_port, ',')) { + int port = str_to_int(str_port); + if (port < 0 || port > USHRT_MAX) { + shutdown(main_socket, SHUT_RDWR); + closesocket(main_socket); + LOG_ERROR(Network, "Couldn't parse server worker ports"); + SignalCommunicationError(); + return false; + } + ports.push_back(static_cast(port)); + } + if (ports.empty()) { + shutdown(main_socket, SHUT_RDWR); + closesocket(main_socket); + LOG_ERROR(Network, "Couldn't parse server worker ports"); + SignalCommunicationError(); + return false; + } + + for (int i = 0; i < 101; i++) { + auto ready_server = SendSimpleRequest("READY"); + if (!ready_server.has_value() || i == 100) { + shutdown(main_socket, SHUT_RDWR); + closesocket(main_socket); + LOG_ERROR(Network, "Couldn't fetch server readiness"); + SignalCommunicationError(); + return false; + } + if (*ready_server == "1") + break; + std::this_thread::sleep_for(100ms); + } + + ping_thread = std::thread(&Client::PingFunction, this); + + int i = 0; + running_handlers = ports.size(); + for (auto it = ports.begin(); it != ports.end(); it++) { + handlers.push_back(new Handler(*this, static_cast(servaddr.sin_addr.s_addr), *it, i)); + i++; + } + + connected = true; + return true; +} + +void Client::StopImpl(bool from_error) { + bool expected = false; + if (!stopped.compare_exchange_strong(expected, true)) + return; + + if (!from_error) { + SendSimpleRequest("STOP"); + } + + if (ping_thread.joinable()) { + std::scoped_lock l2(ping_cv_mutex); + ping_run = false; + ping_cv.notify_one(); + } + + // Stop handlers + for (auto it = handlers.begin(); it != handlers.end(); it++) { + Handler* handler = *it; + handler->should_run = false; + // Shouldn't matter if the socket is shut down twice + shutdown(handler->handler_socket, SHUT_RDWR); + closesocket(handler->handler_socket); + } + + // Close main socket + shutdown(main_socket, SHUT_RDWR); + closesocket(main_socket); +} + +std::optional> Client::Response::GetResponseBuffer(u32 buffer_id) const { + if (!resp_data_buffer) + return std::nullopt; + + char* resp_data_buffer_end = resp_data_buffer + resp_data_size; + char* resp_data_buffer_start = resp_data_buffer; + while (resp_data_buffer_start + sizeof(ArticBaseCommon::Buffer) < resp_data_buffer_end) { + ArticBaseCommon::Buffer* curr_buffer = + reinterpret_cast(resp_data_buffer_start); + resp_data_buffer_start += sizeof(ArticBaseCommon::Buffer); + if (curr_buffer->bufferID == buffer_id) { + if (curr_buffer->data + curr_buffer->bufferSize <= resp_data_buffer_end) { + return std::make_pair(curr_buffer->data, curr_buffer->bufferSize); + } else { + return std::nullopt; + } + } + resp_data_buffer_start += curr_buffer->bufferSize; + } + return std::nullopt; +} + +std::optional Client::Send(Request& request) { + if (stopped) + return std::nullopt; + + request.request_packet.parameterCount = static_cast(request.parameters.size()); + PendingResponse resp(request); + + { + std::scoped_lock l(recv_map_mutex); + pending_responses[request.request_packet.requestID] = &resp; + } + + auto respPacket = SendRequestPacket(request.request_packet, false, request.parameters); + if (stopped || !respPacket.has_value()) { + std::scoped_lock l(recv_map_mutex); + pending_responses.erase(request.request_packet.requestID); + return std::nullopt; + } + + std::unique_lock cv_lk(resp.cv_mutex); + resp.cv.wait(cv_lk, [&resp]() { return resp.is_done; }); + + return std::optional(std::move(resp.response)); +} + +void Client::SignalCommunicationError() { + StopImpl(true); + LOG_CRITICAL(Network, "Communication error"); + if (communication_error_callback) + communication_error_callback(); +} + +void Client::PingFunction() { + // Max silence time => 7 secs interval + 3 secs wait + 10 seconds timeout = 25 seconds + while (ping_run) { + std::chrono::time_point last = last_sent_request; + if (std::chrono::steady_clock::now() - last > std::chrono::seconds(7)) { +#ifdef DISABLE_PING_TIMEOUT + client->last_sent_request = std::chrono::steady_clock::now(); +#else + auto ping_reply = SendSimpleRequest("PING"); + if (!ping_reply.has_value()) { + SignalCommunicationError(); + break; + } +#endif // DISABLE_PING_TIMEOUT + } + + std::unique_lock lk(ping_cv_mutex); + ping_cv.wait_for(lk, std::chrono::seconds(3)); + } +} + +bool Client::ConnectWithTimeout(SocketHolder sockFD, void* server_addr, size_t server_addr_len, + int timeout_seconds) { + + int res = ::connect(sockFD, (struct sockaddr*)server_addr, static_cast(server_addr_len)); + if (res == -1 && ((GET_ERRNO == ERRNO(EINPROGRESS) || GET_ERRNO == ERRNO(EWOULDBLOCK)))) { + struct timeval tv; + fd_set fdset; + FD_ZERO(&fdset); + FD_SET(sockFD, &fdset); + + tv.tv_sec = timeout_seconds; + tv.tv_usec = 0; + int select_res = ::select(static_cast(sockFD + 1), NULL, &fdset, NULL, &tv); +#ifdef _WIN32 + if (select_res == 0) { + return false; + } +#else + bool select_good = false; + if (select_res == 1) { + int so_error; + socklen_t len = sizeof so_error; + + getsockopt(sockFD, SOL_SOCKET, SO_ERROR, &so_error, &len); + + if (so_error == 0) { + select_good = true; + } + } + if (!select_good) { + return false; + } +#endif // _WIN32 + + } else if (res == -1) { + return false; + } + return true; +} + +bool Client::SetNonBlock(SocketHolder sockFD, bool nonBlocking) { + bool blocking = !nonBlocking; +#ifdef _WIN32 + unsigned long nonblocking = (blocking) ? 0 : 1; + int ret = ioctlsocket(sockFD, FIONBIO, &nonblocking); + if (ret == -1) { + return false; + } +#else + int flags = ::fcntl(sockFD, F_GETFL, 0); + if (flags == -1) { + return false; + } + + flags &= ~O_NONBLOCK; + if (!blocking) { // O_NONBLOCK + flags |= O_NONBLOCK; + } + + const int ret = ::fcntl(sockFD, F_SETFL, flags); + if (ret == -1) { + return false; + } +#endif + return true; +} + +bool Client::Read(SocketHolder sockFD, void* buffer, size_t size, + const std::chrono::nanoseconds& timeout) { + size_t read_bytes = 0; + auto before = std::chrono::steady_clock::now(); + while (read_bytes != size) { + int new_read = + ::recv(sockFD, (char*)((uintptr_t)buffer + read_bytes), (int)(size - read_bytes), 0); + if (new_read < 0) { + if (GET_ERRNO == ERRNO(EWOULDBLOCK) && + (timeout == std::chrono::nanoseconds(0) || + std::chrono::steady_clock::now() - before < timeout)) { + continue; + } + read_bytes = 0; + break; + } + if (report_traffic_callback && new_read) { + report_traffic_callback(new_read); + } + read_bytes += new_read; + } + return read_bytes == size; +} + +bool Client::Write(SocketHolder sockFD, const void* buffer, size_t size, + const std::chrono::nanoseconds& timeout) { + size_t write_bytes = 0; + auto before = std::chrono::steady_clock::now(); + while (write_bytes != size) { + int new_written = ::send(sockFD, (const char*)((uintptr_t)buffer + write_bytes), + (int)(size - write_bytes), 0); + if (new_written < 0) { + if (GET_ERRNO == ERRNO(EWOULDBLOCK) && + (timeout == std::chrono::nanoseconds(0) || + std::chrono::steady_clock::now() - before < timeout)) { + continue; + } + write_bytes = 0; + break; + } + if (report_traffic_callback && new_written) { + report_traffic_callback(new_written); + } + write_bytes += new_written; + } + return write_bytes == size; +} + +std::optional Client::SendRequestPacket( + const ArticBaseCommon::RequestPacket& req, bool expect_response, + const std::vector& params, + const std::chrono::nanoseconds& read_timeout) { + std::scoped_lock l(send_mutex); + + if (main_socket == -1) { + return std::nullopt; + } + + if (!Write(main_socket, &req, sizeof(req))) { + LOG_WARNING(Network, "Failed to write to socket"); + SignalCommunicationError(); + return std::nullopt; + } + + if (!params.empty()) { + if (!Write(main_socket, params.data(), + params.size() * sizeof(ArticBaseCommon::RequestParameter))) { + LOG_WARNING(Network, "Failed to write to socket"); + SignalCommunicationError(); + return std::nullopt; + } + } + + ArticBaseCommon::DataPacket resp; + if (expect_response) { + if (!Read(main_socket, &resp, sizeof(resp), read_timeout)) { + LOG_WARNING(Network, "Failed to read from socket"); + SignalCommunicationError(); + return std::nullopt; + } + + if (resp.requestID != req.requestID) { + return std::nullopt; + } + } + + last_sent_request = std::chrono::steady_clock::now(); + return resp; +} + +std::optional Client::SendSimpleRequest(const std::string& method) { + ArticBaseCommon::RequestPacket req{}; + req.requestID = GetNextRequestID(); + const std::string final_method = "$" + method; + if (final_method.size() > sizeof(req.method)) { + return std::nullopt; + } + std::memcpy(req.method.data(), final_method.data(), final_method.size()); + auto resp = SendRequestPacket(req, true, {}, std::chrono::seconds(10)); + if (!resp.has_value() || resp->requestID != req.requestID) { + return std::nullopt; + } + char respBody[sizeof(ArticBaseCommon::DataPacket::dataRaw) + 1] = {0}; + std::memcpy(respBody, resp->dataRaw, sizeof(ArticBaseCommon::DataPacket::dataRaw)); + return respBody; +} + +Client::Handler::Handler(Client& _client, u32 _addr, u16 _port, int _id) + : id(_id), client(_client), addr(_addr), port(_port) { + thread = new std::thread( + [](Handler* handler) { + handler->RunLoop(); + handler->should_run = false; + if (--handler->client.running_handlers == 0) { + handler->client.OnAllHandlersFinished(); + } + }, + this); +} + +void Client::Handler::RunLoop() { + handler_socket = ::socket(AF_INET, SOCK_STREAM, 0); + if (handler_socket == -1) { + LOG_ERROR(Network, "Failed to create socket"); + return; + } + + if (!SetNonBlock(handler_socket, true)) { + closesocket(handler_socket); + client.SignalCommunicationError(); + LOG_ERROR(Network, "Cannot set non-blocking socket mode"); + return; + } + + struct sockaddr_in servaddr = {0}; + servaddr.sin_family = AF_INET; + servaddr.sin_addr.s_addr = static_cast(addr); + servaddr.sin_port = htons(port); + + if (!ConnectWithTimeout(handler_socket, &servaddr, sizeof(servaddr), 10)) { + closesocket(handler_socket); + LOG_ERROR(Network, "Failed to connect"); + client.SignalCommunicationError(); + return; + } + + const auto signal_error = [&] { + if (should_run) { + client.SignalCommunicationError(); + } + }; + + ArticBaseCommon::DataPacket dataPacket; + u32 retry_count = 0; + while (should_run) { + if (!client.Read(handler_socket, &dataPacket, sizeof(dataPacket))) { + if (should_run) { + LOG_WARNING(Network, "Failed to read from socket"); + std::this_thread::sleep_for(100ms); + if (++retry_count == 300) { + signal_error(); + break; + } + continue; + } else { + break; + } + } + retry_count = 0; + + PendingResponse* pending_response; + { + std::scoped_lock l(client.recv_map_mutex); + auto it = client.pending_responses.find(dataPacket.requestID); + if (it == client.pending_responses.end()) { + continue; + } + pending_response = it->second; + } + + switch (dataPacket.resp.articResult) { + case ArticBaseCommon::ResponseMethod::ArticResult::SUCCESS: { + pending_response->response.articResult = dataPacket.resp.articResult; + pending_response->response.methodResult = dataPacket.resp.methodResult; + if (dataPacket.resp.bufferSize) { + pending_response->response.resp_data_buffer = + reinterpret_cast(operator new(dataPacket.resp.bufferSize)); + ASSERT_MSG(pending_response->response.resp_data_buffer != nullptr, + "ArticBase Handler: Cannot allocate buffer"); + pending_response->response.resp_data_size = + static_cast(dataPacket.resp.bufferSize); + if (!client.Read(handler_socket, pending_response->response.resp_data_buffer, + dataPacket.resp.bufferSize)) { + signal_error(); + } + } + } break; + case ArticBaseCommon::ResponseMethod::ArticResult::METHOD_NOT_FOUND: { + LOG_ERROR(Network, "Method {} not found by server", + pending_response->request.method_name); + pending_response->response.articResult = dataPacket.resp.articResult; + } break; + + case ArticBaseCommon::ResponseMethod::ArticResult::PROVIDE_INPUT: { + size_t bufferID = static_cast(dataPacket.resp.provideInputBufferID); + if (bufferID >= pending_response->request.pending_big_buffers.size() || + pending_response->request.pending_big_buffers[bufferID].second != + static_cast(dataPacket.resp.bufferSize)) { + LOG_ERROR(Network, "Method {} incorrect big buffer state {}", + pending_response->request.method_name, bufferID); + dataPacket.resp.articResult = + ArticBaseCommon::ResponseMethod::ArticResult::METHOD_ERROR; + if (client.Write(handler_socket, &dataPacket, sizeof(dataPacket))) { + continue; + } else { + signal_error(); + } + } else { + auto& buffer = pending_response->request.pending_big_buffers[bufferID]; + if (client.Write(handler_socket, &dataPacket, sizeof(dataPacket))) { + if (client.Write(handler_socket, buffer.first, buffer.second)) { + continue; + } else { + signal_error(); + } + } else { + signal_error(); + } + } + } break; + case ArticBaseCommon::ResponseMethod::ArticResult::METHOD_ERROR: + default: { + LOG_ERROR(Network, "Method {} error {}", pending_response->request.method_name, + dataPacket.resp.methodResult); + pending_response->response.articResult = dataPacket.resp.articResult; + pending_response->response.methodState = + static_cast(dataPacket.resp.methodResult); + } break; + } + + { + std::scoped_lock l(client.recv_map_mutex); + client.pending_responses.erase(dataPacket.requestID); + } + + { + std::scoped_lock lk(pending_response->cv_mutex); + pending_response->is_done = true; + pending_response->cv.notify_one(); + } + } + should_run = false; + shutdown(handler_socket, SHUT_RDWR); + closesocket(handler_socket); +} + +void Client::OnAllHandlersFinished() { + // If no handlers are running, signal all pending requests so that + // they don't become stuck. + std::scoped_lock l(recv_map_mutex); + for (auto& [id, response] : pending_responses) { + std::scoped_lock l2(response->cv_mutex); + response->is_done = true; + response->cv.notify_one(); + } + pending_responses.clear(); +} + +} // namespace Network::ArticBase \ No newline at end of file diff --git a/src/network/artic_base/artic_base_client.h b/src/network/artic_base/artic_base_client.h new file mode 100644 index 000000000..4e9bb4ec8 --- /dev/null +++ b/src/network/artic_base/artic_base_client.h @@ -0,0 +1,288 @@ +// Copyright 2024 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once +#include "condition_variable" +#include "cstring" +#include "functional" +#include "map" +#include "memory" +#include "mutex" +#include "optional" +#include "string" +#include "thread" +#include "utility" + +#include "artic_base_common.h" +#include "network/socket_manager.h" + +#ifdef _WIN32 +using SocketHolder = unsigned long long; +#else +using SocketHolder = int; +#endif // _WIN32 + +namespace Network::ArticBase { + +class Client { +public: + class Request { + public: + bool AddParameterS8(s8 parameter); + bool AddParameterU8(u8 parameter) { + return AddParameterS8(static_cast(parameter)); + } + bool AddParameterS16(s16 parameter); + bool AddParameterU16(u16 parameter) { + return AddParameterS16(static_cast(parameter)); + } + bool AddParameterS32(s32 parameter); + bool AddParameterU32(u32 parameter) { + return AddParameterS32(static_cast(parameter)); + } + bool AddParameterS64(s64 parameter); + bool AddParameterU64(u64 parameter) { + return AddParameterS64(static_cast(parameter)); + } + + // NOTE: Buffer pointer must remain alive until the response is received + bool AddParameterBuffer(const void* buffer, size_t bufferSize); + + private: + friend class Client; + Request(u32 request_id, const std::string& method, size_t max_params); + + ArticBaseCommon::RequestPacket request_packet{}; + std::vector parameters; + std::string method_name; + size_t max_param_count; + std::vector> pending_big_buffers; + }; + + Client(const std::string& _address, u16 _port) : address(_address), port(_port) { + SocketManager::EnableSockets(); + } + ~Client(); + + bool Connect(); + bool connected = false; + + size_t GetServerRequestMaxSize() { + return max_server_work_ram; + } + + Request NewRequest(const std::string& method) { + return Request(GetNextRequestID(), method, max_parameter_count); + } + + void Stop() { + StopImpl(false); + } + + void SetCommunicationErrorCallback(const std::function& callback) { + communication_error_callback = callback; + } + + void SetArticReportTrafficCallback(const std::function& callback) { + report_traffic_callback = callback; + } + + void ReportArticEvent(u64 event) { + if (report_artic_event_callback) { + report_artic_event_callback(event); + } + } + void SetReportArticEventCallback(const std::function& callback) { + report_artic_event_callback = callback; + } + +private: + static constexpr const int SERVER_VERSION = 0; + + std::string address; + u16 port; + + SocketHolder main_socket = -1; + std::atomic currRequestID; + u32 GetNextRequestID() { + return currRequestID++; + } + + void SignalCommunicationError(); + std::function communication_error_callback; + + std::function report_artic_event_callback; + + size_t max_server_work_ram = 0; + size_t max_parameter_count = 0; + std::mutex send_mutex; + + std::atomic stopped = false; + + std::atomic> last_sent_request; + std::thread ping_thread; + std::condition_variable ping_cv; + std::mutex ping_cv_mutex; + bool ping_run = true; + + void StopImpl(bool from_error); + + void PingFunction(); + + static bool ConnectWithTimeout(SocketHolder sockFD, void* server_addr, size_t server_addr_len, + int timeout_seconds); + static bool SetNonBlock(SocketHolder sockFD, bool blocking); + bool Read(SocketHolder sockFD, void* buffer, size_t size, + const std::chrono::nanoseconds& timeout = std::chrono::nanoseconds(0)); + bool Write(SocketHolder sockFD, const void* buffer, size_t size, + const std::chrono::nanoseconds& timeout = std::chrono::nanoseconds(0)); + std::function report_traffic_callback; + + std::optional SendRequestPacket( + const ArticBaseCommon::RequestPacket& req, bool expect_response, + const std::vector& params, + const std::chrono::nanoseconds& read_timeout = std::chrono::nanoseconds(0)); + std::optional SendSimpleRequest(const std::string& method); + + class Handler { + public: + Handler(Client& _client, u32 _addr, u16 _port, int _id); + ~Handler() { + delete thread; + } + void RunLoop(); + + int id = 0; + bool should_run = true; + SocketHolder handler_socket = -1; + std::thread* thread = nullptr; + + private: + Client& client; + u32 addr; + u16 port; + }; + + class PendingResponse; + +public: + class Response { + public: + Response() {} + Response(Response& other) + : articResult(other.articResult), methodResult(other.methodResult), + resp_data_size(other.resp_data_size) { + if (resp_data_size) { + resp_data_buffer = reinterpret_cast(operator new(resp_data_size)); + std::memcpy(resp_data_buffer, other.resp_data_buffer, resp_data_size); + } + } + Response(Response&& other) noexcept + : articResult(other.articResult), methodResult(other.methodResult), + resp_data_buffer(std::exchange(other.resp_data_buffer, nullptr)), + resp_data_size(other.resp_data_size) {} + + Response& operator=(Response& other) { + articResult = other.articResult; + methodResult = other.methodResult; + resp_data_size = other.resp_data_size; + if (resp_data_size) { + resp_data_buffer = reinterpret_cast(operator new(resp_data_size)); + std::memcpy(resp_data_buffer, other.resp_data_buffer, resp_data_size); + } + return *this; + } + + Response& operator=(Response&& other) noexcept { + articResult = other.articResult; + methodResult = other.methodResult; + resp_data_size = other.resp_data_size; + resp_data_buffer = std::exchange(other.resp_data_buffer, nullptr); + return *this; + } + + ~Response() { + if (resp_data_buffer) { + operator delete(resp_data_buffer); + } + } + + bool Succeeded() const { + return articResult == ArticBaseCommon::ResponseMethod::ArticResult::SUCCESS; + } + + int GetMethodResult() const { + return methodResult; + } + + std::optional> GetResponseBuffer(u32 buffer_id) const; + + std::optional GetResponseS32(u32 buffer_id) const { + auto buf = GetResponseBuffer(buffer_id); + if (!buf.has_value() || buf->second != sizeof(s32)) { + return std::nullopt; + } + return *reinterpret_cast(buf->first); + } + + std::optional GetResponseS64(u32 buffer_id) const { + auto buf = GetResponseBuffer(buffer_id); + if (!buf.has_value() || buf->second != sizeof(s64)) { + return std::nullopt; + } + return *reinterpret_cast(buf->first); + } + + std::optional GetResponseU64(u32 buffer_id) const { + auto buf = GetResponseBuffer(buffer_id); + if (!buf.has_value() || buf->second != sizeof(u64)) { + return std::nullopt; + } + return *reinterpret_cast(buf->first); + } + + private: + friend class Client; + friend class Client::Handler; + friend class PendingResponse; + + // Start in error state in case the request is not fullfilled properly. + ArticBaseCommon::ResponseMethod::ArticResult articResult = + ArticBaseCommon::ResponseMethod::ArticResult::METHOD_ERROR; + union { + ArticBaseCommon::MethodState methodState = + ArticBaseCommon::MethodState::INTERNAL_METHOD_ERROR; + int methodResult; + }; + char* resp_data_buffer{}; + size_t resp_data_size = 0; + }; + + std::optional Send(Request& request); + +private: + class PendingResponse { + public: + bool is_done = false; + + private: + friend class Client; + friend class Client::Handler; + PendingResponse(const Request& req) : request(req) {} + std::condition_variable cv; + std::mutex cv_mutex; + + const Request& request; + + Response response{}; + }; + + std::mutex recv_map_mutex; + std::map pending_responses; + + std::vector handlers; + std::atomic running_handlers; + void OnAllHandlersFinished(); +}; +} // namespace Network::ArticBase diff --git a/src/network/artic_base/artic_base_common.h b/src/network/artic_base/artic_base_common.h new file mode 100644 index 000000000..671da6e99 --- /dev/null +++ b/src/network/artic_base/artic_base_common.h @@ -0,0 +1,92 @@ +// Copyright 2024 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once +#include +#include "common/common_types.h" + +namespace Network { +namespace ArticBaseCommon { +enum class MethodState : int { + PARSING_INPUT = 0, + PARAMETER_TYPE_MISMATCH = 1, + PARAMETER_COUNT_MISMATCH = 2, + BIG_BUFFER_READ_FAIL = 3, + BIG_BUFFER_WRITE_FAIL = 4, + OUT_OF_MEMORY = 5, + + GENERATING_OUTPUT = 6, + UNEXPECTED_PARSING_INPUT = 7, + OUT_OF_MEMORY_OUTPUT = 8, + + INTERNAL_METHOD_ERROR = 9, + FINISHED = 10, +}; +enum class RequestParameterType : u16 { + IN_INTEGER_8 = 0, + IN_INTEGER_16 = 1, + IN_INTEGER_32 = 2, + IN_INTEGER_64 = 3, + IN_SMALL_BUFFER = 4, + IN_BIG_BUFFER = 5, +}; +struct RequestParameter { + RequestParameterType type{}; + union { + u16 parameterSize{}; + u16 bigBufferID; + }; + + char data[0x1C]{}; +}; +struct RequestPacket { + u32 requestID{}; + std::array method{}; + u32 parameterCount{}; +}; +static_assert(sizeof(RequestPacket) == 0x28); + +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable : 4200) +#endif +struct Buffer { + u32 bufferID; + u32 bufferSize; + + char data[]; +}; +#ifdef _MSC_VER +#pragma warning(pop) +#endif + +struct ResponseMethod { + enum class ArticResult : u32 { + SUCCESS = 0, + METHOD_NOT_FOUND = 1, + METHOD_ERROR = 2, + PROVIDE_INPUT = 3, + }; + ArticResult articResult{}; + union { + int methodResult{}; + int provideInputBufferID; + }; + int bufferSize{}; + u8 padding[0x10]{}; +}; + +struct DataPacket { + DataPacket() {} + u32 requestID{}; + + union { + char dataRaw[0x1C]{}; + ResponseMethod resp; + }; +}; + +static_assert(sizeof(DataPacket) == 0x20); +}; // namespace ArticBaseCommon +} // namespace Network diff --git a/src/network/socket_manager.cpp b/src/network/socket_manager.cpp new file mode 100644 index 000000000..0157e3e84 --- /dev/null +++ b/src/network/socket_manager.cpp @@ -0,0 +1,31 @@ +// Copyright 2024 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#ifdef _WIN32 +#include +#include +#endif + +#include "socket_manager.h" + +namespace Network { +std::atomic SocketManager::count = 0; + +void SocketManager::EnableSockets() { + if (count++ == 0) { +#ifdef _WIN32 + WSADATA data; + WSAStartup(MAKEWORD(2, 2), &data); +#endif + } +} + +void SocketManager::DisableSockets() { + if (--count == 0) { +#ifdef _WIN32 + WSACleanup(); +#endif + } +} +} // namespace Network \ No newline at end of file diff --git a/src/network/socket_manager.h b/src/network/socket_manager.h new file mode 100644 index 000000000..f54aef2c4 --- /dev/null +++ b/src/network/socket_manager.h @@ -0,0 +1,19 @@ +// Copyright 2024 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once +#include "atomic" +#include "common/common_types.h" + +namespace Network { +class SocketManager { +public: + static void EnableSockets(); + static void DisableSockets(); + +private: + SocketManager(); + static std::atomic count; +}; +} // namespace Network \ No newline at end of file