// Copyright 2023 Citra Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #include #include "common/archives.h" #include "common/file_util.h" #include "common/serialization/boost_std_variant.hpp" #include "common/string_util.h" #include "core/core.h" #include "core/file_sys/archive_backend.h" #include "core/file_sys/archive_extsavedata.h" #include "core/file_sys/archive_systemsavedata.h" #include "core/file_sys/directory_backend.h" #include "core/file_sys/errors.h" #include "core/file_sys/file_backend.h" #include "core/hle/kernel/hle_ipc.h" #include "core/hle/service/boss/online_service.h" namespace Service::BOSS { namespace ErrCodes { enum { TaskNotFound = 51, NsDataNotFound = 64, UnknownPropertyID = 77, }; } OnlineService::OnlineService(u64 program_id_, u64 extdata_id_) : program_id(program_id_), extdata_id(extdata_id_) {} template void OnlineService::serialize(Archive& ar, const unsigned int) { ar& current_props; ar& task_id_list; ar& program_id; ar& extdata_id; } SERIALIZE_IMPL(OnlineService) template void BossTaskProperties::serialize(Archive& ar, const unsigned int) { ar& task_result; ar& properties; } SERIALIZE_IMPL(BossTaskProperties) Result OnlineService::InitializeSession(u64 init_program_id) { // The BOSS service uses three databases: // 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 // BOSS_SV: Saved Values? Includes task id and most properties keyed on task_id // It saves data in its 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. current_props = BossTaskProperties(); if (init_program_id == 0) { init_program_id = program_id; } const std::string& nand_directory = FileUtil::GetUserPath(FileUtil::UserPath::NANDDir); FileSys::ArchiveFactory_SystemSaveData systemsavedata_factory(nand_directory); // Open the SystemSaveData archive 0x00010034 const 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 (archive_result.Succeeded()) { 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); // 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()) { LOG_ERROR(Service_BOSS, "Could not open BOSS savedata"); // TODO: Proper error code. return ResultUnknown; } boss_system_save_data_archive = std::move(create_archive_result).Unwrap(); } else { LOG_ERROR(Service_BOSS, "Could not open BOSS savedata"); // TODO: Proper error code. return ResultUnknown; } FileSys::Mode open_mode = {}; open_mode.read_flag.Assign(1); // TODO: Read data from BOSS_A.db auto boss_sv_result = boss_system_save_data_archive->OpenFile(FileSys::Path("/BOSS_SV.db"), open_mode); auto boss_ss_result = boss_system_save_data_archive->OpenFile(FileSys::Path("/BOSS_SS.db"), open_mode); if (!boss_sv_result.Succeeded() || !boss_ss_result.Succeeded()) { LOG_ERROR(Service_BOSS, "Could not open BOSS database."); return ResultSuccess; } auto boss_sv = std::move(boss_sv_result).Unwrap(); auto boss_ss = std::move(boss_ss_result).Unwrap(); 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_ERROR(Service_BOSS, "BOSS database has incorrect size."); return ResultSuccess; } // Read the files if they already exist const u64 num_entries = (boss_sv->GetSize() - BOSS_SAVE_HEADER_SIZE) / BOSS_S_ENTRY_SIZE; for (u64 i = 0; i < num_entries; i++) { const u64 entry_offset = i * BOSS_S_ENTRY_SIZE + BOSS_SAVE_HEADER_SIZE; BossSVData sv_data; boss_sv->Read(entry_offset, sizeof(BossSVData), reinterpret_cast(&sv_data)); BossSSData ss_data; boss_ss->Read(entry_offset, sizeof(BossSSData), reinterpret_cast(&ss_data)); if (sv_data.program_id != init_program_id) { continue; } std::vector url_vector(URL_SIZE); std::memcpy(url_vector.data(), ss_data.url.data(), URL_SIZE); current_props.properties[PropertyID::Url] = url_vector; const auto task_id = std::string(sv_data.task_id.data()); if (task_id_list.contains(task_id)) { LOG_WARNING(Service_BOSS, "TaskId already in list, will be replaced"); task_id_list.erase(task_id); } task_id_list.emplace(task_id, std::move(current_props)); current_props = BossTaskProperties(); } return ResultSuccess; } void OnlineService::RegisterTask(const u32 size, Kernel::MappedBuffer& buffer) { std::string task_id(size, 0); buffer.Read(task_id.data(), 0, size); if (task_id_list.contains(task_id)) { LOG_WARNING(Service_BOSS, "TaskId already in list, will be replaced"); task_id_list.erase(task_id); } task_id_list.emplace(task_id, std::move(current_props)); current_props = BossTaskProperties(); } Result OnlineService::UnregisterTask(const u32 size, Kernel::MappedBuffer& buffer) { if (size > TASK_ID_SIZE) { LOG_WARNING(Service_BOSS, "TaskId cannot be longer than 8"); // TODO: Proper error code. return ResultUnknown; } std::string task_id(size, 0); buffer.Read(task_id.data(), 0, size); if (task_id_list.erase(task_id) == 0) { LOG_WARNING(Service_BOSS, "TaskId '{}' not in list", task_id); return {ErrCodes::TaskNotFound, ErrorModule::BOSS, ErrorSummary::InvalidState, ErrorLevel::Status}; } return ResultSuccess; } void OnlineService::GetTaskIdList() { current_props.properties[PropertyID::TotalTasks] = static_cast(task_id_list.size()); const auto num_task_ids = TASKIDLIST_SIZE / TASK_ID_SIZE; std::vector> task_ids(num_task_ids); u16 num_returned_task_ids = 0; for (const auto& iter : task_id_list) { const std::string current_task_id = iter.first; if (current_task_id.size() > TASK_ID_SIZE || num_returned_task_ids >= num_task_ids) { LOG_WARNING(Service_BOSS, "TaskId {} too long or too many TaskIds", current_task_id); continue; } std::memcpy(task_ids[num_returned_task_ids++].data(), current_task_id.data(), TASK_ID_SIZE); } auto* task_list_prop = std::get_if>(¤t_props.properties[PropertyID::TaskIdList]); if (task_list_prop && task_list_prop->size() == TASKIDLIST_SIZE) { std::memcpy(task_list_prop->data(), task_ids.data(), TASKIDLIST_SIZE); } } FileSys::Path OnlineService::GetBossDataDir() { const u32 high = static_cast(extdata_id >> 32); const u32 low = static_cast(extdata_id & 0xFFFFFFFF); return FileSys::ConstructExtDataBinaryPath(1, high, low); } std::unique_ptr OnlineService::OpenBossExtData() { FileSys::ArchiveFactory_ExtSaveData boss_extdata_archive_factory( FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir), FileSys::ExtSaveDataType::Boss); const FileSys::Path boss_path{GetBossDataDir()}; auto archive_result = boss_extdata_archive_factory.Open(boss_path, 0); if (!archive_result.Succeeded()) { LOG_WARNING(Service_BOSS, "Failed to open SpotPass ext data archive with ID '{:#010x}'.", extdata_id); return nullptr; } return std::move(archive_result).Unwrap(); } std::vector OnlineService::GetBossExtDataFiles( FileSys::ArchiveBackend* boss_archive) { auto dir_result = boss_archive->OpenDirectory("/"); if (!dir_result.Succeeded()) { LOG_WARNING(Service_BOSS, "Failed to open root directory of SpotPass ext data with ID '{:#010x}'.", extdata_id); return {}; } auto dir = std::move(dir_result).Unwrap(); // Keep reading the directory 32 files at a time until all files have been checked std::vector boss_files; constexpr u32 files_to_read = 32; u32 entry_count = 0; std::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 (entry_count > files_to_read); // Resize to trim off unused entries from the final read. boss_files.resize((i - 1) * files_to_read + entry_count); return boss_files; } std::vector OnlineService::GetNsDataEntries() { auto boss_archive = OpenBossExtData(); if (!boss_archive) { return {}; } std::vector ns_data; const auto boss_files = GetBossExtDataFiles(boss_archive.get()); for (const auto& current_file : boss_files) { constexpr u32 boss_header_length = 0x34; if (current_file.is_directory || current_file.file_size < boss_header_length) { LOG_WARNING(Service_BOSS, "SpotPass extdata contains directory or file is too short: '{}'", Common::UTF16ToUTF8(current_file.filename)); continue; } FileSys::Mode mode{}; mode.read_flag.Assign(1); NsDataEntry entry{.filename = Common::UTF16ToUTF8(current_file.filename)}; auto file_result = boss_archive->OpenFile("/" + entry.filename, mode); if (!file_result.Succeeded()) { LOG_WARNING(Service_BOSS, "Opening SpotPass file failed."); continue; } auto file = std::move(file_result).Unwrap(); file->Read(0, boss_header_length, reinterpret_cast(&entry.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, static_cast(entry.header.program_id)); continue; } // Check the payload size is correct, excluding header if (entry.header.payload_size != (current_file.file_size - boss_header_length)) { LOG_WARNING(Service_BOSS, "Mismatched file size, was expecting {:#010x}, found {:#010x}", static_cast(entry.header.payload_size), current_file.file_size - boss_header_length); continue; } ns_data.push_back(entry); } return ns_data; } u16 OnlineService::GetNsDataIdList(const u32 filter, const u32 max_entries, Kernel::MappedBuffer& buffer) { std::vector ns_data = GetNsDataEntries(); std::vector output_entries; for (const auto& current_entry : ns_data) { const u32 datatype_raw = static_cast(current_entry.header.datatype); const u16 datatype_high = static_cast(datatype_raw >> 16); const u16 datatype_low = static_cast(datatype_raw & 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)) { continue; } if (output_entries.size() >= max_entries) { LOG_WARNING(Service_BOSS, "Reached maximum number of entries"); break; } output_entries.push_back(current_entry.header.ns_data_id); } buffer.Write(output_entries.data(), 0, sizeof(u32) * output_entries.size()); return static_cast(output_entries.size()); } std::optional OnlineService::GetNsDataEntryFromId(const 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; } Result OnlineService::GetNsDataHeaderInfo(const u32 ns_data_id, const NsDataHeaderInfoType type, const u32 size, Kernel::MappedBuffer& buffer) { const auto entry = GetNsDataEntryFromId(ns_data_id); if (!entry.has_value()) { LOG_WARNING(Service_BOSS, "Failed to find NsData entry for ID {:#010X}", ns_data_id); return {ErrCodes::NsDataNotFound, ErrorModule::BOSS, ErrorSummary::InvalidState, ErrorLevel::Status}; } static constexpr std::array EXPECTED_NS_DATA_HEADER_INFO_SIZES = { sizeof(u64), // Program ID sizeof(u32), // Unknown sizeof(u32), // Data Type sizeof(u32), // Payload Size sizeof(u32), // NsData ID sizeof(u32), // Version sizeof(NsDataHeaderInfo), // Everything }; if (size != EXPECTED_NS_DATA_HEADER_INFO_SIZES[static_cast(type)]) { LOG_WARNING(Service_BOSS, "Invalid size {} for type {}", size, type); // TODO: Proper error code. return ResultUnknown; } switch (type) { case NsDataHeaderInfoType::ProgramId: buffer.Write(&entry->header.program_id, 0, size); return ResultSuccess; case NsDataHeaderInfoType::Unknown: { // TODO: Figure out what this is. Stubbed to zero for now. const u32 zero = 0; buffer.Write(&zero, 0, size); return ResultSuccess; } case NsDataHeaderInfoType::Datatype: buffer.Write(&entry->header.datatype, 0, size); return ResultSuccess; case NsDataHeaderInfoType::PayloadSize: buffer.Write(&entry->header.payload_size, 0, size); return ResultSuccess; case NsDataHeaderInfoType::NsDataId: buffer.Write(&entry->header.ns_data_id, 0, size); return ResultSuccess; case NsDataHeaderInfoType::Version: buffer.Write(&entry->header.version, 0, size); return ResultSuccess; case NsDataHeaderInfoType::Everything: { const NsDataHeaderInfo info = { .program_id = entry->header.program_id, .datatype = entry->header.datatype, .payload_size = entry->header.payload_size, .ns_data_id = entry->header.ns_data_id, .version = entry->header.version, }; buffer.Write(&info, 0, size); return ResultSuccess; } default: LOG_WARNING(Service_BOSS, "Unknown header info type {}", type); // TODO: Proper error code. return ResultUnknown; } } ResultVal OnlineService::ReadNsData(const u32 ns_data_id, const u64 offset, const u32 size, Kernel::MappedBuffer& buffer) { std::optional entry = GetNsDataEntryFromId(ns_data_id); if (!entry.has_value()) { LOG_WARNING(Service_BOSS, "Failed to find NsData entry for ID {:#010X}", ns_data_id); return Result(ErrCodes::NsDataNotFound, ErrorModule::BOSS, ErrorSummary::InvalidState, ErrorLevel::Status); } if (entry->header.payload_size < size + offset) { LOG_WARNING(Service_BOSS, "Invalid request to read {:#010X} bytes at offset {:#010X}, payload " "length is {:#010X}", size, offset, static_cast(entry->header.payload_size)); // TODO: Proper error code. return ResultUnknown; } auto boss_archive = OpenBossExtData(); if (!boss_archive) { // TODO: Proper error code. return ResultUnknown; } 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, "Failed to open SpotPass extdata file '{}'.", entry->filename); // TODO: Proper error code. return ResultUnknown; } auto file = std::move(file_result).Unwrap(); std::vector ns_data_array(size); auto read_result = file->Read(sizeof(BossHeader) + offset, size, ns_data_array.data()); if (!read_result.Succeeded()) { LOG_WARNING(Service_BOSS, "Failed to read SpotPass extdata file '{}'.", entry->filename); // TODO: Proper error code. return ResultUnknown; } buffer.Write(ns_data_array.data(), 0, size); return read_result; } template struct overload : Ts... { using Ts::operator()...; }; template overload(Ts...) -> overload; Result OnlineService::SendProperty(const u16 id, const u32 size, Kernel::MappedBuffer& buffer) { const auto property_id = static_cast(id); if (!current_props.properties.contains(property_id)) { LOG_ERROR(Service_BOSS, "Unknown property with ID {:#06x} and size {}", property_id, size); return Result(ErrCodes::UnknownPropertyID, ErrorModule::BOSS, ErrorSummary::Internal, ErrorLevel::Status); } auto& prop = current_props.properties[property_id]; auto read_pod = [&](T& old_prop) { static_assert(std::is_trivial::value, "Only trivial types are allowed for read_pod"); if (size != sizeof(old_prop)) { LOG_ERROR(Service_BOSS, "Unexpected size of property {:#06x}, was expecting {}, got {}", property_id, sizeof(old_prop), size); } T cur_prop = 0; buffer.Read(&cur_prop, 0, size); prop = cur_prop; }; auto read_vector = [&](std::vector& old_prop) { if (size != old_prop.size()) { LOG_ERROR(Service_BOSS, "Unexpected size of property {:#06x}, was expecting {}, got {}", property_id, old_prop.size(), size); } std::vector cur_prop(size); buffer.Read(cur_prop.data(), 0, size); prop = cur_prop; }; std::visit(overload{[&](u8& cur_prop) { read_pod(cur_prop); }, [&](u16& cur_prop) { read_pod(cur_prop); }, [&](u32& cur_prop) { read_pod(cur_prop); }, [&](u64& cur_prop) { read_pod(cur_prop); }, [&](std::vector& cur_prop) { read_vector(cur_prop); }, [&](std::vector& cur_prop) { read_vector(cur_prop); }}, prop); return ResultSuccess; } Result OnlineService::ReceiveProperty(const u16 id, const u32 size, Kernel::MappedBuffer& buffer) { const auto property_id = static_cast(id); if (!current_props.properties.contains(property_id)) { LOG_ERROR(Service_BOSS, "Unknown property with ID {:#06x} and size {}", property_id, size); return {ErrCodes::UnknownPropertyID, ErrorModule::BOSS, ErrorSummary::Internal, ErrorLevel::Status}; } auto write_pod = [&](T& cur_prop) { static_assert(std::is_trivial::value, "Only trivial types are allowed for write_pod"); if (size != sizeof(cur_prop)) { LOG_ERROR(Service_BOSS, "Unexpected size of property {:#06x}, was expecting {}, got {}", property_id, sizeof(cur_prop), size); } buffer.Write(&cur_prop, 0, size); }; auto write_vector = [&](std::vector& cur_prop) { if (size != cur_prop.size()) { LOG_ERROR(Service_BOSS, "Unexpected size of property {:#06x}, was expecting {}, got {}", property_id, cur_prop.size(), size); } buffer.Write(cur_prop.data(), 0, size); }; auto& prop = current_props.properties[property_id]; std::visit(overload{[&](u8& cur_prop) { write_pod(cur_prop); }, [&](u16& cur_prop) { write_pod(cur_prop); }, [&](u32& cur_prop) { write_pod(cur_prop); }, [&](u64& cur_prop) { write_pod(cur_prop); }, [&](std::vector& cur_prop) { write_vector(cur_prop); }, [&](std::vector& cur_prop) { write_vector(cur_prop); }}, prop); return ResultSuccess; } } // namespace Service::BOSS