diff --git a/src/common/logging/filter.cpp b/src/common/logging/filter.cpp index 2a2b85e84e..85f271d2c1 100644 --- a/src/common/logging/filter.cpp +++ b/src/common/logging/filter.cpp @@ -115,6 +115,7 @@ bool ParseFilterRule(Filter& instance, Iterator begin, Iterator end) { SUB(Service, Y2R) \ SUB(Service, PS) \ SUB(Service, PLGLDR) \ + SUB(Service, NEWS) \ CLS(HW) \ SUB(HW, Memory) \ SUB(HW, LCD) \ diff --git a/src/common/logging/types.h b/src/common/logging/types.h index 8d27c18c2e..346b36eb96 100644 --- a/src/common/logging/types.h +++ b/src/common/logging/types.h @@ -82,6 +82,7 @@ enum class Class : u8 { Service_Y2R, ///< The Y2R (YUV to RGB conversion) service Service_PS, ///< The PS (Process) service Service_PLGLDR, ///< The PLGLDR (plugin loader) service + Service_NEWS, ///< The NEWS (Notifications) service HW, ///< Low-level hardware emulation HW_Memory, ///< Memory-map and address translation HW_LCD, ///< LCD register emulation diff --git a/src/core/hle/service/news/news.cpp b/src/core/hle/service/news/news.cpp index 957f05212f..f1a32d168d 100644 --- a/src/core/hle/service/news/news.cpp +++ b/src/core/hle/service/news/news.cpp @@ -2,18 +2,765 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include +#include +#include "common/archives.h" +#include "common/assert.h" +#include "common/file_util.h" #include "common/logging/log.h" +#include "common/scope_exit.h" +#include "common/string_util.h" #include "core/core.h" +#include "core/file_sys/archive_systemsavedata.h" +#include "core/file_sys/errors.h" +#include "core/file_sys/file_backend.h" +#include "core/hle/ipc_helpers.h" +#include "core/hle/kernel/shared_page.h" +#include "core/hle/result.h" +#include "core/hle/service/fs/fs_user.h" +#include "core/hle/service/news/news.h" #include "core/hle/service/news/news_s.h" #include "core/hle/service/news/news_u.h" #include "core/hle/service/service.h" +SERVICE_CONSTRUCT_IMPL(Service::NEWS::Module) + namespace Service::NEWS { +namespace ErrCodes { +enum { + /// This error is returned if either the NewsDB header or the header for a notification ID is + /// invalid + InvalidHeader = 5, +}; +} + +constexpr Result ErrorInvalidHeader = // 0xC8A12805 + Result(ErrCodes::InvalidHeader, ErrorModule::News, ErrorSummary::InvalidState, + ErrorLevel::Status); + +constexpr std::array news_system_savedata_id{ + 0x00, 0x00, 0x00, 0x00, 0x35, 0x00, 0x01, 0x00, +}; + +template +void Module::serialize(Archive& ar, const unsigned int) { + ar& db; + ar& notification_ids; + ar& automatic_sync_flag; + ar& news_system_save_data_archive; +} +SERIALIZE_IMPL(Module) + +void Module::Interface::AddNotificationImpl(Kernel::HLERequestContext& ctx, bool news_s) { + IPC::RequestParser rp(ctx); + const u32 header_size = rp.Pop(); + const u32 message_size = rp.Pop(); + const u32 image_size = rp.Pop(); + + u32 process_id; + if (!news_s) { + process_id = rp.PopPID(); + LOG_INFO(Service_NEWS, + "called header_size=0x{:x}, message_size=0x{:x}, image_size=0x{:x}, process_id={}", + header_size, message_size, image_size, process_id); + } else { + LOG_INFO(Service_NEWS, "called header_size=0x{:x}, message_size=0x{:x}, image_size=0x{:x}", + header_size, message_size, image_size); + } + + auto header_buffer = rp.PopMappedBuffer(); + auto message_buffer = rp.PopMappedBuffer(); + auto image_buffer = rp.PopMappedBuffer(); + + NotificationHeader header{}; + header_buffer.Read(&header, 0, + std::min(sizeof(NotificationHeader), static_cast(header_size))); + + std::vector message(message_size); + message_buffer.Read(message.data(), 0, message.size()); + + std::vector image(image_size); + image_buffer.Read(image.data(), 0, image.size()); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 6); + SCOPE_EXIT({ + rb.PushMappedBuffer(header_buffer); + rb.PushMappedBuffer(message_buffer); + rb.PushMappedBuffer(image_buffer); + }); + + if (!news_s) { + // Set the program_id using the input process ID + auto fs_user = news->system.ServiceManager().GetService("fs:USER"); + ASSERT_MSG(fs_user != nullptr, "fs:USER service is missing."); + + auto program_info_result = fs_user->GetProgramLaunchInfo(process_id); + if (program_info_result.Failed()) { + rb.Push(program_info_result.Code()); + return; + } + + header.program_id = program_info_result.Unwrap().program_id; + + // The date_time is set by the sysmodule on news:u requests + auto& share_page = news->system.Kernel().GetSharedPageHandler(); + header.date_time = share_page.GetSystemTimeSince2000(); + } + + const auto save_result = news->SaveNotification(&header, header_size, message, image); + if (R_FAILED(save_result)) { + rb.Push(save_result); + return; + } + + // Mark the DB header new notification flag + if ((news->db.header.flags & 1) == 0) { + news->db.header.flags |= 1; + const auto db_result = news->SaveNewsDBSavedata(); + if (R_FAILED(db_result)) { + rb.Push(db_result); + return; + } + } + + rb.Push(ResultSuccess); +} + +void Module::Interface::AddNotification(Kernel::HLERequestContext& ctx) { + AddNotificationImpl(ctx, false); +} + +void Module::Interface::AddNotificationSystem(Kernel::HLERequestContext& ctx) { + AddNotificationImpl(ctx, true); +} + +void Module::Interface::ResetNotifications(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + LOG_INFO(Service_NEWS, "called"); + + // Cleanup the sorted notification IDs + for (u32 i = 0; i < MAX_NOTIFICATIONS; ++i) { + news->notification_ids[i] = i; + } + + const std::string& nand_directory = FileUtil::GetUserPath(FileUtil::UserPath::NANDDir); + FileSys::ArchiveFactory_SystemSaveData systemsavedata_factory(nand_directory); + + FileSys::Path archive_path(news_system_savedata_id); + + // Format the SystemSaveData archive 0x00010035 + systemsavedata_factory.Format(archive_path, FileSys::ArchiveFormatInfo(), 0); + + news->news_system_save_data_archive = systemsavedata_factory.Open(archive_path, 0).Unwrap(); + + // NOTE: The original sysmodule doesn't clear the News DB in memory + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultSuccess); +} + +void Module::Interface::GetTotalNotifications(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + LOG_INFO(Service_NEWS, "called"); + + IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); + rb.Push(ResultSuccess); + rb.Push(static_cast(news->GetTotalNotifications())); +} + +void Module::Interface::SetNewsDBHeader(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + const u32 size = rp.Pop(); + auto input_buffer = rp.PopMappedBuffer(); + + LOG_INFO(Service_NEWS, "called size=0x{:x}", size); + + NewsDBHeader header{}; + input_buffer.Read(&header, 0, std::min(sizeof(NewsDBHeader), static_cast(size))); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); + rb.Push(news->SetNewsDBHeader(&header, size)); + rb.PushMappedBuffer(input_buffer); +} + +void Module::Interface::SetNotificationHeader(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + const u32 notification_index = rp.Pop(); + const u32 size = rp.Pop(); + auto input_buffer = rp.PopMappedBuffer(); + + LOG_INFO(Service_NEWS, "called notification_index={}, size=0x{:x}", notification_index, size); + + NotificationHeader header{}; + input_buffer.Read(&header, 0, + std::min(sizeof(NotificationHeader), static_cast(size))); + + const auto result = news->SetNotificationHeader(notification_index, &header, size); + + // TODO(DaniElectra): If flag_boss == 1, the original sysmodule updates the optout status of the + // source program on SpotPass with the boss:P command 0x00040600C0 (possibly named + // SetOptoutFlagPrivileged?) using the program_id and flag_optout as parameter + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); + rb.Push(result); + rb.PushMappedBuffer(input_buffer); +} + +void Module::Interface::SetNotificationMessage(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + const u32 notification_index = rp.Pop(); + const u32 size = rp.Pop(); + auto input_buffer = rp.PopMappedBuffer(); + + LOG_INFO(Service_NEWS, "called notification_index={}, size=0x{:x}", notification_index, size); + + std::vector data(size); + input_buffer.Read(data.data(), 0, data.size()); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); + rb.Push(news->SetNotificationMessage(notification_index, data)); + rb.PushMappedBuffer(input_buffer); +} + +void Module::Interface::SetNotificationImage(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + const u32 notification_index = rp.Pop(); + const u32 size = rp.Pop(); + auto input_buffer = rp.PopMappedBuffer(); + + LOG_INFO(Service_NEWS, "called notification_index={}, size=0x{:x}", notification_index, size); + + std::vector data(size); + input_buffer.Read(data.data(), 0, data.size()); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); + rb.Push(news->SetNotificationImage(notification_index, data)); + rb.PushMappedBuffer(input_buffer); +} + +void Module::Interface::GetNewsDBHeader(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + const u32 size = rp.Pop(); + auto output_buffer = rp.PopMappedBuffer(); + + LOG_INFO(Service_NEWS, "called size=0x{:x}", size); + + NewsDBHeader header{}; + const auto result = news->GetNewsDBHeader(&header, size); + + IPC::RequestBuilder rb = rp.MakeBuilder(2, 2); + + if (result.Failed()) { + rb.Push(result.Code()); + rb.Push(0); + } else { + const auto copied_size = result.Unwrap(); + output_buffer.Write(&header, 0, copied_size); + + rb.Push(ResultSuccess); + rb.Push(static_cast(copied_size)); + } + + rb.PushMappedBuffer(output_buffer); +} + +void Module::Interface::GetNotificationHeader(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + const u32 notification_index = rp.Pop(); + const u32 size = rp.Pop(); + auto output_buffer = rp.PopMappedBuffer(); + + LOG_DEBUG(Service_NEWS, "called notification_index={}, size=0x{:x}", notification_index, size); + + NotificationHeader header{}; + const auto result = news->GetNotificationHeader(notification_index, &header, size); + + IPC::RequestBuilder rb = rp.MakeBuilder(2, 2); + SCOPE_EXIT({ rb.PushMappedBuffer(output_buffer); }); + + if (result.Failed()) { + rb.Push(result.Code()); + rb.Push(0); + return; + } + + // TODO(DaniElectra): If flag_boss == 1, the original sysmodule updates the optout flag of the + // header with the result of boss:P command 0x0004070080 (possibly named + // GetOptoutFlagPrivileged?) using the program_id as parameter + + const auto copied_size = result.Unwrap(); + output_buffer.Write(&header, 0, copied_size); + + rb.Push(ResultSuccess); + rb.Push(static_cast(copied_size)); +} + +void Module::Interface::GetNotificationMessage(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + const u32 notification_index = rp.Pop(); + const u32 size = rp.Pop(); + auto output_buffer = rp.PopMappedBuffer(); + + LOG_DEBUG(Service_NEWS, "called notification_index={}, size=0x{:x}", notification_index, size); + + std::vector message(size); + const auto result = news->GetNotificationMessage(notification_index, message); + + IPC::RequestBuilder rb = rp.MakeBuilder(2, 2); + SCOPE_EXIT({ rb.PushMappedBuffer(output_buffer); }); + + if (result.Failed()) { + rb.Push(result.Code()); + rb.Push(0); + return; + } + + const auto copied_size = result.Unwrap(); + output_buffer.Write(message.data(), 0, copied_size); + + rb.Push(ResultSuccess); + rb.Push(static_cast(copied_size)); +} + +void Module::Interface::GetNotificationImage(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + const u32 notification_index = rp.Pop(); + const u32 size = rp.Pop(); + auto output_buffer = rp.PopMappedBuffer(); + + LOG_DEBUG(Service_NEWS, "called notification_index={}, size=0x{:x}", notification_index, size); + + std::vector image(size); + const auto result = news->GetNotificationImage(notification_index, image); + + IPC::RequestBuilder rb = rp.MakeBuilder(2, 2); + SCOPE_EXIT({ rb.PushMappedBuffer(output_buffer); }); + + if (result.Failed()) { + rb.Push(result.Code()); + rb.Push(0); + return; + } + + const auto copied_size = result.Unwrap(); + output_buffer.Write(image.data(), 0, copied_size); + + rb.Push(ResultSuccess); + rb.Push(static_cast(copied_size)); +} + +void Module::Interface::SetAutomaticSyncFlag(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + const u8 flag = rp.Pop(); + + LOG_INFO(Service_NEWS, "called flag=0x{:x}", flag); + + news->automatic_sync_flag = flag; + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultSuccess); +} + +void Module::Interface::SetNotificationHeaderOther(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + const u32 notification_index = rp.Pop(); + const u32 size = rp.Pop(); + auto output_buffer = rp.PopMappedBuffer(); + + LOG_INFO(Service_NEWS, "called notification_index={}, size=0x{:x}", notification_index, size); + + NotificationHeader header{}; + output_buffer.Read(&header, 0, + std::min(sizeof(NotificationHeader), static_cast(size))); + + const auto result = news->SetNotificationHeaderOther(notification_index, &header, size); + + // TODO(DaniElectra): If flag_boss == 1, the original sysmodule updates the optout status of the + // source program on SpotPass with the boss:P command 0x00040600C0 (possibly named + // SetOptoutFlagPrivileged?) using the program_id and flag_optout as parameter + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); + rb.Push(result); + rb.PushMappedBuffer(output_buffer); +} + +void Module::Interface::WriteNewsDBSavedata(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + LOG_INFO(Service_NEWS, "called"); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(news->SaveNewsDBSavedata()); +} + +void Module::Interface::GetTotalArrivedNotifications(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + LOG_WARNING(Service_NEWS, "(STUBBED) called"); + + IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); + rb.Push(ResultSuccess); + rb.Push(0); // Total number of pending BOSS notifications to be synced +} + +std::size_t Module::GetTotalNotifications() { + return std::count_if( + notification_ids.begin(), notification_ids.end(), + [this](const u32 notification_id) { return db.notifications[notification_id].IsValid(); }); +} + +ResultVal Module::GetNewsDBHeader(NewsDBHeader* header, const std::size_t size) { + if (!db.header.IsValid()) { + return ErrorInvalidHeader; + } + + const std::size_t copy_size = std::min(sizeof(NewsDBHeader), size); + std::memcpy(header, &db.header, copy_size); + return copy_size; +} + +ResultVal Module::GetNotificationHeader(const u32 notification_index, + NotificationHeader* header, + const std::size_t size) { + if (!db.header.IsValid()) { + return ErrorInvalidHeader; + } + + if (notification_index >= MAX_NOTIFICATIONS) { + return ErrorInvalidHeader; + } + + const u32 notification_id = notification_ids[notification_index]; + if (!db.notifications[notification_id].IsValid()) { + return ErrorInvalidHeader; + } + + const std::size_t copy_size = std::min(sizeof(NotificationHeader), size); + std::memcpy(header, &db.notifications[notification_id], copy_size); + return copy_size; +} + +ResultVal Module::GetNotificationMessage(const u32 notification_index, + std::span message) { + if (!db.header.IsValid()) { + return ErrorInvalidHeader; + } + + if (notification_index >= MAX_NOTIFICATIONS) { + return ErrorInvalidHeader; + } + + const u32 notification_id = notification_ids[notification_index]; + if (!db.notifications[notification_id].IsValid()) { + return ErrorInvalidHeader; + } + + std::string message_file = fmt::format("/news{:03d}.txt", notification_id); + const auto result = LoadFileFromSavedata(message_file, message); + if (result.Failed()) { + return result.Code(); + } + + return result.Unwrap(); +} + +ResultVal Module::GetNotificationImage(const u32 notification_index, + std::span image) { + if (!db.header.IsValid()) { + return ErrorInvalidHeader; + } + + if (notification_index >= MAX_NOTIFICATIONS) { + return ErrorInvalidHeader; + } + + const u32 notification_id = notification_ids[notification_index]; + if (!db.notifications[notification_id].IsValid()) { + return ErrorInvalidHeader; + } + + std::string image_file = fmt::format("/news{:03d}.mpo", notification_id); + const auto result = LoadFileFromSavedata(image_file, image); + if (result.Failed()) { + return result.Code(); + } + + return result.Unwrap(); +} + +Result Module::SetNewsDBHeader(const NewsDBHeader* header, const std::size_t size) { + const std::size_t copy_size = std::min(sizeof(NewsDBHeader), static_cast(size)); + std::memcpy(&db.header, header, copy_size); + return SaveNewsDBSavedata(); +} + +Result Module::SetNotificationHeader(const u32 notification_index, const NotificationHeader* header, + const std::size_t size) { + if (notification_index >= MAX_NOTIFICATIONS) { + return ErrorInvalidHeader; + } + + const u32 notification_id = notification_ids[notification_index]; + const std::size_t copy_size = std::min(sizeof(NotificationHeader), size); + std::memcpy(&db.notifications[notification_id], header, copy_size); + return SaveNewsDBSavedata(); +} + +Result Module::SetNotificationHeaderOther(const u32 notification_index, + const NotificationHeader* header, + const std::size_t size) { + if (notification_index >= MAX_NOTIFICATIONS) { + return ErrorInvalidHeader; + } + + const u32 notification_id = notification_ids[notification_index]; + const std::size_t copy_size = std::min(sizeof(NotificationHeader), size); + std::memcpy(&db.notifications[notification_id], header, copy_size); + return ResultSuccess; +} + +Result Module::SetNotificationMessage(const u32 notification_index, std::span message) { + if (notification_index >= MAX_NOTIFICATIONS) { + return ErrorInvalidHeader; + } + + const u32 notification_id = notification_ids[notification_index]; + const std::string message_file = fmt::format("/news{:03d}.txt", notification_id); + return SaveFileToSavedata(message_file, message); +} + +Result Module::SetNotificationImage(const u32 notification_index, std::span image) { + if (notification_index >= MAX_NOTIFICATIONS) { + return ErrorInvalidHeader; + } + + const u32 notification_id = notification_ids[notification_index]; + const std::string image_file = fmt::format("/news{:03d}.mpo", notification_id); + return SaveFileToSavedata(image_file, image); +} + +Result Module::SaveNotification(const NotificationHeader* header, const std::size_t header_size, + std::span message, std::span image) { + if (!db.header.IsValid()) { + return ErrorInvalidHeader; + } + + if (!header->IsValid()) { + return ErrorInvalidHeader; + } + + u32 notification_count = static_cast(GetTotalNotifications()); + + // If we have reached the limit of 100 notifications, delete the oldest one + if (notification_count >= MAX_NOTIFICATIONS) { + LOG_WARNING(Service_NEWS, + "Notification limit has been reached. Deleting oldest notification ID: {}", + notification_ids[0]); + R_TRY(DeleteNotification(notification_ids[0])); + + notification_count--; + } + + // Check if there is enough space for storing the new notification data. The header is already + // allocated with the News DB + const u64 needed_space = static_cast(message.size() + image.size()); + while (notification_count > 0) { + const u64 free_space = news_system_save_data_archive->GetFreeBytes(); + if (needed_space <= free_space) { + break; + } + + LOG_WARNING(Service_NEWS, "Not enough space available. Deleting oldest notification ID: {}", + notification_ids[0]); + + // If we don't have space, delete old notifications until we do + R_TRY(DeleteNotification(notification_ids[0])); + + notification_count--; + } + + LOG_DEBUG(Service_NEWS, "New notification: notification_id={}, title={}", + notification_ids[notification_count], Common::UTF16BufferToUTF8(header->title)); + + if (!image.empty()) { + R_TRY(SetNotificationImage(notification_count, image)); + } + + if (!message.empty()) { + R_TRY(SetNotificationMessage(notification_count, message)); + } + + R_TRY(SetNotificationHeader(notification_count, header, header_size)); + + // Sort the notifications after saving + std::sort(notification_ids.begin(), notification_ids.end(), + [this](const u32 first_id, const u32 second_id) -> bool { + return CompareNotifications(first_id, second_id); + }); + + return ResultSuccess; +} + +Result Module::DeleteNotification(const u32 notification_id) { + bool deleted = false; + + // Check if the input notification ID exists, and clear it + if (db.notifications[notification_id].IsValid()) { + db.notifications[notification_id] = {}; + + R_TRY(SaveNewsDBSavedata()); + + deleted = true; + } + + // Cleanup images and messages for invalid notifications + for (u32 i = 0; i < MAX_NOTIFICATIONS; ++i) { + if (!db.notifications[i].IsValid()) { + const std::string image_file = fmt::format("/news{:03d}.mpo", i); + auto result = news_system_save_data_archive->DeleteFile(image_file); + if (R_FAILED(result) && result != FileSys::ResultFileNotFound) { + return result; + } + + const std::string message_file = fmt::format("/news{:03d}.txt", i); + result = news_system_save_data_archive->DeleteFile(message_file); + if (R_FAILED(result) && result != FileSys::ResultFileNotFound) { + return result; + } + } + } + + // If the input notification ID was deleted, reorder the notification IDs list + if (deleted) { + std::sort(notification_ids.begin(), notification_ids.end(), + [this](const u32 first_id, const u32 second_id) -> bool { + return CompareNotifications(first_id, second_id); + }); + } + + return ResultSuccess; +} + +Result Module::LoadNewsDBSavedata() { + const std::string& nand_directory = FileUtil::GetUserPath(FileUtil::UserPath::NANDDir); + FileSys::ArchiveFactory_SystemSaveData systemsavedata_factory(nand_directory); + + // Open the SystemSaveData archive 0x00010035 + FileSys::Path archive_path(news_system_savedata_id); + auto archive_result = systemsavedata_factory.Open(archive_path, 0); + + // If the archive didn't exist, create the files inside + if (archive_result.Code() == FileSys::ResultNotFound) { + // Format the archive to create the directories + systemsavedata_factory.Format(archive_path, FileSys::ArchiveFormatInfo(), 0); + + // Open it again to get a valid archive now that the folder exists + news_system_save_data_archive = systemsavedata_factory.Open(archive_path, 0).Unwrap(); + } else { + ASSERT_MSG(archive_result.Succeeded(), "Could not open the NEWS SystemSaveData archive!"); + + news_system_save_data_archive = std::move(archive_result).Unwrap(); + } + + const std::string news_db_file = "/news.db"; + auto news_result = + LoadFileFromSavedata(news_db_file, std::span{reinterpret_cast(&db), sizeof(NewsDB)}); + + // Read the file if it already exists + if (news_result.Failed()) { + // Create the file immediately if it doesn't exist + db.header = {.valid = 1}; + news_result = SaveFileToSavedata( + news_db_file, std::span{reinterpret_cast(&db), sizeof(NewsDB)}); + } else { + // Sort the notifications from the file + std::sort(notification_ids.begin(), notification_ids.end(), + [this](const u32 first_id, const u32 second_id) -> bool { + return CompareNotifications(first_id, second_id); + }); + } + + return news_result.Code(); +} + +Result Module::SaveNewsDBSavedata() { + return SaveFileToSavedata("/news.db", + std::span{reinterpret_cast(&db), sizeof(NewsDB)}); +} + +ResultVal Module::LoadFileFromSavedata(std::string filename, std::span buffer) { + FileSys::Mode mode = {}; + mode.read_flag.Assign(1); + + FileSys::Path path(filename); + + auto result = news_system_save_data_archive->OpenFile(path, mode); + if (result.Failed()) { + return result.Code(); + } + + auto file = std::move(result).Unwrap(); + const auto bytes_read = file->Read(0, buffer.size(), buffer.data()); + file->Close(); + + ASSERT_MSG(bytes_read.Succeeded(), "could not read file"); + + return bytes_read.Unwrap(); +} + +Result Module::SaveFileToSavedata(std::string filename, std::span buffer) { + FileSys::Mode mode = {}; + mode.write_flag.Assign(1); + mode.create_flag.Assign(1); + + FileSys::Path path(filename); + + auto result = news_system_save_data_archive->OpenFile(path, mode); + ASSERT_MSG(result.Succeeded(), "could not open file"); + + auto file = std::move(result).Unwrap(); + file->Write(0, buffer.size(), 1, buffer.data()); + file->Close(); + + return ResultSuccess; +} + +bool Module::CompareNotifications(const u32 first_id, const u32 second_id) { + // Notification IDs are sorted by date time, with valid notifications being first. + // This is done so that other system applications like the News applet can easily + // iterate over the notifications with an incrementing index. + ASSERT(first_id < MAX_NOTIFICATIONS && second_id < MAX_NOTIFICATIONS); + + if (!db.notifications[first_id].IsValid()) { + return false; + } + + if (!db.notifications[second_id].IsValid()) { + return true; + } + + return db.notifications[first_id].date_time < db.notifications[second_id].date_time; +} + +Module::Interface::Interface(std::shared_ptr news, const char* name, u32 max_session) + : ServiceFramework(name, max_session), news(std::move(news)) {} + +Module::Module(Core::System& system_) : system(system_) { + for (u32 i = 0; i < MAX_NOTIFICATIONS; ++i) { + notification_ids[i] = i; + } + + LoadNewsDBSavedata(); +} + void InstallInterfaces(Core::System& system) { auto& service_manager = system.ServiceManager(); - std::make_shared()->InstallAsService(service_manager); - std::make_shared()->InstallAsService(service_manager); + auto news = std::make_shared(system); + std::make_shared(news)->InstallAsService(service_manager); + std::make_shared(news)->InstallAsService(service_manager); } } // namespace Service::NEWS diff --git a/src/core/hle/service/news/news.h b/src/core/hle/service/news/news.h index 98f3b15838..1d23829373 100644 --- a/src/core/hle/service/news/news.h +++ b/src/core/hle/service/news/news.h @@ -4,12 +4,482 @@ #pragma once +#include "core/file_sys/archive_backend.h" +#include "core/hle/service/service.h" + namespace Core { class System; } namespace Service::NEWS { +constexpr u32 MAX_NOTIFICATIONS = 100; + +struct NewsDBHeader { + u8 valid; + u8 flags; + INSERT_PADDING_BYTES(0xE); + + bool IsValid() const { + return valid == 1; + } + +private: + template + void serialize(Archive& ar, const unsigned int) { + ar& valid; + ar& flags; + } + friend class boost::serialization::access; +}; +static_assert(sizeof(NewsDBHeader) == 0x10, "News DB Header structure size is wrong"); + +struct NotificationHeader { + u8 flag_valid; + u8 flag_read; + u8 flag_jpeg; + u8 flag_boss; + u8 flag_optout; + u8 flag_url; + u8 flag_unk0x6; + INSERT_PADDING_BYTES(0x1); + u64_le program_id; + u32_le ns_data_id; // Only used in BOSS notifications + u32_le version; // Only used in BOSS notifications + u64_le jump_param; + INSERT_PADDING_BYTES(0x8); + u64_le date_time; + std::array title; + + bool IsValid() const { + return flag_valid == 1; + } + +private: + template + void serialize(Archive& ar, const unsigned int) { + ar& flag_valid; + ar& flag_read; + ar& flag_jpeg; + ar& flag_boss; + ar& flag_optout; + ar& flag_url; + ar& flag_unk0x6; + ar& program_id; + ar& ns_data_id; + ar& version; + ar& jump_param; + ar& date_time; + ar& title; + } + friend class boost::serialization::access; +}; +static_assert(sizeof(NotificationHeader) == 0x70, "Notification Header structure size is wrong"); + +struct NewsDB { + NewsDBHeader header; + std::array notifications; + +private: + template + void serialize(Archive& ar, const unsigned int) { + ar& header; + ar& notifications; + } + friend class boost::serialization::access; +}; +static_assert(sizeof(NewsDB) == 0x2BD0, "News DB structure size is wrong"); + +class Module final { +public: + explicit Module(Core::System& system_); + ~Module() = default; + + class Interface : public ServiceFramework { + public: + Interface(std::shared_ptr news, const char* name, u32 max_session); + ~Interface() = default; + + private: + void AddNotificationImpl(Kernel::HLERequestContext& ctx, bool news_s); + + protected: + /** + * AddNotification NEWS:U service function. + * Inputs: + * 0 : 0x000100C8 + * 1 : Header size + * 2 : Message size + * 3 : Image size + * 4 : PID Translation Header (0x20) + * 5 : Caller PID + * 6 : Header Buffer Mapping Translation Header ((Size << 4) | 0xA) + * 7 : Header Buffer Pointer + * 8 : Message Buffer Mapping Translation Header ((Size << 4) | 0xA) + * 9 : Message Buffer Pointer + * 10 : Image Buffer Mapping Translation Header ((Size << 4) | 0xA) + * 11 : Image Buffer Pointer + * Outputs: + * 0 : 0x00010046 + * 1 : Result of function, 0 on success, otherwise error code + */ + void AddNotification(Kernel::HLERequestContext& ctx); + + /** + * AddNotification NEWS:S service function. + * Inputs: + * 0 : 0x000100C6 + * 1 : Header size + * 2 : Message size + * 3 : Image size + * 4 : Header Buffer Mapping Translation Header ((Size << 4) | 0xA) + * 5 : Header Buffer Pointer + * 6 : Message Buffer Mapping Translation Header ((Size << 4) | 0xA) + * 7 : Message Buffer Pointer + * 8 : Image Buffer Mapping Translation Header ((Size << 4) | 0xA) + * 9 : Image Buffer Pointer + * Outputs: + * 0 : 0x00010046 + * 1 : Result of function, 0 on success, otherwise error code + */ + void AddNotificationSystem(Kernel::HLERequestContext& ctx); + + /** + * ResetNotifications service function. + * Inputs: + * 0 : 0x00040000 + * Outputs: + * 0 : 0x00040040 + * 1 : Result of function, 0 on success, otherwise error code + */ + void ResetNotifications(Kernel::HLERequestContext& ctx); + + /** + * GetTotalNotifications service function. + * Inputs: + * 0 : 0x00050000 + * Outputs: + * 0 : 0x00050080 + * 1 : Result of function, 0 on success, otherwise error code + * 2 : Number of notifications + */ + void GetTotalNotifications(Kernel::HLERequestContext& ctx); + + /** + * SetNewsDBHeader service function. + * Inputs: + * 0 : 0x00060042 + * 1 : Size + * 2 : Input Buffer Mapping Translation Header ((Size << 4) | 0xA) + * 3 : Input Buffer Pointer + * Outputs: + * 0 : 0x00060042 + * 1 : Result of function, 0 on success, otherwise error code + */ + void SetNewsDBHeader(Kernel::HLERequestContext& ctx); + + /** + * SetNotificationHeader service function. + * Inputs: + * 0 : 0x00070082 + * 1 : Notification index + * 2 : Size + * 3 : Input Buffer Mapping Translation Header ((Size << 4) | 0xA) + * 4 : Input Buffer Pointer + * Outputs: + * 0 : 0x00070042 + * 1 : Result of function, 0 on success, otherwise error code + */ + void SetNotificationHeader(Kernel::HLERequestContext& ctx); + + /** + * SetNotificationMessage service function. + * Inputs: + * 0 : 0x00080082 + * 1 : Notification index + * 2 : Size + * 3 : Input Buffer Mapping Translation Header ((Size << 4) | 0xA) + * 4 : Input Buffer Pointer + * Outputs: + * 0 : 0x00080042 + * 1 : Result of function, 0 on success, otherwise error code + */ + void SetNotificationMessage(Kernel::HLERequestContext& ctx); + + /** + * SetNotificationImage service function. + * Inputs: + * 0 : 0x00090082 + * 1 : Notification index + * 2 : Size + * 3 : Input Buffer Mapping Translation Header ((Size << 4) | 0xA) + * 4 : Input Buffer Pointer + * Outputs: + * 0 : 0x00090042 + * 1 : Result of function, 0 on success, otherwise error code + */ + void SetNotificationImage(Kernel::HLERequestContext& ctx); + + /** + * GetNewsDBHeader service function. + * Inputs: + * 0 : 0x000A0042 + * 1 : Size + * 2 : Output Buffer Mapping Translation Header ((Size << 4) | 0xC) + * 3 : Output Buffer Pointer + * Outputs: + * 0 : 0x000A0082 + * 1 : Result of function, 0 on success, otherwise error code + * 2 : Actual Size + */ + void GetNewsDBHeader(Kernel::HLERequestContext& ctx); + + /** + * GetNotificationHeader service function. + * Inputs: + * 0 : 0x000B0082 + * 1 : Notification index + * 2 : Size + * 3 : Output Buffer Mapping Translation Header ((Size << 4) | 0xC) + * 4 : Output Buffer Pointer + * Outputs: + * 0 : 0x000B0082 + * 1 : Result of function, 0 on success, otherwise error code + * 2 : Actual Size + */ + void GetNotificationHeader(Kernel::HLERequestContext& ctx); + + /** + * GetNotificationMessage service function. + * Inputs: + * 0 : 0x000C0082 + * 1 : Notification index + * 2 : Size + * 3 : Output Buffer Mapping Translation Header ((Size << 4) | 0xC) + * 4 : Output Buffer Pointer + * Outputs: + * 0 : 0x000C0082 + * 1 : Result of function, 0 on success, otherwise error code + * 2 : Actual Size + */ + void GetNotificationMessage(Kernel::HLERequestContext& ctx); + + /** + * GetNotificationImage service function. + * Inputs: + * 0 : 0x000D0082 + * 1 : Notification index + * 2 : Size + * 3 : Output Buffer Mapping Translation Header ((Size << 4) | 0xC) + * 4 : Output Buffer Pointer + * Outputs: + * 0 : 0x000D0082 + * 1 : Result of function, 0 on success, otherwise error code + * 2 : Actual Size + */ + void GetNotificationImage(Kernel::HLERequestContext& ctx); + + /** + * SetAutomaticSyncFlag service function. + * Inputs: + * 0 : 0x00110040 + * 1 : Flag + * Outputs: + * 0 : 0x00110040 + * 1 : Result of function, 0 on success, otherwise error code + */ + void SetAutomaticSyncFlag(Kernel::HLERequestContext& ctx); + + /** + * SetNotificationHeaderOther service function. + * Inputs: + * 0 : 0x00120082 + * 1 : Notification index + * 2 : Size + * 3 : Input Buffer Mapping Translation Header ((Size << 4) | 0xA) + * 4 : Input Buffer Pointer + * Outputs: + * 0 : 0x00120042 + * 1 : Result of function, 0 on success, otherwise error code + */ + void SetNotificationHeaderOther(Kernel::HLERequestContext& ctx); + + /** + * WriteNewsDBSavedata service function. + * Inputs: + * 0 : 0x00130000 + * Outputs: + * 0 : 0x00130040 + * 1 : Result of function, 0 on success, otherwise error code + */ + void WriteNewsDBSavedata(Kernel::HLERequestContext& ctx); + + /** + * GetTotalArrivedNotifications service function. + * Inputs: + * 0 : 0x00140000 + * Outputs: + * 0 : 0x00140080 + * 1 : Result of function, 0 on success, otherwise error code + * 2 : Number of pending notifications to be synced + */ + void GetTotalArrivedNotifications(Kernel::HLERequestContext& ctx); + + protected: + std::shared_ptr news; + }; + +private: + /** + * Gets the total number of notifications + * @returns Number of notifications + */ + std::size_t GetTotalNotifications(); + + /** + * Loads the News DB into the given buffer + * @param header The header buffer + * @param size The size of the header buffer + * @returns Number of bytes read, or error code + */ + ResultVal GetNewsDBHeader(NewsDBHeader* header, const std::size_t size); + + /** + * Loads the header for a notification ID into the given buffer + * @param notification_index The index of the notification ID + * @param header The header buffer + * @param size The size of the header buffer + * @returns Number of bytes read, or error code + */ + ResultVal GetNotificationHeader(const u32 notification_index, + NotificationHeader* header, + const std::size_t size); + + /** + * Opens the message file for a notification ID and loads it to the message buffer + * @param notification_index The index of the notification ID + * @param mesasge The message buffer + * @returns Number of bytes read, or error code + */ + ResultVal GetNotificationMessage(const u32 notification_index, + std::span message); + + /** + * Opens the image file for a notification ID and loads it to the image buffer + * @param notification_index The index of the notification ID + * @param image The image buffer + * @returns Number of bytes read, or error code + */ + ResultVal GetNotificationImage(const u32 notification_index, std::span image); + + /** + * Modifies the header for the News DB in memory and saves the News DB file + * @param header The database header + * @param size The amount of bytes to copy from the header + * @returns Result indicating the result of the operation, 0 on success + */ + Result SetNewsDBHeader(const NewsDBHeader* header, const std::size_t size); + + /** + * Modifies the header for a notification ID on memory and saves the News DB file + * @param notification_index The index of the notification ID + * @param header The notification header + * @param size The amount of bytes to copy from the header + * @returns Result indicating the result of the operation, 0 on success + */ + Result SetNotificationHeader(const u32 notification_index, const NotificationHeader* header, + const std::size_t size); + + /** + * Modifies the header for a notification ID on memory. The News DB file isn't updated + * @param notification_index The index of the notification ID + * @param header The notification header + * @param size The amount of bytes to copy from the header + * @returns Result indicating the result of the operation, 0 on success + */ + Result SetNotificationHeaderOther(const u32 notification_index, + const NotificationHeader* header, const std::size_t size); + + /** + * Sets a given message to a notification ID + * @param notification_index The index of the notification ID + * @param message The notification message + * @returns Result indicating the result of the operation, 0 on success + */ + Result SetNotificationMessage(const u32 notification_index, std::span message); + + /** + * Sets a given image to a notification ID + * @param notification_index The index of the notification ID + * @param image The notification image + * @returns Result indicating the result of the operation, 0 on success + */ + Result SetNotificationImage(const u32 notification_index, std::span image); + + /** + * Creates a new notification with the given data and saves all the contents + * @param header The notification header + * @param header_size The amount of bytes to copy from the header + * @param message The notification message + * @param image The notification image + * @returns Result indicating the result of the operation, 0 on success + */ + Result SaveNotification(const NotificationHeader* header, const std::size_t header_size, + std::span message, std::span image); + + /** + * Deletes the given notification ID from the database + * @param notification_id The notification ID to delete + * @returns Result indicating the result of the operation, 0 on success + */ + Result DeleteNotification(const u32 notification_id); + + /** + * Opens the news.db savedata file and load it to the memory buffer. If the file or the savedata + * don't exist, they are created + * @returns Result indicating the result of the operation, 0 on success + */ + Result LoadNewsDBSavedata(); + + /** + * Writes the news.db savedata file to the the NEWS system savedata + * @returns Result indicating the result of the operation, 0 on success + */ + Result SaveNewsDBSavedata(); + + /** + * Opens the file with the given filename inside the NEWS system savedata + * @param filename The file to open + * @param buffer The buffer to output the contents on + * @returns Number of bytes read, or error code + */ + ResultVal LoadFileFromSavedata(std::string filename, std::span buffer); + + /** + * Writes the file with the given filename inside the NEWS system savedata + * @param filename The output file + * @param buffer The buffer to read the contents from + * @returns Result indicating the result of the operation, 0 on success + */ + Result SaveFileToSavedata(std::string filename, std::span buffer); + + bool CompareNotifications(const u32 first_id, const u32 second_id); + +private: + Core::System& system; + + NewsDB db{}; + std::array notification_ids; // Notifications ordered by date time + u8 automatic_sync_flag; + std::unique_ptr news_system_save_data_archive; + + template + void serialize(Archive& ar, const unsigned int); + friend class boost::serialization::access; +}; void InstallInterfaces(Core::System& system); } // namespace Service::NEWS + +SERVICE_CONSTRUCT(Service::NEWS::Module) +BOOST_CLASS_EXPORT_KEY(Service::NEWS::Module) diff --git a/src/core/hle/service/news/news_s.cpp b/src/core/hle/service/news/news_s.cpp index c8981cc698..e32f190ee6 100644 --- a/src/core/hle/service/news/news_s.cpp +++ b/src/core/hle/service/news/news_s.cpp @@ -3,63 +3,33 @@ // Refer to the license.txt file included. #include "common/archives.h" -#include "core/hle/ipc_helpers.h" #include "core/hle/service/news/news_s.h" SERIALIZE_EXPORT_IMPL(Service::NEWS::NEWS_S) namespace Service::NEWS { -struct NewsDbHeader { - u8 unknown_one; - u8 flags; - INSERT_PADDING_BYTES(0xE); -}; -static_assert(sizeof(NewsDbHeader) == 0x10, "News DB Header structure size is wrong"); - -void NEWS_S::GetTotalNotifications(Kernel::HLERequestContext& ctx) { - IPC::RequestParser rp(ctx); - - LOG_WARNING(Service, "(STUBBED) called"); - - IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); - - rb.Push(ResultSuccess); - rb.Push(0); -} - -void NEWS_S::GetNewsDBHeader(Kernel::HLERequestContext& ctx) { - IPC::RequestParser rp(ctx); - const auto size = rp.Pop(); - auto output_buffer = rp.PopMappedBuffer(); - - LOG_WARNING(Service, "(STUBBED) called size={}", size); - - NewsDbHeader dummy = {.unknown_one = 1, .flags = 0}; - output_buffer.Write(&dummy, 0, std::min(sizeof(NewsDbHeader), static_cast(size))); - - IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); - - rb.Push(ResultSuccess); - rb.Push(size); -} - -NEWS_S::NEWS_S() : ServiceFramework("news:s", 2) { +NEWS_S::NEWS_S(std::shared_ptr news) : Module::Interface(std::move(news), "news:s", 2) { const FunctionInfo functions[] = { // clang-format off - {0x0001, nullptr, "AddNotification"}, + {0x0001, &NEWS_S::AddNotificationSystem, "AddNotification"}, + {0x0004, &NEWS_S::ResetNotifications, "ResetNotifications"}, {0x0005, &NEWS_S::GetTotalNotifications, "GetTotalNotifications"}, - {0x0006, nullptr, "SetNewsDBHeader"}, - {0x0007, nullptr, "SetNotificationHeader"}, - {0x0008, nullptr, "SetNotificationMessage"}, - {0x0009, nullptr, "SetNotificationImage"}, + {0x0006, &NEWS_S::SetNewsDBHeader, "SetNewsDBHeader"}, + {0x0007, &NEWS_S::SetNotificationHeader, "SetNotificationHeader"}, + {0x0008, &NEWS_S::SetNotificationMessage, "SetNotificationMessage"}, + {0x0009, &NEWS_S::SetNotificationImage, "SetNotificationImage"}, {0x000A, &NEWS_S::GetNewsDBHeader, "GetNewsDBHeader"}, - {0x000B, nullptr, "GetNotificationHeader"}, - {0x000C, nullptr, "GetNotificationMessage"}, - {0x000D, nullptr, "GetNotificationImage"}, + {0x000B, &NEWS_S::GetNotificationHeader, "GetNotificationHeader"}, + {0x000C, &NEWS_S::GetNotificationMessage, "GetNotificationMessage"}, + {0x000D, &NEWS_S::GetNotificationImage, "GetNotificationImage"}, {0x000E, nullptr, "SetInfoLEDPattern"}, - {0x0012, nullptr, "GetNotificationHeaderOther"}, - {0x0013, nullptr, "WriteNewsDBSavedata"}, + {0x000F, nullptr, "SyncArrivedNotifications"}, + {0x0010, nullptr, "SyncOneArrivedNotification"}, + {0x0011, &NEWS_S::SetAutomaticSyncFlag, "SetAutomaticSyncFlag"}, + {0x0012, &NEWS_S::SetNotificationHeaderOther, "SetNotificationHeaderOther"}, + {0x0013, &NEWS_S::WriteNewsDBSavedata, "WriteNewsDBSavedata"}, + {0x0014, &NEWS_S::GetTotalArrivedNotifications, "GetTotalArrivedNotifications"}, // clang-format on }; RegisterHandlers(functions); diff --git a/src/core/hle/service/news/news_s.h b/src/core/hle/service/news/news_s.h index a12d496c13..186fc9b0d9 100644 --- a/src/core/hle/service/news/news_s.h +++ b/src/core/hle/service/news/news_s.h @@ -4,44 +4,19 @@ #pragma once -#include -#include "core/hle/service/service.h" +#include "core/hle/service/news/news.h" namespace Service::NEWS { -class NEWS_S final : public ServiceFramework { +class NEWS_S final : public Module::Interface { public: - NEWS_S(); + explicit NEWS_S(std::shared_ptr news); private: - /** - * GetTotalNotifications service function. - * Inputs: - * 0 : 0x00050000 - * Outputs: - * 0 : 0x00050080 - * 1 : Result of function, 0 on success, otherwise error code - * 2 : Number of notifications - */ - void GetTotalNotifications(Kernel::HLERequestContext& ctx); - - /** - * GetNewsDBHeader service function. - * Inputs: - * 0 : 0x000A0042 - * 1 : Size - * 2 : Output Buffer Mapping Translation Header ((Size << 4) | 0xC) - * 3 : Output Buffer Pointer - * Outputs: - * 0 : 0x000A0080 - * 1 : Result of function, 0 on success, otherwise error code - * 2 : Actual Size - */ - void GetNewsDBHeader(Kernel::HLERequestContext& ctx); - - SERVICE_SERIALIZATION_SIMPLE + SERVICE_SERIALIZATION(NEWS_S, news, Module) }; } // namespace Service::NEWS BOOST_CLASS_EXPORT_KEY(Service::NEWS::NEWS_S) +BOOST_SERIALIZATION_CONSTRUCT(Service::NEWS::NEWS_S) diff --git a/src/core/hle/service/news/news_u.cpp b/src/core/hle/service/news/news_u.cpp index 1753b8e189..e39babd309 100644 --- a/src/core/hle/service/news/news_u.cpp +++ b/src/core/hle/service/news/news_u.cpp @@ -9,10 +9,10 @@ SERIALIZE_EXPORT_IMPL(Service::NEWS::NEWS_U) namespace Service::NEWS { -NEWS_U::NEWS_U() : ServiceFramework("news:u", 1) { +NEWS_U::NEWS_U(std::shared_ptr news) : Module::Interface(std::move(news), "news:u", 1) { const FunctionInfo functions[] = { // clang-format off - {0x0001, nullptr, "AddNotification"}, + {0x0001, &NEWS_U::AddNotification, "AddNotification"}, // clang-format on }; RegisterHandlers(functions); diff --git a/src/core/hle/service/news/news_u.h b/src/core/hle/service/news/news_u.h index 8e672256d4..7a23a6fb8e 100644 --- a/src/core/hle/service/news/news_u.h +++ b/src/core/hle/service/news/news_u.h @@ -4,19 +4,19 @@ #pragma once -#include -#include "core/hle/service/service.h" +#include "core/hle/service/news/news.h" namespace Service::NEWS { -class NEWS_U final : public ServiceFramework { +class NEWS_U final : public Module::Interface { public: - NEWS_U(); + explicit NEWS_U(std::shared_ptr news); private: - SERVICE_SERIALIZATION_SIMPLE + SERVICE_SERIALIZATION(NEWS_U, news, Module) }; } // namespace Service::NEWS BOOST_CLASS_EXPORT_KEY(Service::NEWS::NEWS_U) +BOOST_SERIALIZATION_CONSTRUCT(Service::NEWS::NEWS_U)