From b5ff2440f6f71fc931d8659c62a05d9bd761ffd6 Mon Sep 17 00:00:00 2001 From: Rokkubro Date: Thu, 13 Apr 2023 23:33:21 +1000 Subject: [PATCH] Squash commits during rebase again: Final things from review comments Clang format Shorten property code (similar to #6883) and move to utils Move all additional helper functions into a 'utils' file. Simplify some things as requested in review Minor code changes from review before rebase fix misusing std span Fix leftovers from rebase, and null-terminator problem with download string-view Change downloadbossdatafromurl to take in string_views, make getting the list of files more dynamic Fix error in linux builds and cleanup Squash commits during rebase: Some changes as per review and cleanup More changes as per review Changes as per review Futures cannot be copied, remove stubbed warning on implemented calls, remove unneeded task_status simulation, simplify getting task_status and duration Implement downloading tasks in background Very final changes from review; and use common for converting strings FInal changes for review Attempt to fix codecvt error Use references when any_casting Update boost submodule to use master again, refactor how properties work, other minor changes per review Fix operator overload error on linux/mingw Make some changes as requested by review; change boost submodule url temporarily to use boost.url Fix for android build Fixes android builds when web services are enabled, like in #6555 Avoid crashes when urls are invalid clang-format Return error status properly on task fail Fix implementation of gettaskstate, gettaskstatus and gettaskservicestatus Fix mingw build error Add support for reading tasks from boss save data databases. clang-format Implement storing task properties Fix missing includes and add references in loops Change task_id_list to map, initial implementation of task properties, minor refactor Remove the dependency on the newer behavior of std erase to fix android building Fix compilation on android and other platforms when web services are not enabled Fix clang-format errors Add support for downloading and decrypting spotpass data directly from nintendo servers Fix windows implicit conversion error again Fix comment Fix filter in NsDataIdList; Finish GetNsDataHeaderInfo; Implement basic support for registering tasks and checking if they exist TODO actually read and write from boss savedata dbs Add boss extdata to archive.h so the lle boss module can function properly Implement ReadNsData and partially implement GetNsDataHeaderInfo and GetNsDataLastUpdate; MK7 now reads spotpass data and successfully boots! Made requested changes; added filtering; removed readnsdata implementation Add partial implementations of GetNsDataIdList(1/2/3) and ReadNsData Add zeroed array of nsdataid entries, run clang-format Check the spotpass extdata directory to determine number of ns output entries Check for PLvPWAA Only set the number of output entries in GetNsDataIdList1 to 1 if PLvPWAA is detected. Fix plvpwaa dlc error Return 1 for the number of output entries in the GetNsDataIdList1 stub. This fixes the extra content for Professor Layton vs Phoenix Wright Ace Attorney as the game expects the boss extdata to not be empty. Might break other games if they attempt to do anything with the ns data. (although the readnsdata and deletensdata methods are both still stubbed) --- src/core/CMakeLists.txt | 2 + src/core/file_sys/archive_backend.h | 1 + src/core/file_sys/archive_extsavedata.cpp | 11 +- src/core/file_sys/archive_extsavedata.h | 2 + src/core/hle/service/boss/boss.cpp | 668 ++++++++++++++++--- src/core/hle/service/boss/boss.h | 10 +- src/core/hle/service/boss/spotpass_utils.cpp | 556 +++++++++++++++ src/core/hle/service/boss/spotpass_utils.h | 212 ++++++ src/core/hle/service/fs/archive.cpp | 4 + src/core/hle/service/fs/archive.h | 1 + 10 files changed, 1358 insertions(+), 109 deletions(-) create mode 100644 src/core/hle/service/boss/spotpass_utils.cpp create mode 100644 src/core/hle/service/boss/spotpass_utils.h diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 15c532c87..0906adc0f 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -236,6 +236,8 @@ add_library(citra_core STATIC hle/service/boss/boss_p.h hle/service/boss/boss_u.cpp hle/service/boss/boss_u.h + hle/service/boss/spotpass_utils.cpp + hle/service/boss/spotpass_utils.h hle/service/cam/cam.cpp hle/service/cam/cam.h hle/service/cam/cam_c.cpp diff --git a/src/core/file_sys/archive_backend.h b/src/core/file_sys/archive_backend.h index 0fada0f23..8dc0f14d8 100644 --- a/src/core/file_sys/archive_backend.h +++ b/src/core/file_sys/archive_backend.h @@ -41,6 +41,7 @@ class Path { public: Path() : type(LowPathType::Invalid) {} Path(const char* path) : type(LowPathType::Char), string(path) {} + Path(std::string path) : type(LowPathType::Char), string(std::move(path)) {} Path(std::vector binary_data) : type(LowPathType::Binary), binary(std::move(binary_data)) {} template Path(const std::array& binary_data) diff --git a/src/core/file_sys/archive_extsavedata.cpp b/src/core/file_sys/archive_extsavedata.cpp index 7ab54d39c..643821987 100644 --- a/src/core/file_sys/archive_extsavedata.cpp +++ b/src/core/file_sys/archive_extsavedata.cpp @@ -217,7 +217,13 @@ Path ConstructExtDataBinaryPath(u32 media_type, u32 high, u32 low) { ArchiveFactory_ExtSaveData::ArchiveFactory_ExtSaveData(const std::string& mount_location, bool shared) - : shared(shared), mount_point(GetExtDataContainerPath(mount_location, shared)) { + : shared(shared), boss(false), mount_point(GetExtDataContainerPath(mount_location, shared)) { + LOG_DEBUG(Service_FS, "Directory {} set as base for ExtSaveData.", mount_point); +} + +ArchiveFactory_ExtSaveData::ArchiveFactory_ExtSaveData(const std::string& mount_location, + bool shared, bool boss) + : shared(shared), boss(boss), mount_point(GetExtDataContainerPath(mount_location, shared)) { LOG_DEBUG(Service_FS, "Directory {} set as base for ExtSaveData.", mount_point); } @@ -242,7 +248,8 @@ Path ArchiveFactory_ExtSaveData::GetCorrectedPath(const Path& path) { ResultVal> ArchiveFactory_ExtSaveData::Open(const Path& path, u64 program_id) { - std::string fullpath = GetExtSaveDataPath(mount_point, GetCorrectedPath(path)) + "user/"; + std::string fullpath = + GetExtSaveDataPath(mount_point, GetCorrectedPath(path)) + (boss ? "boss/" : "user/"); 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. diff --git a/src/core/file_sys/archive_extsavedata.h b/src/core/file_sys/archive_extsavedata.h index dec84ade9..2c6b7c8d5 100644 --- a/src/core/file_sys/archive_extsavedata.h +++ b/src/core/file_sys/archive_extsavedata.h @@ -19,6 +19,7 @@ namespace FileSys { class ArchiveFactory_ExtSaveData final : public ArchiveFactory { public: ArchiveFactory_ExtSaveData(const std::string& mount_point, bool shared); + ArchiveFactory_ExtSaveData(const std::string& mount_point, bool shared, bool boss); std::string GetName() const override { return "ExtSaveData"; @@ -44,6 +45,7 @@ public: private: bool shared; ///< Whether this archive represents an ExtSaveData archive or a SharedExtSaveData /// archive + bool boss; ///< Whether this archive is a spotpass archive or not /** * This holds the full directory path for this archive, it is only set after a successful call diff --git a/src/core/hle/service/boss/boss.cpp b/src/core/hle/service/boss/boss.cpp index ea29786cd..64d1af59a 100644 --- a/src/core/hle/service/boss/boss.cpp +++ b/src/core/hle/service/boss/boss.cpp @@ -2,25 +2,152 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. -#include "common/logging/log.h" +#include +#include +#include +#include +#include "common/string_util.h" #include "core/core.h" +#include "core/file_sys/archive_extsavedata.h" +#include "core/file_sys/errors.h" +#include "core/file_sys/file_backend.h" #include "core/hle/ipc_helpers.h" -#include "core/hle/result.h" #include "core/hle/service/boss/boss.h" #include "core/hle/service/boss/boss_p.h" #include "core/hle/service/boss/boss_u.h" +#include "core/hw/aes/key.h" namespace Service::BOSS { void Module::Interface::InitializeSession(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); - const u64 programID = rp.Pop(); + const u64 provided_program_id = rp.Pop(); rp.PopPID(); + util.cur_props = BossTaskProperties(); + // I'm putting this here for now because I don't know where else to put it; + // the BOSS service saves data in its BOSS_A(Archive? A list of program ids and some + // properties that are keyed on program), BOSS_SS (Saved Strings? Includes the url and the + // other string properties, and also some other properties?, keyed on task_id) and BOSS_SV + // (Saved Values? Includes task id and most properties, keyed on task_id) databases in the + // following format: A four byte header (always 00 80 34 12?) followed by any number of + // 0x800(BOSS_A) and 0xC00(BOSS_SS and BOSS_SV) entries. + + if (provided_program_id != 0) { + util.program_id = provided_program_id; + } + + LOG_DEBUG(Service_BOSS, "called, program_id={:#018X}", util.program_id); IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + + // We can always return success from this as the boss dbs are not properly used atm rb.Push(RESULT_SUCCESS); - LOG_WARNING(Service_BOSS, "(STUBBED) programID={:#018X}", programID); + const std::string& nand_directory = FileUtil::GetUserPath(FileUtil::UserPath::NANDDir); + FileSys::ArchiveFactory_SystemSaveData systemsavedata_factory(nand_directory); + + // Open the SystemSaveData archive 0x00010034 + FileSys::Path archive_path(BOSS_SYSTEM_SAVEDATA_ID); + auto archive_result = systemsavedata_factory.Open(archive_path, 0); + + std::unique_ptr boss_system_save_data_archive; + + // If the archive didn't exist, create the files inside + if (archive_result.Code() == FileSys::ERROR_NOT_FOUND) { + // Format the archive to create the directories + systemsavedata_factory.Format(archive_path, FileSys::ArchiveFormatInfo(), 0); + + // Open it again to get a valid archive now that the folder exists + auto create_archive_result = systemsavedata_factory.Open(archive_path, 0); + if (create_archive_result.Succeeded()) { + boss_system_save_data_archive = std::move(create_archive_result).Unwrap(); + } else { + LOG_ERROR(Service_BOSS, "Could not open boss savedata"); + return; + } + } else if (archive_result.Succeeded()) { + boss_system_save_data_archive = std::move(archive_result).Unwrap(); + } else { + LOG_ERROR(Service_BOSS, "Could not open boss savedata"); + return; + } + FileSys::Path boss_a_path("/BOSS_A.db"); + FileSys::Mode open_mode = {}; + open_mode.read_flag.Assign(1); + auto boss_a_result = boss_system_save_data_archive->OpenFile(boss_a_path, open_mode); + + // Read the file if it already exists + if (boss_a_result.Succeeded()) { + auto boss_a = std::move(boss_a_result).Unwrap(); + const u64 boss_a_size = boss_a->GetSize(); + // Check the file has a valid size (multiple of the number of entries, plus header) + if (!(boss_a_size > BOSS_SAVE_HEADER_SIZE && + ((boss_a_size - BOSS_SAVE_HEADER_SIZE) % BOSS_A_ENTRY_SIZE) == 0)) { + } else { + u64 num_entries = (boss_a_size - BOSS_SAVE_HEADER_SIZE) / BOSS_A_ENTRY_SIZE; + // Print the program ids of the entries + for (u64 i = 0; i < num_entries; i++) { + u64 entry_offset = i * BOSS_A_ENTRY_SIZE + BOSS_SAVE_HEADER_SIZE; + u64 prog_id; + boss_a->Read(entry_offset, sizeof(prog_id), reinterpret_cast(&prog_id)); + LOG_DEBUG(Service_BOSS, "Id in entry {} is {:#018X}", i, prog_id); + } + } + } + + FileSys::Path boss_sv_path("/BOSS_SV.db"); + auto boss_sv_result = boss_system_save_data_archive->OpenFile(boss_sv_path, open_mode); + + FileSys::Path boss_ss_path("/BOSS_SS.db"); + auto boss_ss_result = boss_system_save_data_archive->OpenFile(boss_ss_path, open_mode); + + // Read the files if they already exist + if (boss_sv_result.Succeeded() && boss_ss_result.Succeeded()) { + auto boss_sv = std::move(boss_sv_result).Unwrap(); + auto boss_ss = std::move(boss_ss_result).Unwrap(); + + // Check the file length is valid. Both files should have the same number of entries + if (!(boss_sv->GetSize() > BOSS_SAVE_HEADER_SIZE && + ((boss_sv->GetSize() - BOSS_SAVE_HEADER_SIZE) % BOSS_S_ENTRY_SIZE) == 0 && + boss_sv->GetSize() == boss_ss->GetSize())) { + LOG_WARNING(Service_BOSS, "Boss dbs have incorrect size"); + return; + } else { + u64 num_entries = (boss_sv->GetSize() - BOSS_SAVE_HEADER_SIZE) / BOSS_S_ENTRY_SIZE; + for (u64 i = 0; i < num_entries; i++) { + u64 entry_offset = i * BOSS_S_ENTRY_SIZE + BOSS_SAVE_HEADER_SIZE; + + // Print the program id and task id to debug + u64 prog_id; + boss_sv->Read(entry_offset + BOSS_S_PROG_ID_OFFSET, sizeof(prog_id), + reinterpret_cast(&prog_id)); + LOG_DEBUG(Service_BOSS, "Id sv in entry {} is {:#018X}", i, prog_id); + + std::string task_id(TASK_ID_SIZE, 0); + boss_sv->Read(entry_offset + BOSS_S_TASK_ID_OFFSET, TASK_ID_SIZE, + reinterpret_cast(task_id.data())); + LOG_DEBUG(Service_BOSS, "Task id in entry {} is {}", i, task_id); + + std::vector url(URL_SIZE); + boss_ss->Read(entry_offset + BOSS_S_URL_OFFSET, URL_SIZE, url.data()); + LOG_DEBUG(Service_BOSS, "Url for task {} is {}", task_id, + std::string_view(reinterpret_cast(url.data()), url.size())); + + // If the task is for the current program, store the download url for the session. + // In the future we could store more properties, including from the sv db. + if (prog_id == util.program_id) { + LOG_DEBUG(Service_BOSS, "Storing download url"); + util.cur_props.props[PropertyID::Url] = url; + if (util.task_id_list.contains(task_id)) { + LOG_WARNING(Service_BOSS, "Task id already in list, will be replaced"); + util.task_id_list.erase(task_id); + } + util.task_id_list.emplace(task_id, std::move(util.cur_props)); + util.cur_props = BossTaskProperties(); + } + } + } + } } void Module::Interface::SetStorageInfo(Kernel::HLERequestContext& ctx) { @@ -131,12 +258,22 @@ void Module::Interface::RegisterTask(Kernel::HLERequestContext& ctx) { const u8 unk_param3 = rp.Pop(); auto& buffer = rp.PopMappedBuffer(); + std::string task_id(size, 0); + buffer.Read(task_id.data(), 0, size); + if (util.task_id_list.contains(task_id)) { + LOG_WARNING(Service_BOSS, "Task id already in list, will be replaced"); + util.task_id_list.erase(task_id); + } + util.task_id_list.emplace(task_id, std::move(util.cur_props)); + util.cur_props = BossTaskProperties(); + LOG_DEBUG(Service_BOSS, "Read task id {}", task_id); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); rb.Push(RESULT_SUCCESS); rb.PushMappedBuffer(buffer); - LOG_WARNING(Service_BOSS, "(STUBBED) size={:#010X}, unk_param2={:#04X}, unk_param3={:#04X}", - size, unk_param2, unk_param3); + LOG_DEBUG(Service_BOSS, "called, size={:#010X}, unk_param2={:#04X}, unk_param3={:#04X}", size, + unk_param2, unk_param3); } void Module::Interface::UnregisterTask(Kernel::HLERequestContext& ctx) { @@ -145,11 +282,31 @@ void Module::Interface::UnregisterTask(Kernel::HLERequestContext& ctx) { const u8 unk_param2 = rp.Pop(); auto& buffer = rp.PopMappedBuffer(); + ResultCode result = RESULT_FAILED; + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); - rb.Push(RESULT_SUCCESS); + + if (size > TASK_ID_SIZE) { + LOG_WARNING(Service_BOSS, "Task Id cannot be longer than 8"); + rb.Push(result); + rb.PushMappedBuffer(buffer); + return; + } else { + std::string task_id(size, 0); + buffer.Read(task_id.data(), 0, size); + LOG_DEBUG(Service_BOSS, "Read task id {}", task_id); + if (util.task_id_list.erase(task_id) == 0) { + LOG_WARNING(Service_BOSS, "Task Id not in list"); + } else { + LOG_DEBUG(Service_BOSS, "Task Id erased"); + result = RESULT_SUCCESS; + } + } + + rb.Push(result); rb.PushMappedBuffer(buffer); - LOG_WARNING(Service_BOSS, "(STUBBED) size={:#010X}, unk_param2={:#04X}", size, unk_param2); + LOG_DEBUG(Service_BOSS, "called, size={:#010X}, unk_param2={:#04X}", size, unk_param2); } void Module::Interface::ReconfigureTask(Kernel::HLERequestContext& ctx) { @@ -168,10 +325,37 @@ void Module::Interface::ReconfigureTask(Kernel::HLERequestContext& ctx) { void Module::Interface::GetTaskIdList(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); + const u16 num_task_ids = static_cast(util.task_id_list.size()); + util.cur_props.props[PropertyID::TotalTasks] = num_task_ids; + LOG_DEBUG(Service_BOSS, "Prepared total_tasks = {}", num_task_ids); + + u16 num_returned_task_ids = 0; + std::vector> task_ids(TASKIDLIST_SIZE / TASK_ID_SIZE); + + for (const auto& iter : util.task_id_list) { + const std::string_view cur_task_id = iter.first; + if (cur_task_id.size() > TASK_ID_SIZE || + num_returned_task_ids >= TASKIDLIST_SIZE / TASK_ID_SIZE) { + LOG_WARNING(Service_BOSS, "task id {} too long or too many task ids", cur_task_id); + } else { + std::memcpy(task_ids[num_returned_task_ids].data(), cur_task_id.data(), TASK_ID_SIZE); + num_returned_task_ids++; + LOG_TRACE(Service_BOSS, "wrote task id {}", cur_task_id); + } + } + + const auto task_list_prop = + std::get_if>(&util.cur_props.props[PropertyID::TaskIdList]); + if (task_list_prop && task_list_prop->size() == TASKIDLIST_SIZE) { + + std::memcpy(task_list_prop->data(), task_ids.data(), TASKIDLIST_SIZE); + LOG_DEBUG(Service_BOSS, "wrote out {} task ids", num_returned_task_ids); + } + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(RESULT_SUCCESS); - LOG_WARNING(Service_BOSS, "(STUBBED) called"); + LOG_DEBUG(Service_BOSS, "called"); } void Module::Interface::GetStepIdList(Kernel::HLERequestContext& ctx) { @@ -194,16 +378,18 @@ void Module::Interface::GetNsDataIdList(Kernel::HLERequestContext& ctx) { const u32 start_ns_data_id = rp.Pop(); auto& buffer = rp.PopMappedBuffer(); + const u16 entries_count = util.GetOutputEntries(filter, max_entries, buffer); + IPC::RequestBuilder rb = rp.MakeBuilder(3, 2); rb.Push(RESULT_SUCCESS); - rb.Push(0); /// Actual number of output entries - rb.Push(0); /// Last word-index copied to output in the internal NsDataId list. + rb.Push(entries_count); /// Actual number of output entries + rb.Push(0); /// Last word-index copied to output in the internal NsDataId list. rb.PushMappedBuffer(buffer); - LOG_WARNING(Service_BOSS, - "(STUBBED) filter={:#010X}, max_entries={:#010X}, " - "word_index_start={:#06X}, start_ns_data_id={:#010X}", - filter, max_entries, word_index_start, start_ns_data_id); + LOG_DEBUG(Service_BOSS, + "filter={:#010X}, max_entries={:#010X}, " + "word_index_start={:#06X}, start_ns_data_id={:#010X}", + filter, max_entries, word_index_start, start_ns_data_id); } void Module::Interface::GetNsDataIdList1(Kernel::HLERequestContext& ctx) { @@ -214,16 +400,18 @@ void Module::Interface::GetNsDataIdList1(Kernel::HLERequestContext& ctx) { const u32 start_ns_data_id = rp.Pop(); auto& buffer = rp.PopMappedBuffer(); + const u16 entries_count = util.GetOutputEntries(filter, max_entries, buffer); + IPC::RequestBuilder rb = rp.MakeBuilder(3, 2); rb.Push(RESULT_SUCCESS); - rb.Push(0); /// Actual number of output entries - rb.Push(0); /// Last word-index copied to output in the internal NsDataId list. + rb.Push(entries_count); /// Actual number of output entries + rb.Push(0); /// Last word-index copied to output in the internal NsDataId list. rb.PushMappedBuffer(buffer); - LOG_WARNING(Service_BOSS, - "(STUBBED) filter={:#010X}, max_entries={:#010X}, " - "word_index_start={:#06X}, start_ns_data_id={:#010X}", - filter, max_entries, word_index_start, start_ns_data_id); + LOG_DEBUG(Service_BOSS, + "filter={:#010X}, max_entries={:#010X}, " + "word_index_start={:#06X}, start_ns_data_id={:#010X}", + filter, max_entries, word_index_start, start_ns_data_id); } void Module::Interface::GetNsDataIdList2(Kernel::HLERequestContext& ctx) { @@ -234,16 +422,18 @@ void Module::Interface::GetNsDataIdList2(Kernel::HLERequestContext& ctx) { const u32 start_ns_data_id = rp.Pop(); auto& buffer = rp.PopMappedBuffer(); + const u16 entries_count = util.GetOutputEntries(filter, max_entries, buffer); + IPC::RequestBuilder rb = rp.MakeBuilder(3, 2); rb.Push(RESULT_SUCCESS); - rb.Push(0); /// Actual number of output entries - rb.Push(0); /// Last word-index copied to output in the internal NsDataId list. + rb.Push(entries_count); /// Actual number of output entries + rb.Push(0); /// Last word-index copied to output in the internal NsDataId list. rb.PushMappedBuffer(buffer); - LOG_WARNING(Service_BOSS, - "(STUBBED) filter={:#010X}, max_entries={:#010X}, " - "word_index_start={:#06X}, start_ns_data_id={:#010X}", - filter, max_entries, word_index_start, start_ns_data_id); + LOG_DEBUG(Service_BOSS, + "filter={:#010X}, max_entries={:#010X}, " + "word_index_start={:#06X}, start_ns_data_id={:#010X}", + filter, max_entries, word_index_start, start_ns_data_id); } void Module::Interface::GetNsDataIdList3(Kernel::HLERequestContext& ctx) { @@ -254,29 +444,38 @@ void Module::Interface::GetNsDataIdList3(Kernel::HLERequestContext& ctx) { const u32 start_ns_data_id = rp.Pop(); auto& buffer = rp.PopMappedBuffer(); + const u16 entries_count = util.GetOutputEntries(filter, max_entries, buffer); + IPC::RequestBuilder rb = rp.MakeBuilder(3, 2); rb.Push(RESULT_SUCCESS); - rb.Push(0); /// Actual number of output entries - rb.Push(0); /// Last word-index copied to output in the internal NsDataId list. + rb.Push(entries_count); /// Actual number of output entries + rb.Push(0); /// Last word-index copied to output in the internal NsDataId list. rb.PushMappedBuffer(buffer); - LOG_WARNING(Service_BOSS, - "(STUBBED) filter={:#010X}, max_entries={:#010X}, " - "word_index_start={:#06X}, start_ns_data_id={:#010X}", - filter, max_entries, word_index_start, start_ns_data_id); + LOG_DEBUG(Service_BOSS, + "filter={:#010X}, max_entries={:#010X}, " + "word_index_start={:#06X}, start_ns_data_id={:#010X}", + filter, max_entries, word_index_start, start_ns_data_id); } void Module::Interface::SendProperty(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); - const u16 property_id = rp.Pop(); + const PropertyID property_id = static_cast(rp.Pop()); const u32 size = rp.Pop(); auto& buffer = rp.PopMappedBuffer(); - IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); - rb.Push(RESULT_SUCCESS); - rb.PushMappedBuffer(buffer); + LOG_DEBUG(Service_BOSS, "called, property_id={:#06X}, size={:#010X}", property_id, size); - LOG_WARNING(Service_BOSS, "(STUBBED) property_id={:#06X}, size={:#010X}", property_id, size); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); + + ResultCode result = RESULT_FAILED; + + if (util.ReadWriteProperties(false, property_id, size, buffer)) { + result = RESULT_SUCCESS; + } + + rb.Push(result); + rb.PushMappedBuffer(buffer); } void Module::Interface::SendPropertyHandle(Kernel::HLERequestContext& ctx) { @@ -292,16 +491,22 @@ void Module::Interface::SendPropertyHandle(Kernel::HLERequestContext& ctx) { void Module::Interface::ReceiveProperty(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); - const u16 property_id = rp.Pop(); + const PropertyID property_id = static_cast(rp.Pop()); const u32 size = rp.Pop(); auto& buffer = rp.PopMappedBuffer(); + LOG_DEBUG(Service_BOSS, "called, property_id={:#06X}, size={:#010X}", property_id, size); IPC::RequestBuilder rb = rp.MakeBuilder(2, 2); - rb.Push(RESULT_SUCCESS); - rb.Push(size); /// Should be actual read size - rb.PushMappedBuffer(buffer); - LOG_WARNING(Service_BOSS, "(STUBBED) property_id={:#06X}, size={:#010X}", property_id, size); + ResultCode result = RESULT_FAILED; + + if (util.ReadWriteProperties(true, property_id, size, buffer)) { + result = RESULT_SUCCESS; + } + + rb.Push(result); + rb.Push(size); // The size of the property per id, not how much data + rb.PushMappedBuffer(buffer); } void Module::Interface::UpdateTaskInterval(Kernel::HLERequestContext& ctx) { @@ -323,6 +528,14 @@ void Module::Interface::UpdateTaskCount(Kernel::HLERequestContext& ctx) { const u32 unk_param2 = rp.Pop(); auto& buffer = rp.PopMappedBuffer(); + if (size > TASK_ID_SIZE) { + LOG_WARNING(Service_BOSS, "Task Id cannot be longer than 8"); + } else { + std::string task_id(size, 0); + buffer.Read(task_id.data(), 0, size); + LOG_DEBUG(Service_BOSS, "Read task id {}", task_id); + } + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); rb.Push(RESULT_SUCCESS); rb.PushMappedBuffer(buffer); @@ -348,6 +561,14 @@ void Module::Interface::GetTaskCount(Kernel::HLERequestContext& ctx) { const u32 size = rp.Pop(); auto& buffer = rp.PopMappedBuffer(); + if (size > TASK_ID_SIZE) { + LOG_WARNING(Service_BOSS, "Task Id cannot be longer than 8"); + } else { + std::string task_id(size, 0); + buffer.Read(task_id.data(), 0, size); + LOG_DEBUG(Service_BOSS, "Read task id {}", task_id); + } + IPC::RequestBuilder rb = rp.MakeBuilder(2, 2); rb.Push(RESULT_SUCCESS); rb.Push(0); // stub 0 ( 32bit value) @@ -361,9 +582,13 @@ void Module::Interface::GetTaskServiceStatus(Kernel::HLERequestContext& ctx) { const u32 size = rp.Pop(); auto& buffer = rp.PopMappedBuffer(); + // Not sure what this is but it's not the task status. Maybe it's the status of the service + // after running the task? + u8 task_service_status = 1; + IPC::RequestBuilder rb = rp.MakeBuilder(2, 2); rb.Push(RESULT_SUCCESS); - rb.Push(0); // stub 0 ( 8bit value) + rb.Push(task_service_status); // stub 1 ( 8bit value) this is not taskstatus rb.PushMappedBuffer(buffer); LOG_WARNING(Service_BOSS, "(STUBBED) size={:#010X}", size); @@ -374,23 +599,46 @@ void Module::Interface::StartTask(Kernel::HLERequestContext& ctx) { const u32 size = rp.Pop(); auto& buffer = rp.PopMappedBuffer(); + if (size > TASK_ID_SIZE) { + LOG_WARNING(Service_BOSS, "Task Id cannot be longer than 8"); + } else { + std::string task_id(size, 0); + buffer.Read(task_id.data(), 0, size); + if (!util.task_id_list.contains(task_id)) { + LOG_WARNING(Service_BOSS, "Task Id {} not found", task_id); + } else { + util.task_id_list[task_id].times_checked = 0; + if (const auto* url_prop = util.task_id_list[task_id].props.contains(PropertyID::Url) + ? std::get_if>( + &util.task_id_list[task_id].props[PropertyID::Url]) + : nullptr; + url_prop && url_prop->size() == URL_SIZE) { + const char* url_pointer = reinterpret_cast(url_prop->data()); + std::string_view url(url_pointer, strnlen(url_pointer, URL_SIZE)); + std::string_view file_name(util.task_id_list.find(task_id)->first.c_str(), + strnlen(task_id.c_str(), TASK_ID_SIZE)); + util.task_id_list[task_id].download_task = + std::async(std::launch::async, DownloadBossDataFromURL, url, file_name, + util.program_id, util.extdata_id); + } else { + LOG_ERROR(Service_BOSS, "URL property is invalid"); + } + } + LOG_DEBUG(Service_BOSS, "Read task id {}", task_id); + } + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); rb.Push(RESULT_SUCCESS); rb.PushMappedBuffer(buffer); - LOG_WARNING(Service_BOSS, "(STUBBED) size={:#010X}", size); + LOG_DEBUG(Service_BOSS, "size={:#010X}", size); } void Module::Interface::StartTaskImmediate(Kernel::HLERequestContext& ctx) { - IPC::RequestParser rp(ctx); - const u32 size = rp.Pop(); - auto& buffer = rp.PopMappedBuffer(); - - IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); - rb.Push(RESULT_SUCCESS); - rb.PushMappedBuffer(buffer); - - LOG_WARNING(Service_BOSS, "(STUBBED) size={:#010X}", size); + LOG_WARNING(Service_BOSS, "StartTaskImmediate called"); + // StartTask and StartTaskImmediate do much the same thing + StartTask(ctx); + LOG_DEBUG(Service_BOSS, "called"); } void Module::Interface::CancelTask(Kernel::HLERequestContext& ctx) { @@ -398,6 +646,14 @@ void Module::Interface::CancelTask(Kernel::HLERequestContext& ctx) { const u32 size = rp.Pop(); auto& buffer = rp.PopMappedBuffer(); + if (size > TASK_ID_SIZE) { + LOG_WARNING(Service_BOSS, "Task Id cannot be longer than 8"); + } else { + std::string task_id(size, 0); + buffer.Read(task_id.data(), 0, size); + LOG_DEBUG(Service_BOSS, "Read task id {}", task_id); + } + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); rb.Push(RESULT_SUCCESS); rb.PushMappedBuffer(buffer); @@ -418,17 +674,30 @@ void Module::Interface::GetTaskFinishHandle(Kernel::HLERequestContext& ctx) { void Module::Interface::GetTaskState(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); const u32 size = rp.Pop(); - const u8 state = rp.Pop(); + const s8 state = rp.Pop(); auto& buffer = rp.PopMappedBuffer(); + TaskStatus task_status = TaskStatus::Failed; + u32 duration; + + if (size > TASK_ID_SIZE) { + LOG_WARNING(Service_BOSS, "Task Id cannot be longer than 8"); + duration = 0; + } else { + std::string task_id(size, 0); + buffer.Read(task_id.data(), 0, size); + LOG_DEBUG(Service_BOSS, "Read task id {}", task_id); + std::tie(task_status, duration) = util.GetTaskStatusAndDuration(task_id, false); + } + IPC::RequestBuilder rb = rp.MakeBuilder(4, 2); rb.Push(RESULT_SUCCESS); - rb.Push(0); /// TaskStatus - rb.Push(0); /// Current state value for task PropertyID 0x4 - rb.Push(0); /// unknown, usually 0 + rb.Push(static_cast(task_status)); /// TaskStatus + rb.Push(duration); /// Current state value for task PropertyID 0x4 + rb.Push(0); /// unknown, usually 0 rb.PushMappedBuffer(buffer); - LOG_WARNING(Service_BOSS, "(STUBBED) size={:#010X}, state={:#06X}", size, state); + LOG_DEBUG(Service_BOSS, "size={:#010X}, state={:#06X}", size, state); } void Module::Interface::GetTaskResult(Kernel::HLERequestContext& ctx) { @@ -436,14 +705,30 @@ void Module::Interface::GetTaskResult(Kernel::HLERequestContext& ctx) { const u32 size = rp.Pop(); auto& buffer = rp.PopMappedBuffer(); + TaskStatus task_status = TaskStatus::Failed; + u32 duration; + + if (size > TASK_ID_SIZE) { + LOG_WARNING(Service_BOSS, "Task Id cannot be longer than 8"); + duration = 0; + } else { + std::string task_id(size, 0); + buffer.Read(task_id.data(), 0, size); + LOG_DEBUG(Service_BOSS, "Read task id {}", task_id); + std::tie(task_status, duration) = util.GetTaskStatusAndDuration(task_id, true); + } + IPC::RequestBuilder rb = rp.MakeBuilder(4, 2); rb.Push(RESULT_SUCCESS); - rb.Push(0); // stub 0 (8 bit value) - rb.Push(0); // stub 0 (32 bit value) - rb.Push(0); // stub 0 (8 bit value) + // This might be task_status; however it is considered a failure if + // anything other than 0 is returned, apps won't call this method + // unless they have previously determined the task has ended + rb.Push(static_cast(task_status)); + rb.Push(duration); // return duration (number of times to check) + rb.Push(0); // stub 0 (8 bit value) rb.PushMappedBuffer(buffer); - LOG_WARNING(Service_BOSS, "(STUBBED) size={:#010X}", size); + LOG_DEBUG(Service_BOSS, "size={:#010X}", size); } void Module::Interface::GetTaskCommErrorCode(Kernel::HLERequestContext& ctx) { @@ -451,6 +736,17 @@ void Module::Interface::GetTaskCommErrorCode(Kernel::HLERequestContext& ctx) { const u32 size = rp.Pop(); auto& buffer = rp.PopMappedBuffer(); + if (size > TASK_ID_SIZE) { + LOG_WARNING(Service_BOSS, "Task Id cannot be longer than 8"); + } else { + std::string task_id(size, 0); + buffer.Read(task_id.data(), 0, size); + LOG_DEBUG(Service_BOSS, "Read task id {}", task_id); + if (!util.task_id_list.contains(task_id)) { + LOG_WARNING(Service_BOSS, "Could not find task_id in list"); + } + } + IPC::RequestBuilder rb = rp.MakeBuilder(4, 2); rb.Push(RESULT_SUCCESS); rb.Push(0); // stub 0 (32 bit value) @@ -468,13 +764,25 @@ void Module::Interface::GetTaskStatus(Kernel::HLERequestContext& ctx) { const u8 unk_param3 = rp.Pop(); auto& buffer = rp.PopMappedBuffer(); + TaskStatus task_status; + + if (size > TASK_ID_SIZE) { + LOG_WARNING(Service_BOSS, "Task Id cannot be longer than 8"); + task_status = TaskStatus::Failed; + } else { + std::string task_id(size, 0); + buffer.Read(task_id.data(), 0, size); + LOG_DEBUG(Service_BOSS, "Read task id {}", task_id); + task_status = util.GetTaskStatusAndDuration(task_id, false).first; + } + IPC::RequestBuilder rb = rp.MakeBuilder(2, 2); rb.Push(RESULT_SUCCESS); - rb.Push(0); // stub 0 (8 bit value) + rb.Push(static_cast(task_status)); // return current task status rb.PushMappedBuffer(buffer); - LOG_WARNING(Service_BOSS, "(STUBBED) size={:#010X}, unk_param2={:#04X}, unk_param3={:#04X}", - size, unk_param2, unk_param3); + LOG_DEBUG(Service_BOSS, "size={:#010X}, unk_param2={:#04X}, unk_param3={:#04X}", size, + unk_param2, unk_param3); } void Module::Interface::GetTaskError(Kernel::HLERequestContext& ctx) { @@ -515,18 +823,105 @@ void Module::Interface::DeleteNsData(Kernel::HLERequestContext& ctx) { } void Module::Interface::GetNsDataHeaderInfo(Kernel::HLERequestContext& ctx) { + NsDataHeaderInfo info{}; + IPC::RequestParser rp(ctx); - const u32 ns_data_id = rp.Pop(); - const u8 type = rp.Pop(); + info.ns_data_id = rp.Pop(); + const NsDataHeaderInfoType type = static_cast(rp.Pop()); const u32 size = rp.Pop(); auto& buffer = rp.PopMappedBuffer(); + ResultCode result = RESULT_FAILED; + u32 zero = 0; + std::optional entry = util.GetNsDataEntryFromID(info.ns_data_id); + if (entry.has_value()) { + info.program_id = entry->header.program_id; + info.datatype = entry->header.datatype; + info.payload_size = entry->header.payload_size; + info.version = entry->header.version; + + switch (type) { + case NsDataHeaderInfoType::ProgramId: + if (size != sizeof(info.program_id)) { + LOG_WARNING(Service_BOSS, "Invalid size {} for type {}", size, type); + break; + } + buffer.Write(&info.program_id, 0, size); + result = RESULT_SUCCESS; + LOG_DEBUG(Service_BOSS, "Wrote out program id {}", info.program_id); + break; + case NsDataHeaderInfoType::Unknown: + if (size != sizeof(u32)) { + LOG_WARNING(Service_BOSS, "Invalid size {} for type {}", size, type); + break; + } + buffer.Write(&zero, 0, size); + result = RESULT_SUCCESS; + LOG_DEBUG(Service_BOSS, "Wrote out unknown as zero"); + break; + case NsDataHeaderInfoType::Datatype: + if (size != sizeof(info.datatype)) { + LOG_WARNING(Service_BOSS, "Invalid size {} for type {}", size, type); + break; + } + buffer.Write(&info.datatype, 0, size); + result = RESULT_SUCCESS; + LOG_DEBUG(Service_BOSS, "Wrote out content datatype {}", info.datatype); + break; + case NsDataHeaderInfoType::PayloadSize: + if (size != sizeof(info.payload_size)) { + LOG_WARNING(Service_BOSS, "Invalid size {} for type {}", size, type); + break; + } + buffer.Write(&info.payload_size, 0, size); + result = RESULT_SUCCESS; + LOG_DEBUG(Service_BOSS, "Wrote out payload size {}", info.payload_size); + break; + case NsDataHeaderInfoType::NsDataId: + if (size != sizeof(info.ns_data_id)) { + LOG_WARNING(Service_BOSS, "Invalid size {} for type {}", size, type); + break; + } + buffer.Write(&info.ns_data_id, 0, size); + result = RESULT_SUCCESS; + LOG_DEBUG(Service_BOSS, "Wrote out NsDataID {}", info.ns_data_id); + break; + case NsDataHeaderInfoType::Version: + if (size != sizeof(info.version)) { + LOG_WARNING(Service_BOSS, "Invalid size {} for type {}", size, type); + break; + } + buffer.Write(&info.version, 0, size); + result = RESULT_SUCCESS; + LOG_DEBUG(Service_BOSS, "Wrote out version {}", info.version); + break; + case NsDataHeaderInfoType::Everything: + if (size != sizeof(info)) { + LOG_WARNING(Service_BOSS, "Invalid size {} for type {}", size, type); + break; + } + buffer.Write(&info, 0, size); + result = RESULT_SUCCESS; + LOG_DEBUG(Service_BOSS, + "Wrote out unknown with program id {:#018X}, unknown zero, " + "datatype {:#010X}, " + "payload size {:#010X}, NsDataID {:#010X}, version " + "{:#010X} and unknown zero", + info.program_id, info.datatype, info.payload_size, info.ns_data_id, + info.version); + break; + default: + LOG_WARNING(Service_BOSS, "Unknown header info type {}", type); + result = RESULT_FAILED; + } + } + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); - rb.Push(RESULT_SUCCESS); + rb.Push(result); rb.PushMappedBuffer(buffer); - LOG_WARNING(Service_BOSS, "(STUBBED) ns_data_id={:#010X}, type={:#04X}, size={:#010X}", - ns_data_id, type, size); + LOG_DEBUG(Service_BOSS, "ns_data_id={:#010X}, type={:#04X}, size={:#010X}", info.ns_data_id, + type, size); } void Module::Interface::ReadNsData(Kernel::HLERequestContext& ctx) { @@ -536,14 +931,73 @@ void Module::Interface::ReadNsData(Kernel::HLERequestContext& ctx) { const u32 size = rp.Pop(); auto& buffer = rp.PopMappedBuffer(); + // This is the error code for NsDataID not found + ResultCode result = RESULT_FAILED; + u32 read_size = 0; + FileSys::ArchiveFactory_ExtSaveData boss_extdata_archive_factory( + FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir), false, true); + const FileSys::Path boss_path{GetBossDataDir(util.extdata_id)}; + auto archive_result = boss_extdata_archive_factory.Open(boss_path, 0); + IPC::RequestBuilder rb = rp.MakeBuilder(3, 2); - rb.Push(RESULT_SUCCESS); - rb.Push(size); /// Should be actual read size - rb.Push(0); /// unknown + + std::optional entry = util.GetNsDataEntryFromID(ns_data_id); + if (!archive_result.Succeeded() || !entry.has_value()) { + LOG_WARNING(Service_BOSS, "Opening Spotpass Extdata failed."); + rb.Push(result); + rb.Push(read_size); + rb.Push(0); + rb.PushMappedBuffer(buffer); + LOG_DEBUG(Service_BOSS, "ns_data_id={:#010X}, offset={:#018X}, size={:#010X}", ns_data_id, + offset, size); + return; + } + LOG_DEBUG(Service_BOSS, "Spotpass Extdata opened successfully!"); + auto boss_archive = std::move(archive_result).Unwrap().get(); + FileSys::Path file_path = fmt::format("/{}", entry->filename); + FileSys::Mode mode{}; + mode.read_flag.Assign(1); + auto file_result = boss_archive->OpenFile(file_path, mode); + + if (!file_result.Succeeded()) { + LOG_WARNING(Service_BOSS, "Opening Spotpass file failed."); + rb.Push(result); + rb.Push(read_size); + rb.Push(0); + rb.PushMappedBuffer(buffer); + LOG_DEBUG(Service_BOSS, "ns_data_id={:#010X}, offset={:#018X}, size={:#010X}", ns_data_id, + offset, size); + return; + } + auto file = std::move(file_result).Unwrap(); + LOG_DEBUG(Service_BOSS, "Opening Spotpass file succeeded!"); + if (entry->header.payload_size < size + offset) { + LOG_WARNING(Service_BOSS, + "Request to read {:#010X} bytes at offset {:#010X}, payload " + "length is {:#010X}", + size, offset, u32(entry->header.payload_size)); + rb.Push(result); + rb.Push(read_size); + rb.Push(0); + rb.PushMappedBuffer(buffer); + LOG_DEBUG(Service_BOSS, "ns_data_id={:#010X}, offset={:#018X}, size={:#010X}", ns_data_id, + offset, size); + return; + } + std::vector ns_data_array(size); + file->Read(BOSS_HEADER_LENGTH + offset, size, ns_data_array.data()); + buffer.Write(ns_data_array.data(), 0, size); + result = RESULT_SUCCESS; + read_size = size; + LOG_DEBUG(Service_BOSS, "Read {:#010X} bytes from file {}", read_size, entry->filename); + + rb.Push(result); + rb.Push(read_size); /// Should be actual read size + rb.Push(0); /// unknown rb.PushMappedBuffer(buffer); - LOG_WARNING(Service_BOSS, "(STUBBED) ns_data_id={:#010X}, offset={:#018X}, size={:#010X}", - ns_data_id, offset, size); + LOG_DEBUG(Service_BOSS, "ns_data_id={:#010X}, offset={:#018X}, size={:#010X}", ns_data_id, + offset, size); } void Module::Interface::SetNsDataAdditionalInfo(Kernel::HLERequestContext& ctx) { @@ -571,38 +1025,46 @@ void Module::Interface::GetNsDataAdditionalInfo(Kernel::HLERequestContext& ctx) void Module::Interface::SetNsDataNewFlag(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); - const u32 unk_param1 = rp.Pop(); + const u32 ns_data_id = rp.Pop(); ns_data_new_flag = rp.Pop(); IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(RESULT_SUCCESS); - LOG_WARNING(Service_BOSS, "(STUBBED) unk_param1={:#010X}, ns_data_new_flag={:#04X}", unk_param1, + LOG_WARNING(Service_BOSS, "(STUBBED) ns_data_id={:#010X}, ns_data_new_flag={:#04X}", ns_data_id, ns_data_new_flag); } void Module::Interface::GetNsDataNewFlag(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); - const u32 unk_param1 = rp.Pop(); + const u32 ns_data_id = rp.Pop(); IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); rb.Push(RESULT_SUCCESS); rb.Push(ns_data_new_flag); - LOG_WARNING(Service_BOSS, "(STUBBED) unk_param1={:#010X}, ns_data_new_flag={:#04X}", unk_param1, + LOG_WARNING(Service_BOSS, "(STUBBED) ns_data_id={:#010X}, ns_data_new_flag={:#04X}", ns_data_id, ns_data_new_flag); } void Module::Interface::GetNsDataLastUpdate(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); - const u32 unk_param1 = rp.Pop(); + const u32 ns_data_id = rp.Pop(); + + u32 last_update = 0; + + std::optional entry = util.GetNsDataEntryFromID(ns_data_id); + if (entry.has_value()) { + last_update = entry->header.download_date; + LOG_DEBUG(Service_BOSS, "Last update: {}", last_update); + } IPC::RequestBuilder rb = rp.MakeBuilder(3, 0); rb.Push(RESULT_SUCCESS); - rb.Push(0); // stub 0 (32bit value) - rb.Push(0); // stub 0 (32bit value) + rb.Push(0); + rb.Push(last_update); // return the download date from the ns data - LOG_WARNING(Service_BOSS, "(STUBBED) unk_param1={:#010X}", unk_param1); + LOG_DEBUG(Service_BOSS, "ns_data_id={:#010X}", ns_data_id); } void Module::Interface::GetErrorCode(Kernel::HLERequestContext& ctx) { @@ -699,18 +1161,8 @@ void Module::Interface::GetTaskProperty0(Kernel::HLERequestContext& ctx) { } void Module::Interface::RegisterImmediateTask(Kernel::HLERequestContext& ctx) { - IPC::RequestParser rp(ctx); - const u32 size = rp.Pop(); - const u8 unk_param2 = rp.Pop(); - const u8 unk_param3 = rp.Pop(); - auto& buffer = rp.PopMappedBuffer(); - - IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); - rb.Push(RESULT_SUCCESS); - rb.PushMappedBuffer(buffer); - - LOG_WARNING(Service_BOSS, "(STUBBED) size={:#010X}, unk_param2={:#04X}, unk_param3={:#04X}", - size, unk_param2, unk_param3); + RegisterTask(ctx); + LOG_DEBUG(Service_BOSS, "called"); } void Module::Interface::SetTaskQuery(Kernel::HLERequestContext& ctx) { @@ -879,10 +1331,10 @@ void Module::Interface::SetNsDataNewFlagPrivileged(Kernel::HLERequestContext& ct IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(RESULT_SUCCESS); - LOG_WARNING( - Service_BOSS, - "(STUBBED) programID={:#018X}, unk_param1={:#010X}, ns_data_new_flag_privileged={:#04X}", - programID, unk_param1, ns_data_new_flag_privileged); + LOG_WARNING(Service_BOSS, + "(STUBBED) programID={:#018X}, unk_param1={:#010X}, " + "ns_data_new_flag_privileged={:#04X}", + programID, unk_param1, ns_data_new_flag_privileged); } void Module::Interface::GetNsDataNewFlagPrivileged(Kernel::HLERequestContext& ctx) { @@ -894,16 +1346,20 @@ void Module::Interface::GetNsDataNewFlagPrivileged(Kernel::HLERequestContext& ct rb.Push(RESULT_SUCCESS); rb.Push(ns_data_new_flag_privileged); - LOG_WARNING( - Service_BOSS, - "(STUBBED) programID={:#018X}, unk_param1={:#010X}, ns_data_new_flag_privileged={:#04X}", - programID, unk_param1, ns_data_new_flag_privileged); + LOG_WARNING(Service_BOSS, + "(STUBBED) programID={:#018X}, unk_param1={:#010X}, " + "ns_data_new_flag_privileged={:#04X}", + programID, unk_param1, ns_data_new_flag_privileged); } Module::Interface::Interface(std::shared_ptr boss, const char* name, u32 max_session) - : ServiceFramework(name, max_session), boss(std::move(boss)) {} + : ServiceFramework(name, max_session), boss(std::move(boss)) { + util = Util(); + this->boss->loader.ReadProgramId(util.program_id); + this->boss->loader.ReadExtdataId(util.extdata_id); +} -Module::Module(Core::System& system) { +Module::Module(Core::System& system) : loader(system.GetAppLoader()) { using namespace Kernel; // TODO: verify ResetType task_finish_event = diff --git a/src/core/hle/service/boss/boss.h b/src/core/hle/service/boss/boss.h index e4000e851..0fdf89ac4 100644 --- a/src/core/hle/service/boss/boss.h +++ b/src/core/hle/service/boss/boss.h @@ -4,10 +4,16 @@ #pragma once +#include #include +#include #include +#include +#include "core/file_sys/archive_backend.h" +#include "core/file_sys/directory_backend.h" #include "core/global.h" #include "core/hle/kernel/event.h" +#include "core/hle/service/boss/spotpass_utils.h" #include "core/hle/service/service.h" namespace Core { @@ -15,10 +21,10 @@ class System; } namespace Service::BOSS { - class Module final { public: explicit Module(Core::System& system); + Loader::AppLoader& loader; ~Module() = default; class Interface : public ServiceFramework { @@ -963,6 +969,8 @@ public: u8 ns_data_new_flag_privileged; u8 output_flag; + Util util; + template void serialize(Archive& ar, const unsigned int) { ar& new_arrival_flag; diff --git a/src/core/hle/service/boss/spotpass_utils.cpp b/src/core/hle/service/boss/spotpass_utils.cpp new file mode 100644 index 000000000..5f86dd44a --- /dev/null +++ b/src/core/hle/service/boss/spotpass_utils.cpp @@ -0,0 +1,556 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#ifdef ENABLE_WEB_SERVICE +#if defined(__ANDROID__) +#include +#endif +#include +#ifdef WIN32 +// Needed to prevent conflicts with system macros when httplib is included on windows +#undef CreateEvent +#undef CreateFile +#undef ERROR_NOT_FOUND +#undef ERROR_FILE_NOT_FOUND +#undef ERROR_PATH_NOT_FOUND +#undef ERROR_ALREADY_EXISTS +#endif +#endif +#include +#include +#include +#include +#include +#include "common/string_util.h" +#include "core/core.h" +#include "core/file_sys/archive_extsavedata.h" +#include "core/file_sys/errors.h" +#include "core/file_sys/file_backend.h" +#include "core/hle/ipc_helpers.h" +#include "core/hle/service/boss/boss.h" +#include "core/hw/aes/key.h" + +namespace Service::BOSS { + +FileSys::Path GetBossDataDir(u64 extdata_id) { + + const u32 high = static_cast(extdata_id >> 32); + const u32 low = static_cast(extdata_id & 0xFFFFFFFF); + + return FileSys::ConstructExtDataBinaryPath(1, high, low); +} + +bool WriteBossFile(u64 extdata_id, FileSys::Path file_path, u64 size, const u8* buffer, + std::string_view description) { + FileSys::ArchiveFactory_ExtSaveData boss_extdata_archive_factory( + FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir), false, true); + + const FileSys::Path boss_path{GetBossDataDir(extdata_id)}; + + auto archive_result = boss_extdata_archive_factory.Open(boss_path, 0); + if (!archive_result.Succeeded()) { + LOG_WARNING(Service_BOSS, "Extdata opening failed"); + return false; + } + LOG_DEBUG(Service_BOSS, "Spotpass Extdata opened successfully!"); + auto boss_archive = std::move(archive_result).Unwrap().get(); + + auto file_create_result = boss_archive->CreateFile(file_path, size); + if (file_create_result.is_error) { + LOG_WARNING(Service_BOSS, "{} could not be created, it may already exist", description); + } + FileSys::Mode file_open_mode = {}; + file_open_mode.write_flag.Assign(1); + auto file_result = boss_archive->OpenFile(file_path, file_open_mode); + if (!file_result.Succeeded()) { + LOG_WARNING(Service_BOSS, "Could not open {} for writing", description); + return false; + } + auto file = std::move(file_result).Unwrap(); + file->Write(0, size, true, buffer); + file->Close(); + return true; +} + +bool SendNewsMessage(std::vector decrypted_data, const u32 payload_size, u64 extdata_id, + std::string_view file_name_str) { + // TODO: Actually add a notification to the news service + // Looks like it has some sort of header(only 0x60 bytes, datetime and unknown at 0x20 + // missing?), https://www.3dbrew.org/wiki/NEWSS:AddNotification#Header_structure , then + // the message, then the image + LOG_INFO(Service_BOSS, "News message was received"); + constexpr u32 news_header_size = 0x60; + constexpr u32 news_title_offset = 0x20; + constexpr u32 news_title_size = news_header_size - news_title_offset; + constexpr u32 news_message_size = 0x1780; + constexpr u32 news_total_size = news_header_size + news_message_size; + + struct NewsMessage { + INSERT_PADDING_BYTES(news_title_offset); + std::array news_title; + std::array news_message; + }; + static_assert(sizeof(NewsMessage) == news_total_size, "NewsMessage has incorrect size"); + + if (payload_size < news_total_size) { + LOG_ERROR(Service_BOSS, "Payload is too short to contain news message"); + return false; + } + + NewsMessage* message = + reinterpret_cast(decrypted_data.data() + BOSS_ENTIRE_HEADER_LENGTH); + + std::u16string_view news_title_string(reinterpret_cast(message->news_title.data()), + news_title_size / 2); + std::u16string_view news_message_string( + reinterpret_cast(message->news_message.data()), news_message_size / 2); + + LOG_INFO(Service_BOSS, "News title is: {}", Common::UTF16ToUTF8(news_title_string)); + LOG_INFO(Service_BOSS, "News message is:\n{}", Common::UTF16ToUTF8(news_message_string)); + + if (payload_size > news_total_size) { + LOG_INFO(Service_BOSS, "Image is present in news, dumping..."); + if (!WriteBossFile(extdata_id, fmt::format("{}_news_image.jpg", file_name_str), + payload_size - news_total_size, + decrypted_data.data() + BOSS_ENTIRE_HEADER_LENGTH + news_total_size, + "image file")) { + LOG_WARNING(Service_BOSS, "Could not write image file"); + } + } + return true; +} + +bool DecryptBossData(const BossPayloadHeader* payload_header, const u32 data_size, + const u8* encrypted_data, u8* decrypted_data) { + // AES details here: https://www.3dbrew.org/wiki/SpotPass#Content_Container + // IV is data in payload + 32 bit Big Endian 1 + const u32_be one = 1; + std::vector iv(sizeof(payload_header->iv_start) + sizeof(one)); + std::memcpy(iv.data(), payload_header->iv_start.data(), sizeof(payload_header->iv_start)); + std::memcpy(iv.data() + sizeof(payload_header->iv_start), &one, sizeof(one)); + LOG_DEBUG(Service_BOSS, "IV is {:#018X}{:16X}", + static_cast(*reinterpret_cast(iv.data())), + static_cast(*reinterpret_cast(iv.data() + sizeof(u64_be)))); + + CryptoPP::CTR_Mode::Decryption aes; + HW::AES::AESKey key = HW::AES::GetNormalKey(0x38); + if (key == HW::AES::AESKey{}) { + LOG_WARNING(Service_BOSS, "AES Key 0x38 not found"); + return false; + } + + aes.SetKeyWithIV(key.data(), CryptoPP::AES::BLOCKSIZE, iv.data()); + aes.ProcessData(decrypted_data, encrypted_data, data_size); + return true; +} + +bool DownloadBossDataFromURL(std::string_view url, std::string_view file_name, u64 program_id, + u64 extdata_id) { +#ifdef ENABLE_WEB_SERVICE + const auto url_parse_result = boost::urls::parse_uri(url); + const std::string file_name_str = fmt::format("/{}", file_name); + if (url_parse_result.has_error()) { + LOG_ERROR(Service_BOSS, "Invalid URL {}", url); + return false; + } + const auto url_parsed = url_parse_result.value(); + const std::string scheme = url_parsed.scheme(); + const std::string host = url_parsed.host(); + const std::string path = url_parsed.path(); + LOG_DEBUG(Service_BOSS, "Scheme is {}, host is {}, path is {}", scheme, host, path); + const std::unique_ptr client = + std::make_unique(fmt::format("{}://{}", scheme, host)); + httplib::Request request{ + .method = "GET", + .path = path, + // Needed when httplib is included on android + .matches = httplib::Match(), + }; + client->set_follow_location(true); + client->enable_server_certificate_verification(false); + + const auto result = client->send(request); + if (!result) { + LOG_ERROR(Service_BOSS, "GET to {}://{}{} returned error {}", scheme, host, path, + httplib::to_string(result.error())); + return false; + } + const auto& response = result.value(); + if (response.status >= 400) { + LOG_ERROR(Service_BOSS, "GET to {}://{}{} returned error status code: {}", scheme, host, + path, response.status); + return false; + } + if (!response.headers.contains("content-type")) { + LOG_ERROR(Service_BOSS, "GET to {}://{}{} returned no content", scheme, host, path); + return false; + } + + if (response.body.size() < BOSS_PAYLOAD_HEADER_LENGTH) { + LOG_WARNING(Service_BOSS, "Payload size of {} too short for boss payload", + response.body.size()); + return false; + } + const BossPayloadHeader* payload_header = + reinterpret_cast(response.body.data()); + + if (BOSS_MAGIC != payload_header->boss) { + LOG_WARNING(Service_BOSS, "Start of file is not '{}', it's '{}'", BOSS_MAGIC, + static_cast(payload_header->boss)); + return false; + } + + if (payload_header->magic != BOSS_PAYLOAD_MAGIC) { + LOG_WARNING(Service_BOSS, "Magic number mismatch, expecting {}, found {}", + BOSS_PAYLOAD_MAGIC, static_cast(payload_header->magic)); + return false; + } + + if (payload_header->filesize != response.body.size()) { + LOG_WARNING(Service_BOSS, "Expecting response to be size {}, actual size is {}", + static_cast(payload_header->filesize), response.body.size()); + return false; + } + + // Temporarily also write payload (maybe for re-implementing spotpass when it goes down in the + // future?) + + if (!WriteBossFile(extdata_id, fmt::format("{}_payload", file_name_str), response.body.size(), + reinterpret_cast(response.body.data()), "payload file")) { + LOG_WARNING(Service_BOSS, "Could not write payload file"); + } + + // end payload block + const u32 data_size = payload_header->filesize - BOSS_PAYLOAD_HEADER_LENGTH; + + std::vector decrypted_data(data_size); + + if (!DecryptBossData( + payload_header, data_size, + reinterpret_cast(response.body.data() + BOSS_PAYLOAD_HEADER_LENGTH), + decrypted_data.data())) { + LOG_ERROR(Service_BOSS, "Could not decrypt payload"); + return false; + } + + // Temporarily also write raw data + + if (!WriteBossFile(extdata_id, fmt::format("{}_raw_data", file_name_str), decrypted_data.size(), + decrypted_data.data(), "raw data file")) { + LOG_WARNING(Service_BOSS, "Could not write raw data file"); + } + + // end raw data block + + if (decrypted_data.size() < BOSS_ENTIRE_HEADER_LENGTH) { + LOG_WARNING(Service_BOSS, "Payload size to small to be boss data: {}", + decrypted_data.size()); + return false; + } + + BossHeader header{}; + std::memcpy(&header.program_id, decrypted_data.data() + BOSS_CONTENT_HEADER_LENGTH, + BOSS_HEADER_LENGTH - BOSS_EXTDATA_HEADER_LENGTH); + + const u32 payload_size = static_cast(decrypted_data.size() - BOSS_ENTIRE_HEADER_LENGTH); + if (header.payload_size != payload_size) { + LOG_WARNING(Service_BOSS, "Payload has incorrect size, was expecting {}, found {}", + static_cast(header.payload_size), payload_size); + return false; + } + + if (program_id != header.program_id) { + LOG_WARNING(Service_BOSS, "Mismatched program id, was expecting {:#018X}, found {:#018X}", + program_id, u64(header.program_id)); + if (header.program_id == NEWS_PROG_ID) { + SendNewsMessage(decrypted_data, payload_size, extdata_id, file_name_str); + } + return false; + } + + std::vector data_file(BOSS_HEADER_LENGTH + payload_size); + header.header_length = BOSS_EXTDATA_HEADER_LENGTH; + std::memcpy(data_file.data(), &header, BOSS_HEADER_LENGTH); + std::memcpy(data_file.data() + BOSS_HEADER_LENGTH, + decrypted_data.data() + BOSS_ENTIRE_HEADER_LENGTH, payload_size); + + if (!WriteBossFile(extdata_id, file_name_str, data_file.size(), data_file.data(), + "spotpass file")) { + LOG_WARNING(Service_BOSS, "Could not write spotpass file"); + } + return true; +#else + LOG_ERROR(Service_BOSS, "Cannot download data as web services are not enabled"); + return false; +#endif +} + +std::vector Util::GetNsDataEntries() { + std::vector ns_data; + std::vector boss_files = GetBossExtDataFiles(); + FileSys::ArchiveFactory_ExtSaveData boss_extdata_archive_factory( + FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir), false, true); + const FileSys::Path boss_path{GetBossDataDir(extdata_id)}; + auto archive_result = boss_extdata_archive_factory.Open(boss_path, 0); + + if (!archive_result.Succeeded()) { + LOG_ERROR(Service_BOSS, "Extdata opening failed"); + return ns_data; + } + LOG_DEBUG(Service_BOSS, "Spotpass Extdata opened successfully!"); + auto boss_archive = std::move(archive_result).Unwrap().get(); + + for (const auto& cur_file : boss_files) { + if (cur_file.is_directory || cur_file.file_size < BOSS_HEADER_LENGTH) { + LOG_WARNING(Service_BOSS, "Spotpass extdata contains directory or file is too short"); + continue; + } + + NsDataEntry entry{}; + entry.filename = Common::UTF16ToUTF8(cur_file.filename); + const FileSys::Path file_path = fmt::format("/{}", entry.filename); + LOG_DEBUG(Service_BOSS, "Spotpass filename={}", entry.filename); + + FileSys::Mode mode{}; + mode.read_flag.Assign(1); + + auto file_result = boss_archive->OpenFile(file_path, mode); + + if (!file_result.Succeeded()) { + LOG_WARNING(Service_BOSS, "Opening Spotpass file failed."); + continue; + } + auto file = std::move(file_result).Unwrap(); + LOG_DEBUG(Service_BOSS, "Opening Spotpass file succeeded!"); + file->Read(0, BOSS_HEADER_LENGTH, reinterpret_cast(&entry.header)); + // Extdata header should have size 0x18: + // https://www.3dbrew.org/wiki/SpotPass#Payload_Content_Header + if (entry.header.header_length != BOSS_EXTDATA_HEADER_LENGTH) { + LOG_WARNING(Service_BOSS, + "Incorrect header length or non-spotpass file; expected {:#010X}, " + "found {:#010X}", + BOSS_EXTDATA_HEADER_LENGTH, entry.header.header_length); + continue; + } + if (entry.header.program_id != program_id) { + LOG_WARNING(Service_BOSS, + "Mismatched program ID in spotpass data. Was expecting " + "{:#018X}, found {:#018X}", + program_id, u64(entry.header.program_id)); + continue; + } + // Check the payload size is correct, excluding header + if (entry.header.payload_size != cur_file.file_size - BOSS_HEADER_LENGTH) { + LOG_WARNING(Service_BOSS, + "Mismatched file size, was expecting {:#010X}, found {:#010X}", + u32(entry.header.payload_size), cur_file.file_size - BOSS_HEADER_LENGTH); + continue; + } + LOG_DEBUG( + Service_BOSS, "Datatype is {:#010X}, Payload size is {:#010X}, NsDataID is {:#010X}", + static_cast(entry.header.datatype), static_cast(entry.header.payload_size), + static_cast(entry.header.ns_data_id)); + + ns_data.push_back(entry); + } + return ns_data; +} + +std::vector Util::GetBossExtDataFiles() { + + std::vector boss_files; + + FileSys::ArchiveFactory_ExtSaveData boss_extdata_archive_factory( + FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir), false, true); + const FileSys::Path boss_path{GetBossDataDir(extdata_id)}; + + auto archive_result = boss_extdata_archive_factory.Open(boss_path, 0); + if (!archive_result.Succeeded()) { + LOG_WARNING(Service_BOSS, "Extdata opening failed"); + return boss_files; + } + LOG_DEBUG(Service_BOSS, "Spotpass Extdata opened successfully!"); + auto boss_archive = std::move(archive_result).Unwrap().get(); + + auto dir_result = boss_archive->OpenDirectory(DIR_SEP); + if (!dir_result.Succeeded()) { + LOG_WARNING(Service_BOSS, "Extdata directory opening failed"); + return boss_files; + } + LOG_DEBUG(Service_BOSS, "Spotpass Extdata directory opened successfully!"); + const auto dir = std::move(dir_result).Unwrap(); + // Keep reading the directory 32 files at a time until all files have been checked + constexpr u32 files_to_read = 32; + u32 entry_count = 0; + size_t i = 0; + do { + boss_files.resize(boss_files.size() + files_to_read); + entry_count = dir->Read(files_to_read, boss_files.data() + (i * files_to_read)); + } while (files_to_read <= entry_count && ++i); + LOG_DEBUG(Service_BOSS, "Spotpass Extdata directory contains {} files", + i * files_to_read + entry_count); + boss_files.resize(i * files_to_read + entry_count); + return boss_files; +} + +u16 Util::GetOutputEntries(u32 filter, u32 max_entries, Kernel::MappedBuffer& buffer) { + std::vector ns_data = GetNsDataEntries(); + std::vector output_entries; + for (const auto& cur_entry : ns_data) { + const u16 datatype_high = + static_cast(static_cast(cur_entry.header.datatype) >> 16); + const u16 datatype_low = + static_cast(static_cast(cur_entry.header.datatype) & 0xFFFF); + const u16 filter_high = static_cast(filter >> 16); + const u16 filter_low = static_cast(filter & 0xFFFF); + if (filter != 0xFFFFFFFF && + (filter_high != datatype_high || (filter_low & datatype_low) == 0)) { + LOG_DEBUG( + Service_BOSS, + "Filtered out NsDataID {:#010X}; failed filter {:#010X} with datatype {:#010X}", + static_cast(cur_entry.header.ns_data_id), filter, + static_cast(cur_entry.header.datatype)); + continue; + } + if (output_entries.size() >= max_entries) { + LOG_WARNING(Service_BOSS, "Reached maximum number of entries"); + break; + } + output_entries.push_back(cur_entry.header.ns_data_id); + } + buffer.Write(output_entries.data(), 0, sizeof(u32) * output_entries.size()); + LOG_DEBUG(Service_BOSS, "{} usable entries returned", output_entries.size()); + return static_cast(output_entries.size()); +} + +std::pair Util::GetTaskStatusAndDuration(std::string task_id, + bool wait_on_result) { + // Default duration is zero -> means no more runs of the task are allowed + u32 duration = 0; + + if (!task_id_list.contains(task_id)) { + LOG_WARNING(Service_BOSS, "Could not find task_id in list"); + return {TaskStatus::Failed, duration}; + } + LOG_DEBUG(Service_BOSS, "Found currently running task id"); + task_id_list[task_id].times_checked++; + + // Get the duration from the task if available + if (const auto* dur_prop = + task_id_list[task_id].props.contains(PropertyID::Duration) + ? std::get_if(&task_id_list[task_id].props[PropertyID::Duration]) + : nullptr; + dur_prop) { + duration = *dur_prop; + } + + if (task_id_list[task_id].download_task.valid()) { + LOG_DEBUG(Service_BOSS, "Task is still running"); + auto status = task_id_list[task_id].download_task.wait_for(std::chrono::microseconds(0)); + if (status == std::future_status::ready || wait_on_result) { + LOG_DEBUG(Service_BOSS, + wait_on_result ? "Waiting for result..." : "Task just finished"); + + task_id_list[task_id].task_result = task_id_list[task_id].download_task.get(); + if (task_id_list[task_id].task_result) { + LOG_DEBUG(Service_BOSS, "Task ran successfully"); + return {TaskStatus::Success, duration}; + } + + LOG_WARNING(Service_BOSS, "Task failed"); + return {TaskStatus::Failed, duration}; + } + + LOG_DEBUG(Service_BOSS, "Task is still running"); + return {TaskStatus::Running, duration}; + } + + LOG_DEBUG(Service_BOSS, "Task has finished running or is invalid"); + + if (task_id_list[task_id].task_result) { + LOG_DEBUG(Service_BOSS, "Task ran successfully"); + return {TaskStatus::Success, duration}; + } + + LOG_WARNING(Service_BOSS, "Task failed"); + return {TaskStatus::Failed, duration}; +} + +std::optional Util::GetNsDataEntryFromID(u32 ns_data_id) { + std::vector ns_data = GetNsDataEntries(); + const auto entry_iter = std::find_if(ns_data.begin(), ns_data.end(), [ns_data_id](auto entry) { + return entry.header.ns_data_id == ns_data_id; + }); + if (entry_iter == ns_data.end()) { + LOG_WARNING(Service_BOSS, "Could not find NsData with ID {:#010X}", ns_data_id); + return std::nullopt; + } + return *entry_iter; +} + +bool Util::ReadWriteProperties(bool write, PropertyID property_id, u32 size, + Kernel::MappedBuffer& buffer) { + if (!cur_props.props.contains(property_id)) { + LOG_ERROR(Service_BOSS, "Unknown property with id {:#06X}", property_id); + return false; + } + + auto& prop = cur_props.props[property_id]; + + auto readwrite_pod = [&](T& cur_prop) { + static_assert(std::is_trivial::value, + "Only trivial types are allowed for readwrite_pod"); + if (size != sizeof(cur_prop)) { + LOG_ERROR(Service_BOSS, "Unexpected size of property {:#06x}, was expecting {}, got {}", + property_id, sizeof(cur_prop), size); + } + if (write) { + buffer.Write(&cur_prop, 0, size); + } else { + T new_prop = 0; + buffer.Read(&new_prop, 0, size); + prop = new_prop; + LOG_DEBUG(Service_BOSS, "Read property {:#06X}, value {:#010X}", property_id, new_prop); + } + }; + + auto readwrite_vector = [&](std::vector& cur_prop) { + if (size != cur_prop.size() * sizeof(T)) { + LOG_ERROR(Service_BOSS, "Unexpected size of property {:#06x}, was expecting {}, got {}", + property_id, cur_prop.size(), size); + } + LOG_DEBUG(Service_BOSS, + "Vector property contains {} elements of size {} for a total size of {}", + cur_prop.size(), sizeof(T), size); + if (write) { + buffer.Write(cur_prop.data(), 0, size); + } else { + std::vector new_prop(cur_prop.size()); + buffer.Read(new_prop.data(), 0, size); + prop = new_prop; + if (sizeof(T) == sizeof(u8)) { + LOG_DEBUG(Service_BOSS, "Read property {:#06X}, value {}", property_id, + std::string_view(reinterpret_cast(new_prop.data()), size)); + } else if (sizeof(T) == sizeof(u32) && new_prop.size() == CERTIDLIST_SIZE) { + LOG_DEBUG(Service_BOSS, "Read property {:#06X}, values {:#010X},{:#010X},{:#010X}", + property_id, new_prop[0], new_prop[1], new_prop[2]); + } + } + }; + + if (const auto char_prop = std::get_if(&prop)) + readwrite_pod(*char_prop); + else if (const auto short_prop = std::get_if(&prop)) + readwrite_pod(*short_prop); + else if (const auto int_prop = std::get_if(&prop)) + readwrite_pod(*int_prop); + else if (const auto charvec_prop = std::get_if>(&prop)) + readwrite_vector(*charvec_prop); + else if (const auto intvec_prop = std::get_if>(&prop)) + readwrite_vector(*intvec_prop); + + return true; +} + +} // namespace Service::BOSS \ No newline at end of file diff --git a/src/core/hle/service/boss/spotpass_utils.h b/src/core/hle/service/boss/spotpass_utils.h new file mode 100644 index 000000000..dba1a3e30 --- /dev/null +++ b/src/core/hle/service/boss/spotpass_utils.h @@ -0,0 +1,212 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include +#include +#include "core/file_sys/archive_backend.h" +#include "core/file_sys/directory_backend.h" +#include "core/global.h" +#include "core/hle/kernel/event.h" +#include "core/hle/service/service.h" + +namespace Service::BOSS { + +// File header info from +// https://www.3dbrew.org/wiki/SpotPass#Payload_Content_Header +// So the total header is only 52 bytes long + +constexpr u32 BOSS_HEADER_LENGTH = 0x34; +// 52 bytes doesn't align nicely into 8-byte words +#pragma pack(push, 4) +struct BossHeader { + u8 header_length; + INSERT_PADDING_BYTES(11); + u32_be unknown; + u32_be download_date; + INSERT_PADDING_BYTES(4); + u64_be program_id; + INSERT_PADDING_BYTES(4); + u32_be datatype; + u32_be payload_size; + u32_be ns_data_id; + u32_be version; +}; +#pragma pack(pop) + +static_assert(sizeof(BossHeader) == 0x34, "BossHeader has incorrect size"); + +// Payload header info from +// https://www.3dbrew.org/wiki/SpotPass#Content_Container +// So the total header is only 40 bytes long + +constexpr u32 BOSS_PAYLOAD_HEADER_LENGTH = 0x28; +constexpr u32 BOSS_MAGIC = Loader::MakeMagic('b', 'o', 's', 's'); +constexpr u32 BOSS_PAYLOAD_MAGIC = 0x10001; +constexpr u64 NEWS_PROG_ID = 0x0004013000003502; +// 40 bytes doesn't align nicely into 8-byte words either +#pragma pack(push, 4) +struct BossPayloadHeader { + u32_le boss; + u32_be magic; + u32_be filesize; + u64_be release_date; + u16_be one; + INSERT_PADDING_BYTES(2); + u16_be hash_type; + u16_be rsa_size; + std::array iv_start; +}; +#pragma pack(pop) + +static_assert(sizeof(BossPayloadHeader) == 0x28, "BossPayloadHeader has incorrect size"); + +constexpr u32 BOSS_CONTENT_HEADER_LENGTH = 0x132; +constexpr u32 BOSS_HEADER_WITH_HASH_LENGTH = 0x13C; +constexpr u32 BOSS_ENTIRE_HEADER_LENGTH = BOSS_CONTENT_HEADER_LENGTH + BOSS_HEADER_WITH_HASH_LENGTH; +constexpr u32 BOSS_EXTDATA_HEADER_LENGTH = 0x18; +constexpr u32 BOSS_A_ENTRY_SIZE = 0x800; +constexpr u32 BOSS_S_ENTRY_SIZE = 0xC00; +constexpr u32 BOSS_SAVE_HEADER_SIZE = 4; +constexpr u32 BOSS_S_PROG_ID_OFFSET = 0x10; +constexpr u32 BOSS_S_TASK_ID_OFFSET = 0x18; +constexpr u32 BOSS_S_URL_OFFSET = 0x21C; + +struct NsDataEntry { + std::string filename; + BossHeader header; +}; + +constexpr ResultCode RESULT_FAILED(1); +constexpr u8 TASK_ID_SIZE = 8; + +enum class NsDataHeaderInfoType : u8 { + ProgramId, + Unknown, + Datatype, + PayloadSize, + NsDataId, + Version, + Everything, +}; + +struct NsDataHeaderInfo { + u64 program_id; + INSERT_PADDING_BYTES(4); + u32 datatype; + u32 payload_size; + u32 ns_data_id; + u32 version; + INSERT_PADDING_BYTES(4); +}; + +static_assert(sizeof(NsDataHeaderInfo) == 0x20, "NsDataHeaderInfo has incorrect size"); + +enum class TaskStatus : u8 { + Success = 0, + Running = 2, + NotStarted = 5, + Failed = 7, +}; + +enum class PropertyID : u16 { + Interval = 0x03, + Duration = 0x04, + Url = 0x07, + Headers = 0x0D, + CertId = 0x0E, + CertIdList = 0x0F, + LoadCert = 0x10, + LoadRootCert = 0x11, + TotalTasks = 0x35, + TaskIdList = 0x36, +}; + +constexpr size_t URL_SIZE = 0x200; +constexpr size_t HEADERS_SIZE = 0x360; +constexpr size_t CERTIDLIST_SIZE = 3; +constexpr size_t TASKIDLIST_SIZE = 0x400; + +struct BossTaskProperties { + std::future download_task; + bool task_result; + u32 times_checked; + std::map, std::vector>> props{ + {static_cast(0x00), u8()}, + {static_cast(0x01), u8()}, + {static_cast(0x02), u32()}, + // interval + {PropertyID::Interval, u32()}, + // duration + {PropertyID::Duration, u32()}, + {static_cast(0x05), u8()}, + {static_cast(0x06), u8()}, + // url + {PropertyID::Url, std::vector(URL_SIZE)}, + {static_cast(0x08), u32()}, + {static_cast(0x09), u8()}, + {static_cast(0x0A), std::vector(0x100)}, + {static_cast(0x0B), std::vector(0x200)}, + {static_cast(0x0C), u32()}, + // headers + {PropertyID::Headers, std::vector(HEADERS_SIZE)}, + // certid + {PropertyID::CertId, u32()}, + // certidlist + {PropertyID::CertIdList, std::vector(CERTIDLIST_SIZE)}, + // loadcert (bool) + {PropertyID::LoadCert, u8()}, + // loadrootcert (bool) + {PropertyID::LoadRootCert, u8()}, + {static_cast(0x12), u8()}, + {static_cast(0x13), u32()}, + {static_cast(0x14), u32()}, + {static_cast(0x15), std::vector(0x40)}, + {static_cast(0x16), u32()}, + {static_cast(0x18), u8()}, + {static_cast(0x19), u8()}, + {static_cast(0x1A), u8()}, + {static_cast(0x1B), u32()}, + {static_cast(0x1C), u32()}, + // totaltasks + {PropertyID::TotalTasks, u16()}, + // taskidlist + {PropertyID::TaskIdList, std::vector(TASKIDLIST_SIZE)}, + {static_cast(0x3B), u32()}, + {static_cast(0x3E), std::vector(0x200)}, + {static_cast(0x3F), u8()}, + }; +}; + +constexpr std::array BOSS_SYSTEM_SAVEDATA_ID{ + 0x00, 0x00, 0x00, 0x00, 0x34, 0x00, 0x01, 0x00, +}; + +constexpr std::array BOSS_SYSTEM_SAVEDATA_HEADER{0x00, 0x80, 0x34, 0x12}; + +FileSys::Path GetBossDataDir(u64 extdata_id); +bool DownloadBossDataFromURL(std::string_view url, std::string_view file_name, u64 program_id, + u64 extdata_id); + +class Util { +public: + std::vector GetNsDataEntries(); + std::vector GetBossExtDataFiles(); + u16 GetOutputEntries(u32 filter, u32 max_entries, Kernel::MappedBuffer& buffer); + bool ReadWriteProperties(bool write, PropertyID property_id, u32 size, + Kernel::MappedBuffer& buffer); + std::optional GetNsDataEntryFromID(u32 ns_data_id); + std::pair GetTaskStatusAndDuration(std::string task_id, bool wait_on_result); + +public: + u64 program_id; + u64 extdata_id; + std::map task_id_list; + BossTaskProperties cur_props; +}; +} // namespace Service::BOSS \ No newline at end of file diff --git a/src/core/hle/service/fs/archive.cpp b/src/core/hle/service/fs/archive.cpp index ba98ce045..77e539258 100644 --- a/src/core/hle/service/fs/archive.cpp +++ b/src/core/hle/service/fs/archive.cpp @@ -353,6 +353,10 @@ void ArchiveManager::RegisterArchiveTypes() { std::make_unique(sdmc_directory, false); RegisterArchiveType(std::move(extsavedata_factory), ArchiveIdCode::ExtSaveData); + auto bossextsavedata_factory = + std::make_unique(sdmc_directory, false, true); + RegisterArchiveType(std::move(bossextsavedata_factory), ArchiveIdCode::BossExtSaveData); + auto sharedextsavedata_factory = std::make_unique(nand_directory, true); RegisterArchiveType(std::move(sharedextsavedata_factory), ArchiveIdCode::SharedExtSaveData); diff --git a/src/core/hle/service/fs/archive.h b/src/core/hle/service/fs/archive.h index f4604803b..c9e8011ec 100644 --- a/src/core/hle/service/fs/archive.h +++ b/src/core/hle/service/fs/archive.h @@ -37,6 +37,7 @@ enum class ArchiveIdCode : u32 { SaveData = 0x00000004, ExtSaveData = 0x00000006, SharedExtSaveData = 0x00000007, + BossExtSaveData = 0x12345678, SystemSaveData = 0x00000008, SDMC = 0x00000009, SDMCWriteOnly = 0x0000000A,