#include "Cafe/OS/libs/nn_common.h" #include "Cafe/OS/libs/coreinit/coreinit_Time.h" #include "util/helpers/helpers.h" #include "Cafe/Filesystem/fsc.h" #include "Cafe/IOSU/iosu_types_common.h" #include "Cafe/IOSU/nn/iosu_nn_service.h" #include "Cafe/IOSU/legacy/iosu_act.h" #include "Cafe/CafeSystem.h" #include "config/ActiveSettings.h" #include "boss_service.h" #include "boss_common.h" #include #include #include #include #include #include "Cemu/ncrypto/ncrypto.h" #include "Cemu/napi/napi_helper.h" #include "IOSU/legacy/iosu_crypto.h" #include "util/crypto/aes128.h" namespace iosu::boss { using namespace ::nn::boss; static constexpr nnResult RESULT_SUCCESS = 0x200080; static constexpr nnResult RESULT_C0203880 = 0xC0203880; static constexpr nnResult RESULT_NOTEXIST = 0xA021F900; static constexpr nnResult RESULT_STORAGE_NOTEXIST = 0xA025AA00; static constexpr nnResult RESULT_FADENTRY_NOTEXIST = 0xA021FB00; template void AppendHeaderParam(CurlRequestHelper& request, const char* fieldName, const char* format, TArgs&& ... args) { request.addHeaderField(fieldName, fmt::format(fmt::runtime(format), std::forward(args)...).c_str()); } class StorageDatabase // FAD { static constexpr uint32 FAD_ENTRY_MAX_COUNT = 512; // max entries in a single FAD file public: struct BossStorageFadEntry { CafeString<32> fileName; // 0x00 uint32be fileId; // 0x20 uint32 ukn24; // 0x24 uint32 flags; // 0x28 uint32 memo_2C; // 0x2C uint64be entryCreationTimestamp; // flags: // 0x80000000 ReadFlag // 0x40000000 DeleteProtectionFlag // 0x20000000 New arrival flag? }; static_assert(sizeof(BossStorageFadEntry) == 0x38); struct BossStorageFadFile { uint8 _00[0x08]; BossStorageFadEntry entries[FAD_ENTRY_MAX_COUNT]; }; static_assert(sizeof(BossStorageFadFile) == 28680); static bool CheckIfStorageExists(const DirectoryName& bossDirectory, uint64 titleId, uint32 persistentId) { fs::path fadPath = ActiveSettings::GetMlcPath("usr/boss/{:08x}/{:08x}/user/common/{:08x}/{}", GetTitleIdHigh(titleId), GetTitleIdLow(titleId), persistentId, bossDirectory.name2.c_str()); std::error_code ec; return fs::exists(fadPath, ec); } nnResult Load(TaskId& taskId, uint64 titleId, uint32 persistentId) { // todo - if same FAD db already loaded then skip this m_hasValidFadData = false; memset(&m_fadData, 0, sizeof(m_fadData)); fs::path fadPath = ActiveSettings::GetMlcPath("usr/boss/{:08x}/{:08x}/user/common/{:08x}/{}", GetTitleIdHigh(titleId), GetTitleIdLow(titleId), persistentId, taskId.id.c_str()); std::error_code ec; if (!fs::exists(fadPath, ec)) fs::create_directories(fadPath, ec); fadPath /= "fad.db"; m_fadFilePath = fadPath; FileStream* fs = FileStream::openFile2(fadPath); if (!fs) return RESULT_STORAGE_NOTEXIST; fs->readData(&m_fadData, sizeof(m_fadData)); delete fs; m_hasValidFadData = true; // the file may not exist yet, so consider it valid even if we cant open it return RESULT_SUCCESS; } nnResult CreateNewStorage(TaskId& taskId, uint64 titleId, uint32 persistentId) { memset(&m_fadData, 0, sizeof(m_fadData)); fs::path fadPath = ActiveSettings::GetMlcPath("usr/boss/{:08x}/{:08x}/user/common/{:08x}/{}", GetTitleIdHigh(titleId), GetTitleIdLow(titleId), persistentId, taskId.id.c_str()); std::error_code ec; if (!fs::exists(fadPath, ec)) fs::create_directories(fadPath, ec); m_fadFilePath = fadPath / "fad.db"; FileStream* fs = FileStream::createFile2(m_fadFilePath); if (fs) { fs->writeData(&m_fadData, sizeof(m_fadData)); delete fs; m_hasValidFadData = true; } else { cemuLog_log(LogType::Force, "Failed to create FAD data file at {}", _pathToUtf8(m_fadFilePath)); return NN_RESULT_PLACEHOLDER_ERROR; } return RESULT_SUCCESS; } void Store() { if (!m_hasValidFadData) return; FileStream* fs = FileStream::createFile2(m_fadFilePath); if (fs) { fs->writeData(&m_fadData, sizeof(m_fadData)); delete fs; } else { cemuLog_log(LogType::Force, "Failed to store FAD data to {}", m_fadFilePath.string()); } } void Clear() { m_hasValidFadData = false; memset(&m_fadData, 0, sizeof(m_fadData)); } void DeleteEntryByFileName(CafeString<32> fileName) { cemu_assert_debug(m_hasValidFadData); for (sint32 i = 0; i < FAD_ENTRY_MAX_COUNT; i++) { if (m_fadData.entries[i].fileName == fileName) { // shift remaining entries memmove(m_fadData.entries + i, m_fadData.entries + i + 1, (FAD_ENTRY_MAX_COUNT - i - 1) * sizeof(BossStorageFadEntry)); // reset last entry memset(m_fadData.entries + FAD_ENTRY_MAX_COUNT - 1, 0, sizeof(BossStorageFadEntry)); return; } } } nnResult AddEntry(CafeString<32> fileName, uint32 fileId) { if (!fileId) { cemuLog_log(LogType::Force, "Cannot add BOSS file with fileId 0"); return false; } if (fileName.empty()) return false; DeleteEntryByFileName(fileName); cemu_assert_debug(m_hasValidFadData); for (sint32 i=0; i& dataList) { cemu_assert_debug(m_hasValidFadData); dataList.clear(); for (sint32 i = 0; i < FAD_ENTRY_MAX_COUNT; i++) { if (m_fadData.entries[i].fileId == 0) break; // end of list DataName dataName; dataName.name = m_fadData.entries[i].fileName; dataList.push_back(dataName); } } bool GetEntryByFilename(const DataName& fileName, BossStorageFadEntry& outEntry) { if (fileName.name.empty()) return false; cemu_assert_debug(m_hasValidFadData); for (sint32 i = 0; i < FAD_ENTRY_MAX_COUNT; i++) { if (m_fadData.entries[i].fileName == fileName.name) { outEntry = m_fadData.entries[i]; return true; } if (m_fadData.entries[i].fileId == 0) break; // end of list } return false; } private: BossStorageFadFile m_fadData; bool m_hasValidFadData = false; fs::path m_fadFilePath; }; StorageDatabase m_fadDb; class NsDataAccessor { public: nnResult Open(const DirectoryName& bossDirectory, uint64 titleId, uint32 persistentId, DataName& fileName) { m_isValid = false; m_nsDataPath.clear(); TaskId taskId(bossDirectory.name2.c_str()); nnResult r = m_fadDb.Load(taskId, titleId, persistentId); if (NN_RESULT_IS_FAILURE(r)) return r; if (!m_fadDb.GetEntryByFilename(fileName, m_fadEntry)) return RESULT_FADENTRY_NOTEXIST; m_nsDataPath = BuildNsDataPath(bossDirectory, titleId, persistentId, m_fadEntry.fileId); m_isValid = true; m_bossDirectory = bossDirectory; m_titleId = titleId; m_persistentId = persistentId; m_fileName = fileName; return RESULT_SUCCESS; } void Close() { m_isValid = false; } nnResult Delete() { cemu_assert_debug(m_isValid); // only call this after successful Open() std::error_code ec; if (!fs::remove(m_nsDataPath, ec) ) cemuLog_log(LogType::Force, "Failed to delete BOSS file {}", _pathToUtf8(m_nsDataPath)); // remove from FAD TaskId taskId(m_bossDirectory.name2.c_str()); nnResult r = m_fadDb.Load(taskId, m_titleId, m_persistentId); if (NN_RESULT_IS_SUCCESS(r)) m_fadDb.DeleteEntryByFileName(m_fileName.name); m_isValid = false; return RESULT_SUCCESS; } bool Exists() const { if (!m_isValid) return false; // check if the file actually exists on the host filesystem std::error_code ec; if (m_nsDataPath.empty()) return false; bool r = fs::exists(m_nsDataPath, ec); if (!r) cemuLog_log(LogType::Force, "BOSS: File exists in FAD cache but not on disk {}. To fix this reset the SpotPass cache. In the menu under debug select \"Clear SpotPass cache\"", _pathToUtf8(m_nsDataPath)); return r; } nnResult GetSize(uint32& fileSize) const { cemu_assert_debug(m_isValid); // only call this after successful Open() if (!m_isValid) return NN_RESULT_PLACEHOLDER_ERROR; std::unique_ptr fs(FileStream::openFile2(m_nsDataPath)); if (!fs) return NN_RESULT_PLACEHOLDER_ERROR; fileSize = (uint32)fs->GetSize(); return RESULT_SUCCESS; } bool Read(void* buffer, uint32 size, uint32 offset, uint32& bytesRead) { if (!m_isValid) return false; std::unique_ptr fs(FileStream::openFile2(m_nsDataPath)); if (!fs) return false; fs->SetPosition(offset); bytesRead = (uint32)fs->readData(buffer, size); return true; } private: fs::path BuildNsDataPath(const DirectoryName& bossDirectory, uint64 titleId, uint32 persistentId, uint32 dataId) { return ActiveSettings::GetMlcPath("usr/boss/{:08x}/{:08x}/user/common/data/{}/{:08x}", (uint32)(titleId >> 32), (uint32)(titleId & 0xFFFFFFFF), bossDirectory.name2.c_str(), dataId); } bool m_isValid{false}; StorageDatabase::BossStorageFadEntry m_fadEntry; fs::path m_nsDataPath; // if m_isValid is true, these hold the current file information: DirectoryName m_bossDirectory; uint64 m_titleId{}; uint32 m_persistentId{}; DataName m_fileName; }; NsDataAccessor m_nsDataAccessor; class RegisteredTask { public: RegisteredTask(const TaskId& taskId, const TaskSettingCore& taskSettings) : m_taskId(taskId), m_taskSettings(taskSettings) { m_persistentId = iosuAct_getAccountIdOfCurrentAccount(); } nnResult Run() { std::unique_lock _l(m_mutex); cemu_assert_debug(m_taskState == TaskState::Initial || m_taskState == TaskState::Done); m_taskTurnState = TaskTurnState::Ready; m_taskState = TaskState::Ready; return RESULT_SUCCESS; } TaskState GetState() { std::unique_lock _l(m_mutex); return m_taskState; } TaskTurnState GetTurnState() { std::unique_lock _l(m_mutex); return m_taskTurnState; } sint32 GetHttpStatusCode() const { return m_httpStatusCode; } uint32 GetContentLength() const { return m_contentLength; } uint32 GetProcessedLength() const { return m_contentLength; // todo - unlike content length, this value is getting updated as the download happens. But for now we just return the content length } nnResult TaskDoRequest(CURL* curl) { std::unique_lock _l(m_mutex); if (m_taskState != TaskState::Ready) { cemuLog_log(LogType::Force, "Task {} is not ready to run, current state: {}", m_taskId.id.c_str(), static_cast(m_taskState)); return BUILD_NN_RESULT(NN_RESULT_LEVEL_FATAL, NN_RESULT_MODULE_NN_BOSS, 0); } m_taskState = TaskState::Running; m_taskTurnState = TaskTurnState::Running; m_contentLength = 0; _l.unlock(); if (!ActiveSettings::IsOnlineEnabled()) { m_taskState = TaskState::Done; m_taskTurnState = TaskTurnState::DoneError; return BUILD_NN_RESULT(NN_RESULT_LEVEL_SUCCESS, NN_RESULT_MODULE_NN_BOSS, 0); } uint64 titleId = CafeSystem::GetForegroundTitleId(); // construct request URL std::string requestUrl; if (!m_taskSettings.url.empty()) requestUrl.assign(m_taskSettings.url.c_str()); // custom url else { switch (ActiveSettings::GetNetworkService()) { case NetworkService::Pretendo: requestUrl = PretendoURLs::BOSSURL; break; case NetworkService::Custom: requestUrl = GetNetworkConfig().urls.BOSS.GetValue(); break; case NetworkService::Nintendo: default: requestUrl = NintendoURLs::BOSSURL; break; } } char languageCode[8]; switch (GetConfig().console_language) { case CafeConsoleLanguage::JA: strcpy(languageCode, "ja"); break; case CafeConsoleLanguage::EN: strcpy(languageCode, "en"); break; case CafeConsoleLanguage::FR: strcpy(languageCode, "fr"); break; case CafeConsoleLanguage::DE: strcpy(languageCode, "de"); break; case CafeConsoleLanguage::IT: strcpy(languageCode, "it"); break; case CafeConsoleLanguage::ES: strcpy(languageCode, "es"); break; case CafeConsoleLanguage::ZH: strcpy(languageCode, "zh"); break; case CafeConsoleLanguage::KO: strcpy(languageCode, "ko"); break; case CafeConsoleLanguage::NL: strcpy(languageCode, "nl"); break; case CafeConsoleLanguage::PT: strcpy(languageCode, "pt"); break; case CafeConsoleLanguage::RU: strcpy(languageCode, "ru"); break; case CafeConsoleLanguage::TW: strcpy(languageCode, "tw"); // usually zh-tw? break; default: strcpy(languageCode, "en"); break; } const char* countryCode = NCrypto::GetCountryAsString(Account::GetCurrentAccount().GetCountry()); if (m_taskSettings.taskType == TaskType::NbdlDataListTaskSetting) { // use bossCode (and fileName?) as part of the URL requestUrl.append(fmt::format(fmt::runtime("/{}/{}/{}?c={}&l={}"), "1", m_taskSettings.nbdl.bossCode.c_str(), m_taskId.id.c_str(), countryCode, languageCode)); cemu_assert_unimplemented(); cemuLog_logDebug(LogType::Force, "IOSU-BOSS: Unsupported task type: {}", static_cast(m_taskSettings.taskType.value())); _l.lock(); m_taskState = TaskState::Done; m_taskTurnState = TaskTurnState::DoneError; m_contentLength = 0; m_httpStatusCode = 0; return BUILD_NN_RESULT(NN_RESULT_LEVEL_SUCCESS, NN_RESULT_MODULE_NN_BOSS, 0); } else if (m_taskSettings.taskType == TaskType::NbdlTaskSetting) { // todo - language and country code come from the task settings cemu_assert_debug(m_taskSettings.nbdl.fileName.empty()); // todo - changes the url const char* apiVersion = "1"; requestUrl.append(fmt::format(fmt::runtime("/{}/{}/{}?c={}&l={}"), apiVersion, m_taskSettings.nbdl.bossCode.c_str(), m_taskId.id.c_str(), countryCode, languageCode)); } else if (m_taskSettings.taskType == TaskType::PlayReportSetting) { cemuLog_logDebug(LogType::Force, "IOSU-BOSS: PlayReport tasks are not implemented yet"); _l.lock(); m_taskState = TaskState::Done; m_taskTurnState = TaskTurnState::DoneError; m_contentLength = 0; m_httpStatusCode = 200; return BUILD_NN_RESULT(NN_RESULT_LEVEL_SUCCESS, NN_RESULT_MODULE_NN_BOSS, 0); } else { cemuLog_logDebug(LogType::Force, "IOSU-BOSS: Unknown task type: {}", static_cast(m_taskSettings.taskType.value())); _l.lock(); m_taskState = TaskState::Done; m_taskTurnState = TaskTurnState::DoneError; m_contentLength = 0; m_httpStatusCode = 0; return BUILD_NN_RESULT(NN_RESULT_LEVEL_SUCCESS, NN_RESULT_MODULE_NN_BOSS, 0); } CurlRequestHelper request; request.initate(ActiveSettings::GetNetworkService(), requestUrl, CurlRequestHelper::SERVER_SSL_CONTEXT::CUSTOM); // add certs SetupSSL(request); // parameters AppendHeaderParam(request, "X-BOSS-Digest:", ""); // todo - implement this AppendHeaderParam(request, "X-Boss-UniqueId", "{:05x}", ((titleId >> 8) & 0xFFFFF)); AppendHeaderParam(request, "X-BOSS-TitleId", "{:016x}", titleId); if (!m_taskSettings.serviceToken.empty()) AppendHeaderParam(request, "X-Nintendo-ServiceToken", "{}", m_taskSettings.serviceToken.c_str()); if (!request.submitRequest(false)) { cemuLog_log(LogType::Force, fmt::format("Failed BOSS request")); _l.lock(); m_taskState = TaskState::Done; m_taskTurnState = TaskTurnState::DoneError; return BUILD_NN_RESULT(NN_RESULT_LEVEL_FATAL, NN_RESULT_MODULE_NN_BOSS, 0); } sint32 httpStatusCode = request.GetHTTPStatusCode(); m_httpStatusCode = httpStatusCode; m_contentLength = request.getReceivedData().size(); // note - for Nbdl tasks which length is returned? { if (httpStatusCode != 200) { cemuLog_logDebug(LogType::Force, "BOSS task_run: Received unexpected HTTP response code {}", httpStatusCode); cemuLog_logDebug(LogType::Force, "URL: {}", requestUrl); } if (httpStatusCode == 404) { _l.lock(); m_taskState = TaskState::Done; m_taskTurnState = TaskTurnState::DoneError; cemuLog_logDebug(LogType::Force, "BOSS task_run: Received 404 Not Found for URL: {}", requestUrl); return BUILD_NN_RESULT(NN_RESULT_LEVEL_FATAL, NN_RESULT_MODULE_NN_BOSS, 0); } } // for Nbdl tasks get the list of files to download if (m_taskSettings.taskType == TaskType::NbdlTaskSetting) { std::vector queuedFiles = ParseNbdlTasksheet(request.getReceivedData()); cemuLog_log(LogType::Force, "SpotPass - NbdlTask has {} files to download:", queuedFiles.size()); for (const auto& file : queuedFiles) { DownloadNbdlFile(file); } } if (m_taskSettings.taskType == TaskType::RawDlTaskSetting_1 || m_taskSettings.taskType == TaskType::RawDlTaskSetting_3 || m_taskSettings.taskType == TaskType::RawDlTaskSetting_9) { cemu_assert_unimplemented(); } { _l.lock(); m_taskState = TaskState::Done; m_taskTurnState = TaskTurnState::DoneSuccess; } return BUILD_NN_RESULT(NN_RESULT_LEVEL_SUCCESS, NN_RESULT_MODULE_NN_BOSS, 0); } std::recursive_mutex& GetMutex() { return m_mutex; } private: struct NbdlQueuedFile { NbdlQueuedFile(std::string_view url, std::string_view fileName, uint32 dataId, uint32 fileType, uint32 size) : url(url), fileName(fileName), dataId(dataId), fileType(fileType), size(size) {} std::string url; std::string fileName; uint32 dataId; uint32 fileType; // 0 = AppData, 1 = Message, 2 = Unknown uint32 size; }; std::vector ParseNbdlTasksheet(std::span xmlContent) { pugi::xml_document doc; pugi::xml_parse_result result = doc.load_buffer(xmlContent.data(), xmlContent.size()); if (!result) return {}; std::vector fileList; for (pugi::xml_node sheet = doc.child("TaskSheet"); sheet; sheet = sheet.next_sibling("TaskSheet")) { pugi::xml_node serviceStatus = sheet.child("ServiceStatus"); if (strcmp(serviceStatus.child_value(), "close") == 0) { // service is closed, skip all files cemuLog_log(LogType::Force, "BossNbdl: Service is closed, skipping all files in tasksheet"); continue; } else if (strcmp(serviceStatus.child_value(), "open") != 0) { cemuLog_log(LogType::Force, "BossNbdl: Unknown ServiceStatus value: {}", serviceStatus.child_value()); } // read and verify titleId pugi::xml_node nodeTitleId = sheet.child("TitleId"); if (nodeTitleId) { uint64 titleId; TitleIdParser::ParseFromStr(nodeTitleId.child_value(), titleId); if (titleId != CafeSystem::GetForegroundTitleId()) cemuLog_log(LogType::Force, "BossNbdl: TitleId in tasksheet ({:016x}) does not match foreground titleId ({:016x})", titleId, CafeSystem::GetForegroundTitleId()); } else { cemuLog_log(LogType::Force, "BossNbdl: Missing TitleId field in tasksheet"); } // read and verify taskId pugi::xml_node nodeTaskId = sheet.child("TaskId"); if (nodeTaskId) { if (strcmp(nodeTaskId.child_value(), m_taskId.id.c_str()) != 0) cemuLog_log(LogType::Force, "BossNbdl: TaskId in tasksheet ({}) does not match current taskId ({})", nodeTaskId.child_value(), m_taskId.id.c_str()); } else { cemuLog_log(LogType::Force, "BossNbdl: Missing TaskId field in tasksheet"); } // read files list pugi::xml_node files = sheet.child("Files"); if (!files) continue; for (pugi::xml_node file = files.child("File"); file; file = file.next_sibling("File")) { pugi::xml_node fileName = file.child("Filename"); if (!fileName) continue; pugi::xml_node dataId = file.child("DataId"); if (!dataId) continue; pugi::xml_node url = file.child("Url"); if (!url) continue; pugi::xml_node size = file.child("Size"); if (!size) continue; pugi::xml_node type = file.child("Type"); if (!type) continue; uint32 fileType; if (strcmp(type.child_value(), "AppData") == 0) fileType = 0; else if (strcmp(type.child_value(), "Message") == 0) fileType = 1; else { cemuLog_log(LogType::Force, "BossNbdl: Unknown file Type value: {}", type.child_value()); fileType = 0; } if (!file.child("AutoDelete").empty()) cemuLog_log(LogType::Force, "BossNbdl: Field AutoDelete is set but not supported. Value: {}", file.child_value("AutoDelete")); // fields and categories which we dont handle yet: // Attributes, Attribute1, Attribute2, Attribute3 // Notify, Conditions, SecInterval, LED, Played fileList.emplace_back( url.child_value(), fileName.child_value(), dataId.text().as_int(), fileType, size.text().as_int() ); } } return fileList; } void SetupSSL(CurlRequestHelper& request) { request.ClearCaCertIds(); request.ClearClientCertIds(); for (sint32 i=0; i<3; i++) { if (m_taskSettings.internalCaCert[i] != 0) request.AddCaCertId(m_taskSettings.internalCaCert[i]); } request.AddCaCertId(105); if (m_taskSettings.internalClientCert != 0) request.AddCaCertId(m_taskSettings.internalClientCert); else request.AddCaCertId(3); // whats the default for each task type? cemu_assert_debug(m_taskSettings.caCert[0].empty()); cemu_assert_debug(m_taskSettings.caCert[1].empty()); cemu_assert_debug(m_taskSettings.caCert[2].empty()); cemu_assert_debug(m_taskSettings.clientCertName.empty()); } void DownloadNbdlFile(const NbdlQueuedFile& nbdlFile) { uint64 titleId = CafeSystem::GetForegroundTitleId(); // todo - use titleId from task settings? fs::path dataFilePath = ActiveSettings::GetMlcPath("usr/boss/{:08x}/{:08x}/user/common/data/{}/{:08x}", (uint32)(titleId >> 32), (uint32)(titleId & 0xFFFFFFFF), m_taskId.id.c_str(), nbdlFile.dataId); // if the file already exists locally, skip download // but still add the entry to the FAD db std::error_code ec; if (fs::exists(dataFilePath)) { cemuLog_log(LogType::Force, "\t- {} (DataId:{:08x} {}KB) (Skipping, already downloaded)", nbdlFile.fileName, nbdlFile.dataId, (nbdlFile.size+1023)/1024); TrackDownloadedNbdlFile(nbdlFile); return; } cemuLog_log(LogType::Force, "\t- {} (DataId:{:08x} {}KB)", nbdlFile.fileName, nbdlFile.dataId, (nbdlFile.size+1023)/1024); CurlRequestHelper request; request.initate(ActiveSettings::GetNetworkService(), nbdlFile.url, CurlRequestHelper::SERVER_SSL_CONTEXT::CUSTOM); SetupSSL(request); bool r = request.submitRequest(); if (!r) { cemuLog_log(LogType::Force, "Failed to download SpotPass nbdl file: {}", nbdlFile.fileName); return; } // decrypt and store file std::vector receivedData = request.getReceivedData(); struct BossNbdlHeader { /* +0x00 */ uint32be magic; /* +0x04 */ uint32be version; // guessed /* +0x08 */ uint16be ukn08; // must always be 1 /* +0x0A */ uint16be ukn0A; // must always be 2 /* +0x0C */ uint8 nonce[0xC]; /* +0x18 */ uint32 padding18; // unused /* +0x1C */ uint32 padding1C; // unused /* +0x20 */ struct { uint8 uknHashData[0x20]; } encryptedHeader; }; if (receivedData.size() < sizeof(BossNbdlHeader)) { cemuLog_log(LogType::Force, "Received SpotPass nbdl file is too small: {} bytes", receivedData.size()); return; } BossNbdlHeader nbdlHeader; memcpy(&nbdlHeader, receivedData.data(), sizeof(BossNbdlHeader)); if (nbdlHeader.magic != 'boss' || nbdlHeader.version != 0x20001 || nbdlHeader.ukn08 != 1 || nbdlHeader.ukn0A != 2) { cemuLog_log(LogType::Force, "Received SpotPass nbdl file has incorrect header"); return; } // file must be padded to 16 byte alignment for AES decryption (padding is cut off after decryption) size_t originalFileSize = receivedData.size(); const uint32 paddedFileSize = (originalFileSize + 0xF) & (~0xF); receivedData.resize(paddedFileSize); // extra validation if (originalFileSize != nbdlFile.size) { cemuLog_log(LogType::Force, "SpotPass nbdl file size mismatch, expected {} bytes but got {} bytes", nbdlFile.size, originalFileSize); return; } if (nbdlFile.fileName.find("..") != std::string::npos) { cemuLog_log(LogType::Force, "SpotPass nbdl filename contains suspicious characters"); return; } // prepare nonce for AES128-CTR uint8 aesNonce[0x10]; memset(aesNonce, 0, sizeof(aesNonce)); memcpy(aesNonce, nbdlHeader.nonce, 0xC); aesNonce[0xF] = 1; // decrypt uint8 bossAesKey[16] = { 0x39,0x70,0x57,0x35,0x58,0x70,0x34,0x58,0x37,0x41,0x7a,0x30,0x71,0x5a,0x70,0x74 }; memset(aesNonce, 0, sizeof(aesNonce)); memcpy(aesNonce, nbdlHeader.nonce, 0xC); aesNonce[0xF] = 1; AES128CTR_transform(receivedData.data() + offsetof(BossNbdlHeader, encryptedHeader), receivedData.size() - sizeof(BossNbdlHeader::encryptedHeader), bossAesKey, aesNonce); // get HMAC from header uint8 fileHMAC[32]; memcpy(fileHMAC, &((BossNbdlHeader*)receivedData.data())->encryptedHeader.uknHashData, 32); // write decrypted data to filesystem using a temporary path fs::path tmpDataPath = ActiveSettings::GetMlcPath("usr/boss/{:08x}/{:08x}/user/common/data/{}/.{:08x}", (uint32)(titleId >> 32), (uint32)(titleId & 0xFFFFFFFF), m_taskId.id.c_str(), nbdlFile.dataId); if (!fs::exists(tmpDataPath.parent_path())) fs::create_directories(tmpDataPath.parent_path(), ec); FileStream* fs = FileStream::createFile2(tmpDataPath); if (fs) { fs->writeData(receivedData.data() + sizeof(BossNbdlHeader), originalFileSize - sizeof(BossNbdlHeader)); delete fs; } else { cemuLog_log(LogType::Force, "Failed to create temporary SpotPass nbdl file: {}", _pathToUtf8(tmpDataPath)); return; } // to make sure that the file actually stored correctly to disk we read it back and verify the hash auto readbackFileData = FileStream::LoadIntoMemory(tmpDataPath); uint8 hmacHash[32]; unsigned int hmacHashLen = 32; HMAC(EVP_sha256(), "uyr5I8pu4ycq2pOT3D53Bp0n7jK8eyjLO5U20ocUNdN5muwUcC4By881UXECeM08", 64, readbackFileData->data(), originalFileSize - sizeof(BossNbdlHeader), hmacHash, &hmacHashLen); if (memcmp(hmacHash, fileHMAC, 32) != 0) { // failed to verify hash cemuLog_log(LogType::Force, "Failed to verify hash for SpotPass nbdl file: {}", _pathToUtf8(tmpDataPath)); fs::remove(tmpDataPath, ec); return; } // rename temporary file to final path fs::rename(tmpDataPath, dataFilePath, ec); // update FAD entry TrackDownloadedNbdlFile(nbdlFile); } void TrackDownloadedNbdlFile(const NbdlQueuedFile& nbdlFile) { uint64 titleId = CafeSystem::GetForegroundTitleId(); nnResult storageResult = m_fadDb.Load(m_taskId, titleId, m_persistentId); if (storageResult == RESULT_STORAGE_NOTEXIST) storageResult = m_fadDb.CreateNewStorage(m_taskId, titleId, m_persistentId); if (NN_RESULT_IS_FAILURE(storageResult)) { cemuLog_log(LogType::Force, "Failed to create FAD database for task {}: {:08x}", m_taskId.id.c_str(), storageResult); return; } CafeString<32> filenameStr; filenameStr.assign(nbdlFile.fileName); m_fadDb.AddEntry(filenameStr, nbdlFile.dataId); m_fadDb.Store(); // todo - DIDX and ref database } TaskId m_taskId; uint32 m_persistentId; TaskSettingCore m_taskSettings; std::recursive_mutex m_mutex; TaskState m_taskState{ TaskState::Initial }; TaskTurnState m_taskTurnState{ TaskTurnState::Ukn }; sint32 m_httpStatusCode{ 0 }; uint32 m_contentLength{ 0 }; // content length of the last request }; class BossDaemon { public: void Start() { if (m_threadRunning.exchange(true)) return; m_bossDaemonThread = std::thread(&BossDaemon::BossDaemonThread, this); } void Stop() { if (!m_threadRunning.exchange(false)) return; m_bossDaemonThread.join(); } void RegisterTask(const TaskSettingCore& taskSetting) { std::unique_lock _l(m_taskMtx); if (m_registeredTasks.find(taskSetting.taskId) != m_registeredTasks.end()) { // task already registered. Return error? cemuLog_logDebug(LogType::Force, "IOSU-Boss: Task {} is already registered", taskSetting.taskId.id.c_str()); return; } m_registeredTasks.try_emplace(taskSetting.taskId, std::make_shared(taskSetting.taskId, taskSetting)); } void UnregisterTask(uint32 persistentId, const TaskId& taskId) { std::unique_lock _l(m_taskMtx); auto it = m_registeredTasks.find(taskId); if (it != m_registeredTasks.end()) { m_registeredTasks.erase(it); } else { // todo - return error } } bool TaskIsRegistered(uint32 persistentId, const TaskId& taskId) { std::unique_lock _l(m_taskMtx); return m_registeredTasks.find(taskId) != m_registeredTasks.end(); } void TaskRun(uint32 persistentId, const TaskId& taskId) { // todo - there is an extra parameter for controlling background/foreground running? std::shared_ptr registeredTask = GetRegisteredTask2(persistentId, taskId); if (registeredTask) { cemuLog_log(LogType::Force, "IOSU-Boss: Running task {}", taskId.id.c_str()); registeredTask->Run(); } else { cemuLog_log(LogType::Force, "IOSU-Boss: Trying to run task {} which has not been registered", taskId.id.c_str()); // todo - return error -> 0xA021F900 } } void StartScheduleTask(uint32 persistentId, const TaskId& taskId, bool runImmediately) { if (runImmediately) { // proper implementation is still todo // handling automatic scheduling and task state transitions is quite complex // for now we only run the task once std::shared_ptr registeredTask = GetRegisteredTask2(persistentId, taskId); if(!registeredTask) return; TaskState state = registeredTask->GetState(); if (state == TaskState::Stopped || state == TaskState::Initial) { registeredTask->Run(); } else { cemuLog_logDebug(LogType::Force, "StartScheduleTask(): No support for scheduling tasks that aren't in stopped state"); } } else { cemuLog_logDebug(LogType::Force, "IOSU-Boss: Unsupported StartScheduling called for task {}", taskId.id.c_str()); } } void StopScheduleTask(uint32 persistentId, const TaskId& taskId) { // todo // task scheduling is not yet implemented so this does nothing for now } std::shared_ptr GetRegisteredTask2(const uint32 persistentId, const TaskId& taskId) { std::unique_lock _l(m_taskMtx); auto it = m_registeredTasks.find(taskId); // todo - check persistentId as well (make it part of the key?) if (it != m_registeredTasks.end()) return it->second; return nullptr; } private: void BossDaemonThread() { CURL* curl = curl_easy_init(); while ( m_threadRunning ) { // check for tasks to run { std::shared_ptr task = GetNextRunableTask(); if (task) { task->TaskDoRequest(curl); cemu_assert_debug(task->GetState() != TaskState::Ready); } } std::this_thread::sleep_for(std::chrono::milliseconds(100)); } curl_easy_cleanup(curl); } std::shared_ptr GetNextRunableTask() { std::unique_lock _l(m_taskMtx); for (auto& task : m_registeredTasks) { if (task.second->GetState() == TaskState::Ready) return task.second; } return nullptr; } std::thread m_bossDaemonThread; std::atomic_bool m_threadRunning{ false }; // task list std::mutex m_taskMtx; std::map> m_registeredTasks; }s_bossDaemon; class BossMainService : public iosu::nn::IPCService { public: BossMainService() : iosu::nn::IPCService("/dev/boss") {} nnResult ServiceCall(IPCServiceCall& serviceCall) override { if (serviceCall.GetServiceId() == 0) { switch (static_cast(serviceCall.GetCommandId())) { case BossCommandId::TaskIsRegistered: return HandleTaskIsRegistered(serviceCall); case BossCommandId::TaskRegisterA: return HandleTaskRegister(serviceCall, false); case BossCommandId::TaskRegisterForImmediateRunA: return HandleTaskRegister(serviceCall, true); case BossCommandId::TaskUnregister: return HandleTaskUnregister(serviceCall); case BossCommandId::TaskRun: return HandleTaskRun(serviceCall); case BossCommandId::TaskStartScheduling: return HandleTaskStartScheduling(serviceCall); case BossCommandId::TaskStopScheduling: return HandleTaskStopScheduling(serviceCall); case BossCommandId::TaskWaitA: return HandleTaskWait(serviceCall); case BossCommandId::TaskGetTurnState: return HandleTaskGetTurnState(serviceCall); case BossCommandId::TaskGetHttpStatusCodeA: return HandleTaskGetHttpStatusCodeA(serviceCall); case BossCommandId::TaskGetContentLength: return HandleTaskGetContentLength(serviceCall); case BossCommandId::TaskGetProcessedLength: return HandleTaskGetProcessedLength(serviceCall); case BossCommandId::UknA7: return HandleUknA7(serviceCall); case BossCommandId::DeleteDataRelated: return HandleDeleteDataRelated(serviceCall); case BossCommandId::StorageExist: return HandleStorageExist(serviceCall); case BossCommandId::StorageGetDataList: return HandleStorageGetDataList(serviceCall); case BossCommandId::NsDataExist: return HandleNsDataExist(serviceCall); case BossCommandId::NsDataRead: return HandleNsDataRead(serviceCall); case BossCommandId::NsDataGetSize: return HandleNsDataGetSize(serviceCall); case BossCommandId::NsDataDeleteFile: return HandleNsDataDeleteFile(serviceCall, false); case BossCommandId::NsDataDeleteFileWithHistory: return HandleNsDataDeleteFile(serviceCall, true); case BossCommandId::NsFinalize: return HandleNsFinalize(serviceCall); case BossCommandId::TitleSetNewArrivalFlag: return HandleTitleSetNewArrivalFlag(serviceCall); default: cemuLog_log(LogType::Force, "Unsupported service call to /dev/boss"); cemu_assert_unimplemented(); break; } } else { cemu_assert_suspicious(); } return BUILD_NN_RESULT(NN_RESULT_LEVEL_SUCCESS, NN_RESULT_MODULE_NN_BOSS, 0); } // helper function to get proper persistentId uint32 BossGetPersistentId(uint32 persistentId) { if (persistentId != 0) return persistentId; uint32 currentPersistentId; bool r = iosu::act::GetPersistentId(iosu::act::ACT_SLOT_CURRENT, ¤tPersistentId);; if (!r) return 0; return currentPersistentId; } nnResult HandleTaskIsRegistered(IPCServiceCall& serviceCall) { uint32 persistentId = BossGetPersistentId(serviceCall.ReadParameter()); if (!persistentId) return RESULT_C0203880; TaskId taskId = serviceCall.ReadParameter(); bool isRegistered = s_bossDaemon.TaskIsRegistered(persistentId, taskId); // write response serviceCall.WriteResponse(isRegistered ? 1 : 0); return RESULT_SUCCESS; } nnResult HandleTaskRegister(IPCServiceCall& serviceCall, bool forImmediateRun) { uint32 persistentId = BossGetPersistentId(serviceCall.ReadParameter()); if (!persistentId) return RESULT_C0203880; TaskId taskId = serviceCall.ReadParameter(); IPCServiceCall::UnalignedBuffer taskSettingsBuffer = serviceCall.ReadUnalignedInputBufferInfo(); TaskSettingCore taskSetting = taskSettingsBuffer.ReadType(); // bugged.. (but on the submission side) if (taskSetting.persistentId == 0) taskSetting.persistentId = persistentId; taskSetting.taskId = taskId; s_bossDaemon.RegisterTask(taskSetting); return RESULT_SUCCESS; } nnResult HandleTaskUnregister(IPCServiceCall& serviceCall) { uint32 persistentId = BossGetPersistentId(serviceCall.ReadParameter()); if (!persistentId) return RESULT_C0203880; TaskId taskId = serviceCall.ReadParameter(); s_bossDaemon.UnregisterTask(persistentId, taskId); return RESULT_SUCCESS; } nnResult HandleTaskRun(IPCServiceCall& serviceCall) { uint32 persistentId = BossGetPersistentId(serviceCall.ReadParameter()); if (!persistentId) return RESULT_C0203880; TaskId taskId = serviceCall.ReadParameter(); bool isForegroundRun = serviceCall.ReadParameter() != 0; // todo - handle this s_bossDaemon.TaskRun(persistentId, taskId); return RESULT_SUCCESS; } nnResult HandleTaskStartScheduling(IPCServiceCall& serviceCall) { uint32 persistentId = BossGetPersistentId(serviceCall.ReadParameter()); if (!persistentId) return RESULT_C0203880; TaskId taskId = serviceCall.ReadParameter(); uint8 startImmediately = serviceCall.ReadParameter(); s_bossDaemon.StartScheduleTask(persistentId, taskId, startImmediately != 0); return RESULT_SUCCESS; } nnResult HandleTaskStopScheduling(IPCServiceCall& serviceCall) { uint32 persistentId = BossGetPersistentId(serviceCall.ReadParameter()); if (!persistentId) return RESULT_C0203880; TaskId taskId = serviceCall.ReadParameter(); s_bossDaemon.StopScheduleTask(persistentId, taskId); return RESULT_SUCCESS; } nnResult HandleTaskWait(IPCServiceCall& serviceCall) { static_assert(sizeof(betype) == 4); uint32 persistentId = BossGetPersistentId(serviceCall.ReadParameter()); if (!persistentId) return RESULT_C0203880; TaskId taskId = serviceCall.ReadParameter(); TaskWaitState waitState = serviceCall.ReadParameter>(); uint32 timeout = serviceCall.ReadParameter(); std::chrono::steady_clock::time_point endTime; if (timeout != 0) endTime = std::chrono::steady_clock::now() + std::chrono::seconds(timeout); std::shared_ptr registeredTask = s_bossDaemon.GetRegisteredTask2(persistentId, taskId); cemu_assert_debug(registeredTask != nullptr); if (!registeredTask) return RESULT_NOTEXIST; while (true) { // todo - make async std::this_thread::sleep_for(std::chrono::milliseconds(100)); TaskState currentState = registeredTask->GetState(); // does the state match the wait state? if (waitState == TaskWaitState::Done) { if (currentState == TaskState::Done) { serviceCall.WriteResponse(1); // 0 -> timeout, 1 -> no timeout return RESULT_SUCCESS; } } else { cemuLog_log(LogType::Force, "IOSU-Boss: Unknown task wait state {}", static_cast(waitState)); serviceCall.WriteResponse(0); return RESULT_SUCCESS; } // check for timeout if (timeout != 0 && std::chrono::steady_clock::now() >= endTime) { serviceCall.WriteResponse(0); return RESULT_SUCCESS; // which error to return here? } } } nnResult HandleTaskGetTurnState(IPCServiceCall& serviceCall) { static_assert(sizeof(betype) == 4); uint32 persistentId = BossGetPersistentId(serviceCall.ReadParameter()); if (!persistentId) return RESULT_C0203880; TaskId taskId = serviceCall.ReadParameter(); std::shared_ptr registeredTask = s_bossDaemon.GetRegisteredTask2(persistentId, taskId); TaskTurnState turnState = registeredTask ? registeredTask->GetTurnState() : TaskTurnState::Ukn; serviceCall.WriteResponse(1); // execution counter - todo serviceCall.WriteResponse>(turnState); if (!registeredTask) return RESULT_NOTEXIST; return RESULT_SUCCESS; } nnResult HandleTaskGetHttpStatusCodeA(IPCServiceCall& serviceCall) { uint32 persistentId = BossGetPersistentId(serviceCall.ReadParameter()); if (!persistentId) return RESULT_C0203880; TaskId taskId = serviceCall.ReadParameter(); std::shared_ptr registeredTask = s_bossDaemon.GetRegisteredTask2(persistentId, taskId); sint32 httpStatusCode = registeredTask ? registeredTask->GetHttpStatusCode() : 0; serviceCall.WriteResponse(1); // execution counter - todo serviceCall.WriteResponse(httpStatusCode); if (!registeredTask) return RESULT_NOTEXIST; return RESULT_SUCCESS; } nnResult HandleTaskGetContentLength(IPCServiceCall& serviceCall) { uint32 persistentId = BossGetPersistentId(serviceCall.ReadParameter()); if (!persistentId) return RESULT_C0203880; TaskId taskId = serviceCall.ReadParameter(); std::shared_ptr registeredTask = s_bossDaemon.GetRegisteredTask2(persistentId, taskId); uint32 contentLength = registeredTask ? registeredTask->GetContentLength() : 0; serviceCall.WriteResponse(1); // execution counter - todo serviceCall.WriteResponse(contentLength); if (!registeredTask) return RESULT_NOTEXIST; return RESULT_SUCCESS; } nnResult HandleTaskGetProcessedLength(IPCServiceCall& serviceCall) { uint32 persistentId = BossGetPersistentId(serviceCall.ReadParameter()); if (!persistentId) return RESULT_C0203880; TaskId taskId = serviceCall.ReadParameter(); std::shared_ptr registeredTask = s_bossDaemon.GetRegisteredTask2(persistentId, taskId); uint32 processedLength = registeredTask ? registeredTask->GetProcessedLength() : 0; serviceCall.WriteResponse(1); // execution counter - todo serviceCall.WriteResponse(processedLength); // this the actual length? if (!registeredTask) return RESULT_NOTEXIST; return RESULT_SUCCESS; } /* Storage */ nnResult HandleStorageExist(IPCServiceCall& serviceCall) { static_assert(sizeof(DirectoryName) == 8); static_assert(sizeof(betype) == 4); auto persistentId = BossGetPersistentId(serviceCall.ReadParameter()); DirectoryName directoryName = serviceCall.ReadParameter(); StorageKind storageKind = serviceCall.ReadParameter>(); if (!persistentId) return RESULT_C0203880; uint64 titleId = CafeSystem::GetForegroundTitleId(); // todo - investigate how IOSU handles this bool storageExists = true; if (storageKind == StorageKind::StorageNbdl) { // but for now we only check if the fad.db file exists for nbdl storageExists = m_fadDb.CheckIfStorageExists(directoryName, titleId, persistentId); } else { cemuLog_logDebug(LogType::Force, "IOSU-Boss: Unhandled storage kind {}", static_cast(storageKind)); cemu_assert_unimplemented(); } serviceCall.WriteResponse(storageExists ? 1 : 0); return RESULT_SUCCESS; } nnResult HandleStorageGetDataList(IPCServiceCall& serviceCall) { auto persistentId = BossGetPersistentId(serviceCall.ReadParameter()); if (!persistentId) return RESULT_C0203880; DirectoryName directoryName = serviceCall.ReadParameter(); uint32 ukn = serviceCall.ReadParameter(); cemu_assert_debug(ukn == 0); IPCServiceCall::UnalignedBuffer dataListOutputBuffer = serviceCall.ReadUnalignedOutputBufferInfo(); uint32 startOffset = serviceCall.ReadParameter(); // guessed cemu_assert_debug(startOffset == 0); // todo - implement offset size_t dataListSize = dataListOutputBuffer.GetSize() / sizeof(DataName); std::vector dataListTmp; uint64 titleId = CafeSystem::GetForegroundTitleId(); // open FAD database TaskId taskId(directoryName.name2.c_str()); nnResult r = m_fadDb.Load(taskId, titleId, persistentId); if (NN_RESULT_IS_FAILURE(r)) return r; m_fadDb.GetDataList(dataListTmp); if (dataListTmp.size() > dataListSize) dataListTmp.resize(dataListSize); // write to output dataListOutputBuffer.WriteData(dataListTmp.data(), dataListTmp.size() * sizeof(DataName)); serviceCall.WriteResponse((uint32)dataListTmp.size()); serviceCall.WriteResponse(1); // extra byte to indicate success or failure return RESULT_SUCCESS; } /* NsData */ nnResult HandleNsDataExist(IPCServiceCall& serviceCall) { auto persistentId = BossGetPersistentId(serviceCall.ReadParameter()); if (!persistentId) return RESULT_C0203880; DirectoryName directoryName = serviceCall.ReadParameter(); uint32 ukn = serviceCall.ReadParameter(); // storage kind? DataName fileName = serviceCall.ReadParameter(); uint64 titleId = CafeSystem::GetForegroundTitleId(); nnResult r = m_nsDataAccessor.Open(directoryName, titleId, persistentId, fileName); if (NN_RESULT_IS_FAILURE(r)) return r; bool nsDataExists = m_nsDataAccessor.Exists(); serviceCall.WriteResponse(nsDataExists); return RESULT_SUCCESS; } nnResult HandleNsDataRead(IPCServiceCall& serviceCall) { auto persistentId = BossGetPersistentId(serviceCall.ReadParameter()); if (!persistentId) return RESULT_C0203880; DirectoryName directoryName = serviceCall.ReadParameter(); uint32 ukn = serviceCall.ReadParameter(); // storage kind? DataName fileName = serviceCall.ReadParameter(); IPCServiceCall::UnalignedBuffer dataOutputBuffer = serviceCall.ReadUnalignedOutputBufferInfo(); uint64 readOffset = serviceCall.ReadParameter(); uint64 titleId = CafeSystem::GetForegroundTitleId(); nnResult r = m_nsDataAccessor.Open(directoryName, titleId, persistentId, fileName); if (NN_RESULT_IS_FAILURE(r)) { serviceCall.WriteResponse(0); // should we still write the bytes read in case of failure? return r; } uint32 bytesRead = 0; std::vector tmpBuffer; tmpBuffer.resize(dataOutputBuffer.GetSize()); bool rRead = m_nsDataAccessor.Read(tmpBuffer.data(), tmpBuffer.size(), readOffset, bytesRead); dataOutputBuffer.WriteData(tmpBuffer.data(), tmpBuffer.size()); serviceCall.WriteResponse(bytesRead); // todo - handle rRead return RESULT_SUCCESS; } nnResult HandleNsDataGetSize(IPCServiceCall& serviceCall) { auto persistentId = BossGetPersistentId(serviceCall.ReadParameter()); if (!persistentId) return RESULT_C0203880; DirectoryName directoryName = serviceCall.ReadParameter(); uint32 ukn = serviceCall.ReadParameter(); // storage kind? DataName fileName = serviceCall.ReadParameter(); uint64 titleId = CafeSystem::GetForegroundTitleId(); nnResult r = m_nsDataAccessor.Open(directoryName, titleId, persistentId, fileName); if (NN_RESULT_IS_FAILURE(r)) return r; uint32 fileSize = 0; r = m_nsDataAccessor.GetSize(fileSize); serviceCall.WriteResponse(fileSize); return r; } nnResult HandleNsDataDeleteFile(IPCServiceCall& serviceCall, bool withHistory) { auto persistentId = BossGetPersistentId(serviceCall.ReadParameter()); if (!persistentId) return RESULT_C0203880; DirectoryName directoryName = serviceCall.ReadParameter(); uint32 ukn = serviceCall.ReadParameter(); // storage kind? DataName fileName = serviceCall.ReadParameter(); uint64 titleId = CafeSystem::GetForegroundTitleId(); nnResult r = m_nsDataAccessor.Open(directoryName, titleId, persistentId, fileName); if (NN_RESULT_IS_FAILURE(r)) return r; // todo - handle withHistory return m_nsDataAccessor.Delete(); } nnResult HandleNsFinalize(IPCServiceCall& serviceCall) { auto persistentId = BossGetPersistentId(serviceCall.ReadParameter()); if (!persistentId) return RESULT_C0203880; DirectoryName directoryName = serviceCall.ReadParameter(); uint32 ukn = serviceCall.ReadParameter(); // storage kind? DataName fileName = serviceCall.ReadParameter(); m_nsDataAccessor.Close(); return RESULT_SUCCESS; } /* Title */ nnResult HandleTitleSetNewArrivalFlag(IPCServiceCall& serviceCall) { auto persistentId = BossGetPersistentId(serviceCall.ReadParameter()); if (!persistentId) return RESULT_C0203880; uint8 flagValue = serviceCall.ReadParameter(); cemuLog_logDebug(LogType::Force, "IOSU-Boss: Unhandled command TitleSetNewArrivalFlag"); return RESULT_SUCCESS; } /* stubbed opcodes */ nnResult HandleUknA7(IPCServiceCall& serviceCall) { cemuLog_logDebug(LogType::Force, "IOSU-Boss: Unhandled command A7"); // no response data return RESULT_SUCCESS; } nnResult HandleDeleteDataRelated(IPCServiceCall& serviceCall) { cemuLog_logDebug(LogType::Force, "IOSU-Boss: Unhandled delete command"); // no response data return RESULT_SUCCESS; } }; BossMainService s_bossService; class : public ::IOSUModule { void TitleStart() override { s_bossService.Start(); s_bossDaemon.Start(); } void TitleStop() override { s_bossService.Stop(); m_fadDb.Clear(); m_nsDataAccessor.Close(); } }sIOSUModuleNNBOSS; IOSUModule* GetModule() { return static_cast(&sIOSUModuleNNBOSS); } }