committed by
crueter
13 changed files with 2181 additions and 54 deletions
-
11src/core/CMakeLists.txt
-
455src/core/hle/service/bcat/news/builtin_news.cpp
-
27src/core/hle/service/bcat/news/builtin_news.h
-
886src/core/hle/service/bcat/news/msgpack.cpp
-
149src/core/hle/service/bcat/news/msgpack.h
-
116src/core/hle/service/bcat/news/news_data_service.cpp
-
17src/core/hle/service/bcat/news/news_data_service.h
-
186src/core/hle/service/bcat/news/news_database_service.cpp
-
28src/core/hle/service/bcat/news/news_database_service.h
-
76src/core/hle/service/bcat/news/news_service.cpp
-
13src/core/hle/service/bcat/news/news_service.h
-
169src/core/hle/service/bcat/news/news_storage.cpp
-
100src/core/hle/service/bcat/news/news_storage.h
@ -0,0 +1,455 @@ |
|||||
|
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
|
||||
|
#include "core/hle/service/bcat/news/builtin_news.h"
|
||||
|
#include "core/hle/service/bcat/news/msgpack.h"
|
||||
|
#include "core/hle/service/bcat/news/news_storage.h"
|
||||
|
|
||||
|
#include "common/fs/file.h"
|
||||
|
#include "common/fs/fs.h"
|
||||
|
#include "common/fs/path_util.h"
|
||||
|
#include "common/logging/log.h"
|
||||
|
|
||||
|
#include <fmt/format.h>
|
||||
|
#include <httplib.h>
|
||||
|
#include <nlohmann/json.hpp>
|
||||
|
|
||||
|
#include <algorithm>
|
||||
|
#include <chrono>
|
||||
|
#include <filesystem>
|
||||
|
#include <fstream>
|
||||
|
#include <iomanip>
|
||||
|
#include <mutex>
|
||||
|
#include <optional>
|
||||
|
#include <sstream>
|
||||
|
|
||||
|
namespace Service::News { |
||||
|
namespace { |
||||
|
|
||||
|
constexpr const char* GitHubAPI_EdenReleases = "/repos/eden-emulator/Releases/releases"; |
||||
|
|
||||
|
// Cached logo data
|
||||
|
std::vector<u8> g_logo_cache; |
||||
|
bool g_logo_loaded = false; |
||||
|
|
||||
|
std::filesystem::path GetCachePath() { |
||||
|
return Common::FS::GetEdenPath(Common::FS::EdenPath::CacheDir) / "news" / "github_releases.json"; |
||||
|
} |
||||
|
|
||||
|
std::filesystem::path GetLogoPath() { |
||||
|
return Common::FS::GetEdenPath(Common::FS::EdenPath::CacheDir) / "news" / "eden_logo.jpg"; |
||||
|
} |
||||
|
|
||||
|
u32 HashToNewsId(std::string_view key) { |
||||
|
return static_cast<u32>(std::hash<std::string_view>{}(key) & 0x7FFFFFFF); |
||||
|
} |
||||
|
|
||||
|
u64 ParseIsoTimestamp(const std::string& iso) { |
||||
|
if (iso.empty()) return 0; |
||||
|
|
||||
|
std::string buf = iso; |
||||
|
if (buf.back() == 'Z') buf.pop_back(); |
||||
|
|
||||
|
std::tm tm{}; |
||||
|
std::istringstream ss(buf); |
||||
|
ss >> std::get_time(&tm, "%Y-%m-%dT%H:%M:%S"); |
||||
|
if (ss.fail()) return 0; |
||||
|
|
||||
|
#ifdef _WIN32
|
||||
|
return static_cast<u64>(_mkgmtime(&tm)); |
||||
|
#else
|
||||
|
return static_cast<u64>(timegm(&tm)); |
||||
|
#endif
|
||||
|
} |
||||
|
|
||||
|
std::vector<u8> LoadLogo() { |
||||
|
if (g_logo_loaded) { |
||||
|
return g_logo_cache; |
||||
|
} |
||||
|
g_logo_loaded = true; |
||||
|
|
||||
|
const auto logo_path = GetLogoPath(); |
||||
|
|
||||
|
// Try loading from cached disk
|
||||
|
if (std::filesystem::exists(logo_path)) { |
||||
|
std::ifstream f(logo_path, std::ios::binary | std::ios::ate); |
||||
|
if (f) { |
||||
|
const auto file_size = static_cast<std::streamsize>(f.tellg()); |
||||
|
if (file_size > 0 && file_size < 10 * 1024 * 1024) { |
||||
|
f.seekg(0); |
||||
|
g_logo_cache.resize(static_cast<size_t>(file_size)); |
||||
|
if (f.read(reinterpret_cast<char*>(g_logo_cache.data()), file_size)) { |
||||
|
return g_logo_cache; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// yes...i uploaded it to imgur
|
||||
|
try { |
||||
|
httplib::Client cli("https://i.imgur.com"); |
||||
|
cli.set_follow_location(true); |
||||
|
cli.set_connection_timeout(std::chrono::seconds(10)); |
||||
|
cli.set_read_timeout(std::chrono::seconds(30)); |
||||
|
|
||||
|
if (auto res = cli.Get("/1OuqHlk.jpeg"); res && res->status == 200 && !res->body.empty()) { |
||||
|
g_logo_cache.assign(res->body.begin(), res->body.end()); |
||||
|
|
||||
|
std::error_code ec; |
||||
|
std::filesystem::create_directories(logo_path.parent_path(), ec); |
||||
|
if (std::ofstream out(logo_path, std::ios::binary); out) { |
||||
|
out.write(res->body.data(), static_cast<std::streamsize>(res->body.size())); |
||||
|
} |
||||
|
return g_logo_cache; |
||||
|
} |
||||
|
} catch (...) { |
||||
|
LOG_WARNING(Service_BCAT, "failed to download eden logo"); |
||||
|
} |
||||
|
|
||||
|
return {}; |
||||
|
} |
||||
|
|
||||
|
std::optional<std::string> ReadCachedJson() { |
||||
|
const auto path = GetCachePath(); |
||||
|
if (!std::filesystem::exists(path)) return std::nullopt; |
||||
|
|
||||
|
auto content = Common::FS::ReadStringFromFile(path, Common::FS::FileType::TextFile); |
||||
|
return content.empty() ? std::nullopt : std::optional{std::move(content)}; |
||||
|
} |
||||
|
|
||||
|
void WriteCachedJson(std::string_view json) { |
||||
|
const auto path = GetCachePath(); |
||||
|
std::error_code ec; |
||||
|
std::filesystem::create_directories(path.parent_path(), ec); |
||||
|
(void)Common::FS::WriteStringToFile(path, Common::FS::FileType::TextFile, json); |
||||
|
} |
||||
|
|
||||
|
std::optional<std::string> DownloadReleasesJson() { |
||||
|
try { |
||||
|
httplib::SSLClient cli{"api.github.com", 443}; |
||||
|
cli.set_connection_timeout(10); |
||||
|
cli.set_read_timeout(10); |
||||
|
|
||||
|
httplib::Headers headers{ |
||||
|
{"User-Agent", "eden"}, |
||||
|
{"Accept", "application/vnd.github+json"}, |
||||
|
}; |
||||
|
|
||||
|
if (auto res = cli.Get(GitHubAPI_EdenReleases, headers); res && res->status < 400) { |
||||
|
return res->body; |
||||
|
} |
||||
|
} catch (...) { |
||||
|
LOG_WARNING(Service_BCAT, " failed to download releases"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// idk but News App does not render Markdown or HTML, so remove some formatting.
|
||||
|
std::string SanitizeMarkdown(std::string_view markdown) { |
||||
|
std::string result; |
||||
|
result.reserve(markdown.size()); |
||||
|
|
||||
|
// our current structure for markdown is after "# Packages" remove everything.
|
||||
|
std::string text{markdown}; |
||||
|
if (auto pos = text.find("# Packages"); pos != std::string::npos) { |
||||
|
text = text.substr(0, pos); |
||||
|
} |
||||
|
|
||||
|
std::istringstream stream(text); |
||||
|
std::string line; |
||||
|
bool first_line = true; |
||||
|
|
||||
|
while (std::getline(stream, line)) { |
||||
|
if (!line.empty() && line.back() == '\r') { |
||||
|
line.pop_back(); |
||||
|
} |
||||
|
|
||||
|
if (first_line && line.empty()) continue; |
||||
|
|
||||
|
// Remove markdown headers
|
||||
|
size_t start = 0; |
||||
|
while (start < line.size() && line[start] == '#') start++; |
||||
|
if (start > 0 && start < line.size() && line[start] == ' ') { |
||||
|
line = line.substr(start + 1); |
||||
|
} |
||||
|
|
||||
|
// Remove bold/italic marker
|
||||
|
std::string cleaned; |
||||
|
size_t i = 0; |
||||
|
while (i < line.size()) { |
||||
|
if (i + 1 < line.size() && line[i] == '*' && line[i + 1] == '*') { |
||||
|
i += 2; |
||||
|
} else if (line[i] == '*' || line[i] == '_') { |
||||
|
i++; |
||||
|
} else { |
||||
|
cleaned += line[i++]; |
||||
|
} |
||||
|
} |
||||
|
line = cleaned; |
||||
|
|
||||
|
// Remove links and convert it to text
|
||||
|
std::string no_links; |
||||
|
i = 0; |
||||
|
while (i < line.size()) { |
||||
|
if (line[i] == '[') { |
||||
|
auto close = line.find(']', i); |
||||
|
if (close != std::string::npos && close + 1 < line.size() && line[close + 1] == '(') { |
||||
|
no_links += line.substr(i + 1, close - i - 1); |
||||
|
auto paren_close = line.find(')', close + 2); |
||||
|
i = (paren_close != std::string::npos) ? paren_close + 1 : close + 1; |
||||
|
} else { |
||||
|
no_links += line[i++]; |
||||
|
} |
||||
|
} else { |
||||
|
no_links += line[i++]; |
||||
|
} |
||||
|
} |
||||
|
line = no_links; |
||||
|
|
||||
|
// Convert bullet points to something nicer
|
||||
|
if (line.size() >= 2 && line[0] == '-' && line[1] == ' ') { |
||||
|
line = "• " + line.substr(2); |
||||
|
} |
||||
|
|
||||
|
if (!first_line) { |
||||
|
result += '\n'; |
||||
|
} |
||||
|
result += line; |
||||
|
first_line = false; |
||||
|
} |
||||
|
|
||||
|
// Remove excessive newlines, we are heavy char limited...
|
||||
|
while (result.find("\n\n\n") != std::string::npos) { |
||||
|
result.replace(result.find("\n\n\n"), 3, "\n\n"); |
||||
|
} |
||||
|
|
||||
|
// Trim trailing whitespace/newlines
|
||||
|
while (!result.empty() && (result.back() == '\n' || result.back() == ' ')) { |
||||
|
result.pop_back(); |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
std::string FormatBody(const nlohmann::json& release, std::string_view title) { |
||||
|
std::string body = release.value("body", std::string{}); |
||||
|
|
||||
|
if (body.empty()) { |
||||
|
return std::string(title); |
||||
|
} |
||||
|
|
||||
|
// Sanitize markdown
|
||||
|
body = SanitizeMarkdown(body); |
||||
|
|
||||
|
// Limit body length - News app has character limits
|
||||
|
size_t max_body_length = 4000; |
||||
|
if (body.size() > max_body_length) { |
||||
|
size_t cut_pos = body.rfind('\n', max_body_length); |
||||
|
if (cut_pos == std::string::npos || cut_pos < max_body_length / 2) { |
||||
|
cut_pos = body.rfind(". ", max_body_length); |
||||
|
} |
||||
|
if (cut_pos == std::string::npos || cut_pos < max_body_length / 2) { |
||||
|
cut_pos = max_body_length; |
||||
|
} |
||||
|
body = body.substr(0, cut_pos); |
||||
|
|
||||
|
// Trim trailing whitespace
|
||||
|
while (!body.empty() && (body.back() == '\n' || body.back() == ' ')) { |
||||
|
body.pop_back(); |
||||
|
} |
||||
|
|
||||
|
body += "\n\n... View more on GitHub"; |
||||
|
} |
||||
|
|
||||
|
return body; |
||||
|
} |
||||
|
|
||||
|
void ImportReleases(std::string_view json_text) { |
||||
|
nlohmann::json root; |
||||
|
try { |
||||
|
root = nlohmann::json::parse(json_text); |
||||
|
} catch (...) { |
||||
|
LOG_WARNING(Service_BCAT, "failed to parse JSON"); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (!root.is_array()) return; |
||||
|
|
||||
|
for (const auto& rel : root) { |
||||
|
if (!rel.is_object()) continue; |
||||
|
|
||||
|
std::string title = rel.value("name", rel.value("tag_name", std::string{})); |
||||
|
if (title.empty()) continue; |
||||
|
|
||||
|
const u64 release_id = rel.value("id", 0); |
||||
|
const u32 news_id = release_id ? static_cast<u32>(release_id & 0x7FFFFFFF) : HashToNewsId(title); |
||||
|
const u64 published = ParseIsoTimestamp(rel.value("published_at", std::string{})); |
||||
|
const u64 pickup_limit = published + 600000000; |
||||
|
const u32 priority = rel.value("prerelease", false) ? 1500 : 2500; |
||||
|
|
||||
|
std::string author = "eden"; |
||||
|
if (rel.contains("author") && rel["author"].is_object()) { |
||||
|
author = rel["author"].value("login", "eden"); |
||||
|
} |
||||
|
|
||||
|
auto payload = BuildMsgpack(title, FormatBody(rel, title), title, published, |
||||
|
pickup_limit, priority, {"en"}, author, {}, |
||||
|
rel.value("html_url", std::string{}), news_id); |
||||
|
|
||||
|
const std::string news_id_str = fmt::format("LA{:020}", news_id); |
||||
|
|
||||
|
GithubNewsMeta meta{ |
||||
|
.news_id = news_id_str, |
||||
|
.topic_id = "1", |
||||
|
.published_at = published, |
||||
|
.pickup_limit = pickup_limit, |
||||
|
.essential_pickup_limit = pickup_limit, |
||||
|
.expire_at = 0, |
||||
|
.priority = priority, |
||||
|
.deletion_priority = 100, |
||||
|
.decoration_type = 1, |
||||
|
.opted_in = 1, |
||||
|
.essential_pickup_limit_flag = 1, |
||||
|
.category = 0, |
||||
|
.language_mask = 1, |
||||
|
}; |
||||
|
|
||||
|
NewsStorage::Instance().UpsertRaw(meta, std::move(payload)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} // anonymous namespace
|
||||
|
|
||||
|
std::vector<u8> BuildMsgpack(std::string_view title, std::string_view body, |
||||
|
std::string_view topic_name, u64 published_at, |
||||
|
u64 pickup_limit, u32 priority, |
||||
|
const std::vector<std::string>& languages, |
||||
|
const std::string& author, |
||||
|
const std::vector<std::pair<std::string, std::string>>& /*assets*/, |
||||
|
const std::string& html_url, |
||||
|
std::optional<u32> override_id) { |
||||
|
MsgPack::Writer w; |
||||
|
|
||||
|
const u32 news_id = override_id.value_or(HashToNewsId(title.empty() ? "eden" : title)); |
||||
|
const auto logo = LoadLogo(); |
||||
|
|
||||
|
w.WriteFixMap(23); |
||||
|
|
||||
|
// Version infos, could exist a 2?
|
||||
|
w.WriteKey("version"); |
||||
|
w.WriteFixMap(2); |
||||
|
w.WriteKey("format"); |
||||
|
w.WriteUInt(1); |
||||
|
w.WriteKey("semantics"); |
||||
|
w.WriteUInt(1); |
||||
|
|
||||
|
// Metadata
|
||||
|
w.WriteKey("news_id"); |
||||
|
w.WriteUInt(news_id); |
||||
|
w.WriteKey("published_at"); |
||||
|
w.WriteUInt(published_at); |
||||
|
w.WriteKey("pickup_limit"); |
||||
|
w.WriteUInt(pickup_limit); |
||||
|
w.WriteKey("priority"); |
||||
|
w.WriteUInt(priority); |
||||
|
w.WriteKey("deletion_priority"); |
||||
|
w.WriteUInt(100); |
||||
|
|
||||
|
// Language
|
||||
|
w.WriteKey("language"); |
||||
|
w.WriteString(languages.empty() ? "en" : languages.front()); |
||||
|
w.WriteKey("supported_languages"); |
||||
|
w.WriteFixArray(languages.size()); |
||||
|
for (const auto& lang : languages) w.WriteString(lang); |
||||
|
|
||||
|
// Display settings
|
||||
|
w.WriteKey("display_type"); |
||||
|
w.WriteString("NORMAL"); |
||||
|
w.WriteKey("topic_id"); |
||||
|
w.WriteString("eden"); |
||||
|
|
||||
|
w.WriteKey("no_photography"); // still show image
|
||||
|
w.WriteUInt(0); |
||||
|
w.WriteKey("surprise"); // no idea
|
||||
|
w.WriteUInt(0); |
||||
|
w.WriteKey("bashotorya"); // no idea
|
||||
|
w.WriteUInt(0); |
||||
|
w.WriteKey("movie"); |
||||
|
w.WriteUInt(0); // 1 = has video, movie_url must be set but we don't support it yet
|
||||
|
|
||||
|
// News Subject (Title)
|
||||
|
w.WriteKey("subject"); |
||||
|
w.WriteFixMap(2); |
||||
|
w.WriteKey("caption"); |
||||
|
w.WriteUInt(1); |
||||
|
w.WriteKey("text"); |
||||
|
w.WriteString(title.empty() ? "No title" : title); |
||||
|
|
||||
|
// Topic name = who wrote it
|
||||
|
w.WriteKey("topic_name"); |
||||
|
w.WriteString("Eden"); |
||||
|
|
||||
|
w.WriteKey("list_image"); |
||||
|
w.WriteBinary(logo); |
||||
|
|
||||
|
// Footer
|
||||
|
w.WriteKey("footer"); |
||||
|
w.WriteFixMap(1); |
||||
|
w.WriteKey("text"); |
||||
|
w.WriteString(""); |
||||
|
|
||||
|
w.WriteKey("allow_domains"); |
||||
|
w.WriteString("^https?://github.com(/|$)"); |
||||
|
|
||||
|
// More link
|
||||
|
w.WriteKey("more"); |
||||
|
w.WriteFixMap(1); |
||||
|
w.WriteKey("browser"); |
||||
|
w.WriteFixMap(2); |
||||
|
w.WriteKey("url"); |
||||
|
w.WriteString(html_url); |
||||
|
w.WriteKey("text"); |
||||
|
w.WriteString("Open GitHub"); |
||||
|
|
||||
|
// Body
|
||||
|
w.WriteKey("body"); |
||||
|
w.WriteFixMap(4); |
||||
|
w.WriteKey("text"); |
||||
|
w.WriteString(body); |
||||
|
w.WriteKey("main_image_height"); |
||||
|
w.WriteUInt(450); |
||||
|
w.WriteKey("movie_url"); |
||||
|
w.WriteString(""); |
||||
|
w.WriteKey("main_image"); |
||||
|
w.WriteBinary(logo); |
||||
|
|
||||
|
// no clue
|
||||
|
w.WriteKey("contents_descriptors"); |
||||
|
w.WriteString(""); |
||||
|
w.WriteKey("interactive_elements"); |
||||
|
w.WriteString(""); |
||||
|
|
||||
|
return w.Take(); |
||||
|
} |
||||
|
|
||||
|
void EnsureBuiltinNewsLoaded() { |
||||
|
static std::once_flag once; |
||||
|
std::call_once(once, [] { |
||||
|
LoadLogo(); |
||||
|
|
||||
|
if (const auto fresh = DownloadReleasesJson()) { |
||||
|
WriteCachedJson(*fresh); |
||||
|
ImportReleases(*fresh); |
||||
|
LOG_DEBUG(Service_BCAT, ", {} entries, downloaded", NewsStorage::Instance().ListAll().size()); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// Fallback to cached JSON if download failed
|
||||
|
if (const auto cached = ReadCachedJson()) { |
||||
|
ImportReleases(*cached); |
||||
|
LOG_DEBUG(Service_BCAT, ", {} entries, cached", NewsStorage::Instance().ListAll().size()); |
||||
|
return; |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
} // namespace Service::News
|
||||
@ -0,0 +1,27 @@ |
|||||
|
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project |
||||
|
// SPDX-License-Identifier: GPL-3.0-or-later |
||||
|
|
||||
|
#pragma once |
||||
|
|
||||
|
#include "common/common_types.h" |
||||
|
#include <mutex> |
||||
|
#include <optional> |
||||
|
#include <string_view> |
||||
|
#include <vector> |
||||
|
|
||||
|
namespace Service::News { |
||||
|
|
||||
|
void EnsureBuiltinNewsLoaded(); |
||||
|
|
||||
|
std::vector<u8> BuildMsgpack(std::string_view title, std::string_view body, |
||||
|
std::string_view topic_name, |
||||
|
u64 published_at, |
||||
|
u64 pickup_limit, |
||||
|
u32 priority, |
||||
|
const std::vector<std::string>& languages, |
||||
|
const std::string& author_name, |
||||
|
const std::vector<std::pair<std::string, std::string>>& assets, |
||||
|
const std::string& html_url, |
||||
|
std::optional<u32> override_news_id = std::nullopt); |
||||
|
|
||||
|
} // namespace Service::News |
||||
@ -0,0 +1,886 @@ |
|||||
|
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
|
||||
|
#include "core/hle/service/bcat/news/msgpack.h"
|
||||
|
|
||||
|
#include <algorithm>
|
||||
|
#include <cstring>
|
||||
|
|
||||
|
// This file is a partial MsgPack implementation, only implementing the features
|
||||
|
// needed for the News service. Can be extended but enough for the news use case.
|
||||
|
|
||||
|
namespace Service::News { |
||||
|
|
||||
|
void MsgPack::Writer::WriteBytes(std::span<const u8> bytes) { |
||||
|
out.insert(out.end(), bytes.begin(), bytes.end()); |
||||
|
} |
||||
|
|
||||
|
void MsgPack::Writer::WriteFixMap(size_t count) { |
||||
|
if (count <= 15) { |
||||
|
out.push_back(static_cast<u8>(0x80 | count)); |
||||
|
} else if (count <= 0xFFFF) { |
||||
|
out.push_back(0xDE); |
||||
|
out.push_back(static_cast<u8>((count >> 8) & 0xFF)); |
||||
|
out.push_back(static_cast<u8>(count & 0xFF)); |
||||
|
} else { |
||||
|
WriteMap32(count); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void MsgPack::Writer::WriteMap32(size_t count) { |
||||
|
out.push_back(0xDF); |
||||
|
out.push_back(static_cast<u8>((count >> 24) & 0xFF)); |
||||
|
out.push_back(static_cast<u8>((count >> 16) & 0xFF)); |
||||
|
out.push_back(static_cast<u8>((count >> 8) & 0xFF)); |
||||
|
out.push_back(static_cast<u8>(count & 0xFF)); |
||||
|
} |
||||
|
|
||||
|
void MsgPack::Writer::WriteKey(std::string_view s) { |
||||
|
WriteString(s); |
||||
|
} |
||||
|
|
||||
|
void MsgPack::Writer::WriteString(std::string_view s) { |
||||
|
if (s.size() <= 31) { |
||||
|
out.push_back(static_cast<u8>(0xA0 | s.size())); |
||||
|
} else if (s.size() <= 0xFF) { |
||||
|
out.push_back(0xD9); |
||||
|
out.push_back(static_cast<u8>(s.size())); |
||||
|
} else if (s.size() <= 0xFFFF) { |
||||
|
out.push_back(0xDA); |
||||
|
out.push_back(static_cast<u8>((s.size() >> 8) & 0xFF)); |
||||
|
out.push_back(static_cast<u8>(s.size() & 0xFF)); |
||||
|
} else { |
||||
|
out.push_back(0xDB); |
||||
|
out.push_back(static_cast<u8>((s.size() >> 24) & 0xFF)); |
||||
|
out.push_back(static_cast<u8>((s.size() >> 16) & 0xFF)); |
||||
|
out.push_back(static_cast<u8>((s.size() >> 8) & 0xFF)); |
||||
|
out.push_back(static_cast<u8>(s.size() & 0xFF)); |
||||
|
} |
||||
|
WriteBytes({reinterpret_cast<const u8*>(s.data()), s.size()}); |
||||
|
} |
||||
|
|
||||
|
void MsgPack::Writer::WriteInt64(s64 v) { |
||||
|
if (v >= 0) { |
||||
|
WriteUInt(static_cast<u64>(v)); |
||||
|
return; |
||||
|
} |
||||
|
if (v >= -32) { |
||||
|
out.push_back(static_cast<u8>(0xE0 | (v + 32))); |
||||
|
} else if (v >= -128) { |
||||
|
out.push_back(0xD0); |
||||
|
out.push_back(static_cast<u8>(v & 0xFF)); |
||||
|
} else if (v >= -32768) { |
||||
|
out.push_back(0xD1); |
||||
|
out.push_back(static_cast<u8>((v >> 8) & 0xFF)); |
||||
|
out.push_back(static_cast<u8>(v & 0xFF)); |
||||
|
} else if (v >= INT32_MIN) { |
||||
|
out.push_back(0xD2); |
||||
|
out.push_back(static_cast<u8>((v >> 24) & 0xFF)); |
||||
|
out.push_back(static_cast<u8>((v >> 16) & 0xFF)); |
||||
|
out.push_back(static_cast<u8>((v >> 8) & 0xFF)); |
||||
|
out.push_back(static_cast<u8>(v & 0xFF)); |
||||
|
} else { |
||||
|
out.push_back(0xD3); |
||||
|
for (int i = 7; i >= 0; --i) { |
||||
|
out.push_back(static_cast<u8>((static_cast<u64>(v) >> (8 * i)) & 0xFF)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void MsgPack::Writer::WriteUInt(u64 v) { |
||||
|
if (v < 0x80) { |
||||
|
out.push_back(static_cast<u8>(v)); |
||||
|
} else if (v <= 0xFF) { |
||||
|
out.push_back(0xCC); |
||||
|
out.push_back(static_cast<u8>(v)); |
||||
|
} else if (v <= 0xFFFF) { |
||||
|
out.push_back(0xCD); |
||||
|
out.push_back(static_cast<u8>((v >> 8) & 0xFF)); |
||||
|
out.push_back(static_cast<u8>(v & 0xFF)); |
||||
|
} else if (v <= 0xFFFFFFFFULL) { |
||||
|
out.push_back(0xCE); |
||||
|
out.push_back(static_cast<u8>((v >> 24) & 0xFF)); |
||||
|
out.push_back(static_cast<u8>((v >> 16) & 0xFF)); |
||||
|
out.push_back(static_cast<u8>((v >> 8) & 0xFF)); |
||||
|
out.push_back(static_cast<u8>(v & 0xFF)); |
||||
|
} else { |
||||
|
out.push_back(0xCF); |
||||
|
for (int i = 7; i >= 0; --i) { |
||||
|
out.push_back(static_cast<u8>((v >> (8 * i)) & 0xFF)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void MsgPack::Writer::WriteNil() { |
||||
|
out.push_back(0xC0); |
||||
|
} |
||||
|
|
||||
|
void MsgPack::Writer::WriteFixArray(size_t count) { |
||||
|
if (count <= 15) { |
||||
|
out.push_back(static_cast<u8>(0x90 | count)); |
||||
|
} else if (count <= 0xFFFF) { |
||||
|
out.push_back(0xDC); |
||||
|
out.push_back(static_cast<u8>((count >> 8) & 0xFF)); |
||||
|
out.push_back(static_cast<u8>(count & 0xFF)); |
||||
|
} else { |
||||
|
out.push_back(0xDD); |
||||
|
out.push_back(static_cast<u8>((count >> 24) & 0xFF)); |
||||
|
out.push_back(static_cast<u8>((count >> 16) & 0xFF)); |
||||
|
out.push_back(static_cast<u8>((count >> 8) & 0xFF)); |
||||
|
out.push_back(static_cast<u8>(count & 0xFF)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void MsgPack::Writer::WriteBinary(const std::vector<u8>& data) { |
||||
|
if (data.size() <= 0xFF) { |
||||
|
out.push_back(0xC4); |
||||
|
out.push_back(static_cast<u8>(data.size())); |
||||
|
} else if (data.size() <= 0xFFFF) { |
||||
|
out.push_back(0xC5); |
||||
|
out.push_back(static_cast<u8>((data.size() >> 8) & 0xFF)); |
||||
|
out.push_back(static_cast<u8>(data.size() & 0xFF)); |
||||
|
} else { |
||||
|
out.push_back(0xC6); |
||||
|
out.push_back(static_cast<u8>((data.size() >> 24) & 0xFF)); |
||||
|
out.push_back(static_cast<u8>((data.size() >> 16) & 0xFF)); |
||||
|
out.push_back(static_cast<u8>((data.size() >> 8) & 0xFF)); |
||||
|
out.push_back(static_cast<u8>(data.size() & 0xFF)); |
||||
|
} |
||||
|
WriteBytes(data); |
||||
|
} |
||||
|
|
||||
|
void MsgPack::Writer::WriteBool(bool v) { |
||||
|
out.push_back(v ? 0xC3 : 0xC2); |
||||
|
} |
||||
|
|
||||
|
std::vector<u8> MsgPack::Writer::Take() { |
||||
|
return std::move(out); |
||||
|
} |
||||
|
|
||||
|
MsgPack::Reader::Reader(std::span<const u8> buffer) : data(buffer) {} |
||||
|
|
||||
|
bool MsgPack::Reader::Fail(const char* msg) { |
||||
|
error = msg; |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
bool MsgPack::Reader::Ensure(size_t n) const { |
||||
|
return offset + n <= data.size(); |
||||
|
} |
||||
|
|
||||
|
u8 MsgPack::Reader::Peek() const { |
||||
|
return Ensure(1) ? data[offset] : 0; |
||||
|
} |
||||
|
|
||||
|
u8 MsgPack::Reader::ReadByte() { |
||||
|
if (!Ensure(1)) { |
||||
|
return 0; |
||||
|
} |
||||
|
return data[offset++]; |
||||
|
} |
||||
|
|
||||
|
bool MsgPack::Reader::ReadSize(size_t byte_count, size_t& out_size) { |
||||
|
if (!Ensure(byte_count)) { |
||||
|
return Fail("size out of range"); |
||||
|
} |
||||
|
size_t value = 0; |
||||
|
for (size_t i = 0; i < byte_count; ++i) { |
||||
|
value = (value << 8) | data[offset + i]; |
||||
|
} |
||||
|
offset += byte_count; |
||||
|
out_size = value; |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
bool MsgPack::Reader::SkipBytes(size_t n) { |
||||
|
if (!Ensure(n)) { |
||||
|
return Fail("skip out of range"); |
||||
|
} |
||||
|
offset += n; |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
bool MsgPack::Reader::SkipValue() { |
||||
|
if (End()) { |
||||
|
return Fail("unexpected end"); |
||||
|
} |
||||
|
const u8 byte = ReadByte(); |
||||
|
|
||||
|
if (byte <= 0x7F || (byte >= 0xE0)) { |
||||
|
return true; |
||||
|
} |
||||
|
if ((byte & 0xE0) == 0xA0) { |
||||
|
const size_t len = byte & 0x1F; |
||||
|
return SkipBytes(len); |
||||
|
} |
||||
|
if ((byte & 0xF0) == 0x80) { |
||||
|
const size_t count = byte & 0x0F; |
||||
|
return SkipContainer(count * 2, true); |
||||
|
} |
||||
|
if ((byte & 0xF0) == 0x90) { |
||||
|
const size_t count = byte & 0x0F; |
||||
|
return SkipContainer(count, false); |
||||
|
} |
||||
|
|
||||
|
switch (byte) { |
||||
|
case 0xC0: |
||||
|
case 0xC2: |
||||
|
case 0xC3: |
||||
|
return true; |
||||
|
case 0xC4: |
||||
|
case 0xC5: |
||||
|
case 0xC6: { |
||||
|
size_t size = 0; |
||||
|
if (!ReadSize(static_cast<size_t>(1) << (byte - 0xC4), size)) { |
||||
|
return false; |
||||
|
} |
||||
|
return SkipBytes(size); |
||||
|
} |
||||
|
case 0xC7: |
||||
|
case 0xC8: |
||||
|
case 0xC9: |
||||
|
return Fail("ext not supported"); |
||||
|
case 0xCA: |
||||
|
return SkipBytes(4); |
||||
|
case 0xCB: |
||||
|
return SkipBytes(8); |
||||
|
case 0xCC: |
||||
|
return SkipBytes(1); |
||||
|
case 0xCD: |
||||
|
return SkipBytes(2); |
||||
|
case 0xCE: |
||||
|
return SkipBytes(4); |
||||
|
case 0xCF: |
||||
|
return SkipBytes(8); |
||||
|
case 0xD0: |
||||
|
return SkipBytes(1); |
||||
|
case 0xD1: |
||||
|
return SkipBytes(2); |
||||
|
case 0xD2: |
||||
|
return SkipBytes(4); |
||||
|
case 0xD3: |
||||
|
return SkipBytes(8); |
||||
|
case 0xD4: |
||||
|
case 0xD5: |
||||
|
case 0xD6: |
||||
|
case 0xD7: |
||||
|
case 0xD8: |
||||
|
return Fail("fixext not supported"); |
||||
|
case 0xD9: { |
||||
|
size_t size = 0; |
||||
|
if (!ReadSize(1, size)) { |
||||
|
return false; |
||||
|
} |
||||
|
return SkipBytes(size); |
||||
|
} |
||||
|
case 0xDA: { |
||||
|
size_t size = 0; |
||||
|
if (!ReadSize(2, size)) { |
||||
|
return false; |
||||
|
} |
||||
|
return SkipBytes(size); |
||||
|
} |
||||
|
case 0xDB: { |
||||
|
size_t size = 0; |
||||
|
if (!ReadSize(4, size)) { |
||||
|
return false; |
||||
|
} |
||||
|
return SkipBytes(size); |
||||
|
} |
||||
|
case 0xDC: { |
||||
|
size_t count = 0; |
||||
|
if (!ReadSize(2, count)) { |
||||
|
return false; |
||||
|
} |
||||
|
return SkipContainer(count, false); |
||||
|
} |
||||
|
case 0xDD: { |
||||
|
size_t count = 0; |
||||
|
if (!ReadSize(4, count)) { |
||||
|
return false; |
||||
|
} |
||||
|
return SkipContainer(count, false); |
||||
|
} |
||||
|
case 0xDE: { |
||||
|
size_t count = 0; |
||||
|
if (!ReadSize(2, count)) { |
||||
|
return false; |
||||
|
} |
||||
|
return SkipContainer(count * 2, true); |
||||
|
} |
||||
|
case 0xDF: { |
||||
|
size_t count = 0; |
||||
|
if (!ReadSize(4, count)) { |
||||
|
return false; |
||||
|
} |
||||
|
return SkipContainer(count * 2, true); |
||||
|
} |
||||
|
default: |
||||
|
return Fail("unknown type"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
bool MsgPack::Reader::SkipContainer(size_t count, bool /*map_mode*/) { |
||||
|
for (size_t i = 0; i < count; ++i) { |
||||
|
if (!SkipValue()) { |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
bool MsgPack::Reader::SkipAll() { |
||||
|
while (!End()) { |
||||
|
if (!SkipValue()) { |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
bool MsgPack::Reader::ReadMapHeader(size_t& count) { |
||||
|
if (End()) { |
||||
|
return Fail("unexpected end"); |
||||
|
} |
||||
|
const u8 byte = Peek(); |
||||
|
if ((byte & 0xF0) == 0x80) { |
||||
|
count = byte & 0x0F; |
||||
|
offset++; |
||||
|
return true; |
||||
|
} |
||||
|
if (byte == 0xDE) { |
||||
|
ReadByte(); |
||||
|
return ReadSize(2, count); |
||||
|
} |
||||
|
if (byte == 0xDF) { |
||||
|
ReadByte(); |
||||
|
return ReadSize(4, count); |
||||
|
} |
||||
|
return Fail("not a map"); |
||||
|
} |
||||
|
|
||||
|
bool MsgPack::Reader::ReadArrayHeader(size_t& count) { |
||||
|
if (End()) { |
||||
|
return Fail("unexpected end"); |
||||
|
} |
||||
|
const u8 byte = Peek(); |
||||
|
if ((byte & 0xF0) == 0x90) { |
||||
|
count = byte & 0x0F; |
||||
|
offset++; |
||||
|
return true; |
||||
|
} |
||||
|
if (byte == 0xDC) { |
||||
|
ReadByte(); |
||||
|
return ReadSize(2, count); |
||||
|
} |
||||
|
if (byte == 0xDD) { |
||||
|
ReadByte(); |
||||
|
return ReadSize(4, count); |
||||
|
} |
||||
|
return Fail("not an array"); |
||||
|
} |
||||
|
|
||||
|
bool MsgPack::Reader::ReadUInt(u64& value) { |
||||
|
if (End()) { |
||||
|
return Fail("unexpected end"); |
||||
|
} |
||||
|
const u8 byte = Peek(); |
||||
|
if (byte <= 0x7F) { |
||||
|
value = byte; |
||||
|
offset++; |
||||
|
return true; |
||||
|
} |
||||
|
if (byte == 0xCC) { |
||||
|
ReadByte(); |
||||
|
if (!Ensure(1)) { |
||||
|
return Fail("uint8 truncated"); |
||||
|
} |
||||
|
value = data[offset++]; |
||||
|
return true; |
||||
|
} |
||||
|
if (byte == 0xCD) { |
||||
|
ReadByte(); |
||||
|
size_t tmp = 0; |
||||
|
if (!ReadSize(2, tmp)) { |
||||
|
return false; |
||||
|
} |
||||
|
value = tmp; |
||||
|
return true; |
||||
|
} |
||||
|
if (byte == 0xCE) { |
||||
|
ReadByte(); |
||||
|
size_t tmp = 0; |
||||
|
if (!ReadSize(4, tmp)) { |
||||
|
return false; |
||||
|
} |
||||
|
value = tmp; |
||||
|
return true; |
||||
|
} |
||||
|
if (byte == 0xCF) { |
||||
|
ReadByte(); |
||||
|
if (!Ensure(8)) { |
||||
|
return Fail("uint64 truncated"); |
||||
|
} |
||||
|
u64 tmp = 0; |
||||
|
for (int i = 0; i < 8; ++i) { |
||||
|
tmp = (tmp << 8) | data[offset + i]; |
||||
|
} |
||||
|
offset += 8; |
||||
|
value = tmp; |
||||
|
return true; |
||||
|
} |
||||
|
return Fail("not uint"); |
||||
|
} |
||||
|
|
||||
|
bool MsgPack::Reader::ReadInt(s64& value) { |
||||
|
if (End()) { |
||||
|
return Fail("unexpected end"); |
||||
|
} |
||||
|
const u8 byte = Peek(); |
||||
|
if (byte <= 0x7F) { |
||||
|
value = byte; |
||||
|
offset++; |
||||
|
return true; |
||||
|
} |
||||
|
if (byte >= 0xE0) { |
||||
|
value = static_cast<int8_t>(byte); |
||||
|
offset++; |
||||
|
return true; |
||||
|
} |
||||
|
if (byte == 0xD0) { |
||||
|
ReadByte(); |
||||
|
if (!Ensure(1)) { |
||||
|
return Fail("int8 truncated"); |
||||
|
} |
||||
|
value = static_cast<int8_t>(data[offset]); |
||||
|
offset += 1; |
||||
|
return true; |
||||
|
} |
||||
|
if (byte == 0xD1) { |
||||
|
ReadByte(); |
||||
|
if (!Ensure(2)) { |
||||
|
return Fail("int16 truncated"); |
||||
|
} |
||||
|
value = static_cast<int16_t>((data[offset] << 8) | data[offset + 1]); |
||||
|
offset += 2; |
||||
|
return true; |
||||
|
} |
||||
|
if (byte == 0xD2) { |
||||
|
ReadByte(); |
||||
|
if (!Ensure(4)) { |
||||
|
return Fail("int32 truncated"); |
||||
|
} |
||||
|
const s32 tmp = static_cast<int32_t>((data[offset] << 24) | (data[offset + 1] << 16) | |
||||
|
(data[offset + 2] << 8) | data[offset + 3]); |
||||
|
offset += 4; |
||||
|
value = tmp; |
||||
|
return true; |
||||
|
} |
||||
|
if (byte == 0xD3) { |
||||
|
ReadByte(); |
||||
|
if (!Ensure(8)) { |
||||
|
return Fail("int64 truncated"); |
||||
|
} |
||||
|
s64 tmp = 0; |
||||
|
for (int i = 0; i < 8; ++i) { |
||||
|
tmp = (tmp << 8) | data[offset + i]; |
||||
|
} |
||||
|
offset += 8; |
||||
|
value = tmp; |
||||
|
return true; |
||||
|
} |
||||
|
return Fail("not int"); |
||||
|
} |
||||
|
|
||||
|
bool MsgPack::Reader::ReadBool(bool& value) { |
||||
|
if (End()) { |
||||
|
return Fail("unexpected end"); |
||||
|
} |
||||
|
const u8 byte = ReadByte(); |
||||
|
if (byte == 0xC2) { |
||||
|
value = false; |
||||
|
return true; |
||||
|
} |
||||
|
if (byte == 0xC3) { |
||||
|
value = true; |
||||
|
return true; |
||||
|
} |
||||
|
return Fail("not bool"); |
||||
|
} |
||||
|
|
||||
|
bool MsgPack::Reader::ReadString(std::string& value) { |
||||
|
if (End()) { |
||||
|
return Fail("unexpected end"); |
||||
|
} |
||||
|
const u8 byte = ReadByte(); |
||||
|
size_t len = 0; |
||||
|
if ((byte & 0xE0) == 0xA0) { |
||||
|
len = byte & 0x1F; |
||||
|
} else if (byte == 0xD9) { |
||||
|
if (!ReadSize(1, len)) { |
||||
|
return false; |
||||
|
} |
||||
|
} else if (byte == 0xDA) { |
||||
|
if (!ReadSize(2, len)) { |
||||
|
return false; |
||||
|
} |
||||
|
} else if (byte == 0xDB) { |
||||
|
if (!ReadSize(4, len)) { |
||||
|
return false; |
||||
|
} |
||||
|
} else { |
||||
|
return Fail("not string"); |
||||
|
} |
||||
|
|
||||
|
if (!Ensure(len)) { |
||||
|
return Fail("string truncated"); |
||||
|
} |
||||
|
value.assign(reinterpret_cast<const char*>(data.data() + offset), len); |
||||
|
offset += len; |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
bool MsgPack::Reader::ReadBinary(std::vector<u8>& value) { |
||||
|
if (End()) { |
||||
|
return Fail("unexpected end"); |
||||
|
} |
||||
|
const u8 byte = ReadByte(); |
||||
|
size_t len = 0; |
||||
|
if (byte == 0xC4) { |
||||
|
if (!ReadSize(1, len)) { |
||||
|
return false; |
||||
|
} |
||||
|
} else if (byte == 0xC5) { |
||||
|
if (!ReadSize(2, len)) { |
||||
|
return false; |
||||
|
} |
||||
|
} else if (byte == 0xC6) { |
||||
|
if (!ReadSize(4, len)) { |
||||
|
return false; |
||||
|
} |
||||
|
} else { |
||||
|
return Fail("not binary"); |
||||
|
} |
||||
|
if (!Ensure(len)) { |
||||
|
return Fail("binary truncated"); |
||||
|
} |
||||
|
value.assign(data.begin() + offset, data.begin() + offset + len); |
||||
|
offset += len; |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
bool MsgPack::Reader::ReadBinaryCompat(std::vector<u8>& value) { |
||||
|
if (!ReadBinary(value)) { |
||||
|
value.clear(); |
||||
|
return false; |
||||
|
} |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
bool MsgPack::Reader::ReadStringArray(std::vector<std::string>& out) { |
||||
|
size_t count = 0; |
||||
|
if (!ReadArrayHeader(count)) { |
||||
|
return false; |
||||
|
} |
||||
|
out.clear(); |
||||
|
out.reserve(count); |
||||
|
for (size_t i = 0; i < count; ++i) { |
||||
|
std::string value; |
||||
|
if (!ReadString(value)) { |
||||
|
return false; |
||||
|
} |
||||
|
out.push_back(std::move(value)); |
||||
|
} |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
bool MsgPack::Reader::ReadNewsVersion(NewsStruct::Version& out) { |
||||
|
size_t count = 0; |
||||
|
if (!ReadMapHeader(count)) { |
||||
|
return false; |
||||
|
} |
||||
|
for (size_t i = 0; i < count; ++i) { |
||||
|
std::string key; |
||||
|
if (!ReadString(key)) { |
||||
|
return false; |
||||
|
} |
||||
|
if (key == "format") { |
||||
|
if (!ReadUInt(out.format)) { |
||||
|
return false; |
||||
|
} |
||||
|
} else if (key == "semantics") { |
||||
|
if (!ReadUInt(out.semantics)) { |
||||
|
return false; |
||||
|
} |
||||
|
} else { |
||||
|
if (!SkipValue()) { |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
bool MsgPack::Reader::ReadNewsSubject(NewsStruct::Subject& out) { |
||||
|
size_t count = 0; |
||||
|
if (!ReadMapHeader(count)) { |
||||
|
return false; |
||||
|
} |
||||
|
for (size_t i = 0; i < count; ++i) { |
||||
|
std::string key; |
||||
|
if (!ReadString(key)) { |
||||
|
return false; |
||||
|
} |
||||
|
if (key == "caption") { |
||||
|
if (!ReadUInt(out.caption)) { |
||||
|
return false; |
||||
|
} |
||||
|
} else if (key == "text") { |
||||
|
if (!ReadString(out.text)) { |
||||
|
return false; |
||||
|
} |
||||
|
} else { |
||||
|
if (!SkipValue()) { |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
bool MsgPack::Reader::ReadNewsFooter(NewsStruct::Footer& out) { |
||||
|
size_t count = 0; |
||||
|
if (!ReadMapHeader(count)) { |
||||
|
return false; |
||||
|
} |
||||
|
for (size_t i = 0; i < count; ++i) { |
||||
|
std::string key; |
||||
|
if (!ReadString(key)) { |
||||
|
return false; |
||||
|
} |
||||
|
if (key == "text") { |
||||
|
if (!ReadString(out.text)) { |
||||
|
return false; |
||||
|
} |
||||
|
} else { |
||||
|
if (!SkipValue()) { |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
bool MsgPack::Reader::ReadByteArray(std::vector<u8>& out) { |
||||
|
if (!ReadBinary(out)) { |
||||
|
out.clear(); |
||||
|
return false; |
||||
|
} |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
bool MsgPack::Reader::ReadNewsBody(NewsStruct::Body& out) { |
||||
|
size_t count = 0; |
||||
|
if (!ReadMapHeader(count)) { |
||||
|
return false; |
||||
|
} |
||||
|
for (size_t i = 0; i < count; ++i) { |
||||
|
std::string key; |
||||
|
if (!ReadString(key)) { |
||||
|
return false; |
||||
|
} |
||||
|
if (key == "text") { |
||||
|
if (!ReadString(out.text)) { |
||||
|
return false; |
||||
|
} |
||||
|
} else if (key == "main_image_height") { |
||||
|
if (!ReadUInt(out.main_image_height)) { |
||||
|
return false; |
||||
|
} |
||||
|
} else if (key == "movie_url") { |
||||
|
if (!ReadString(out.movie_url)) { |
||||
|
return false; |
||||
|
} |
||||
|
} else if (key == "main_image") { |
||||
|
if (!ReadByteArray(out.main_image)) { |
||||
|
return false; |
||||
|
} |
||||
|
} else { |
||||
|
if (!SkipValue()) { |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
bool MsgPack::Reader::ReadNewsBrowser(NewsStruct::More::Browser& out) { |
||||
|
size_t count = 0; |
||||
|
if (!ReadMapHeader(count)) { |
||||
|
return false; |
||||
|
} |
||||
|
out.present = true; |
||||
|
for (size_t i = 0; i < count; ++i) { |
||||
|
std::string key; |
||||
|
if (!ReadString(key)) { |
||||
|
return false; |
||||
|
} |
||||
|
if (key == "url") { |
||||
|
if (!ReadString(out.url)) { |
||||
|
return false; |
||||
|
} |
||||
|
} else if (key == "text") { |
||||
|
if (!ReadString(out.text)) { |
||||
|
return false; |
||||
|
} |
||||
|
} else { |
||||
|
if (!SkipValue()) { |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
bool MsgPack::Reader::ReadNewsMore(NewsStruct::More& out) { |
||||
|
size_t count = 0; |
||||
|
if (!ReadMapHeader(count)) { |
||||
|
return false; |
||||
|
} |
||||
|
out.has_browser = false; |
||||
|
for (size_t i = 0; i < count; ++i) { |
||||
|
std::string key; |
||||
|
if (!ReadString(key)) { |
||||
|
return false; |
||||
|
} |
||||
|
if (key == "browser") { |
||||
|
out.has_browser = true; |
||||
|
if (!ReadNewsBrowser(out.browser)) { |
||||
|
return false; |
||||
|
} |
||||
|
} else { |
||||
|
if (!SkipValue()) { |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
bool MsgPack::Reader::ReadNewsStruct(NewsStruct& out) { |
||||
|
size_t count = 0; |
||||
|
if (!ReadMapHeader(count)) { |
||||
|
return false; |
||||
|
} |
||||
|
for (size_t i = 0; i < count; ++i) { |
||||
|
std::string key; |
||||
|
if (!ReadString(key)) { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
auto read_u64 = [&](u64& target) -> bool { |
||||
|
return ReadUInt(target); |
||||
|
}; |
||||
|
|
||||
|
if (key == "version") { |
||||
|
if (!ReadNewsVersion(out.version)) { |
||||
|
return false; |
||||
|
} |
||||
|
} else if (key == "news_id") { |
||||
|
if (!read_u64(out.news_id)) { |
||||
|
return false; |
||||
|
} |
||||
|
} else if (key == "published_at") { |
||||
|
if (!read_u64(out.published_at)) { |
||||
|
return false; |
||||
|
} |
||||
|
} else if (key == "pickup_limit") { |
||||
|
if (!read_u64(out.pickup_limit)) { |
||||
|
return false; |
||||
|
} |
||||
|
} else if (key == "priority") { |
||||
|
if (!read_u64(out.priority)) { |
||||
|
return false; |
||||
|
} |
||||
|
} else if (key == "deletion_priority") { |
||||
|
if (!read_u64(out.deletion_priority)) { |
||||
|
return false; |
||||
|
} |
||||
|
} else if (key == "language") { |
||||
|
if (!ReadString(out.language)) { |
||||
|
return false; |
||||
|
} |
||||
|
} else if (key == "supported_languages") { |
||||
|
if (!ReadStringArray(out.supported_languages)) { |
||||
|
return false; |
||||
|
} |
||||
|
} else if (key == "display_type") { |
||||
|
if (!ReadString(out.display_type)) { |
||||
|
return false; |
||||
|
} |
||||
|
} else if (key == "topic_id") { |
||||
|
if (!ReadString(out.topic_id)) { |
||||
|
return false; |
||||
|
} |
||||
|
} else if (key == "no_photography") { |
||||
|
if (!read_u64(out.no_photography)) { |
||||
|
return false; |
||||
|
} |
||||
|
} else if (key == "surprise") { |
||||
|
if (!read_u64(out.surprise)) { |
||||
|
return false; |
||||
|
} |
||||
|
} else if (key == "bashotorya") { |
||||
|
if (!read_u64(out.bashotorya)) { |
||||
|
return false; |
||||
|
} |
||||
|
} else if (key == "movie") { |
||||
|
if (!read_u64(out.movie)) { |
||||
|
return false; |
||||
|
} |
||||
|
} else if (key == "subject") { |
||||
|
if (!ReadNewsSubject(out.subject)) { |
||||
|
return false; |
||||
|
} |
||||
|
} else if (key == "topic_name") { |
||||
|
if (!ReadString(out.topic_name)) { |
||||
|
return false; |
||||
|
} |
||||
|
} else if (key == "list_image") { |
||||
|
if (!ReadByteArray(out.list_image)) { |
||||
|
return false; |
||||
|
} |
||||
|
} else if (key == "footer") { |
||||
|
if (!ReadNewsFooter(out.footer)) { |
||||
|
return false; |
||||
|
} |
||||
|
} else if (key == "allow_domains") { |
||||
|
if (!ReadString(out.allow_domains)) { |
||||
|
return false; |
||||
|
} |
||||
|
} else if (key == "more") { |
||||
|
if (!ReadNewsMore(out.more)) { |
||||
|
return false; |
||||
|
} |
||||
|
} else if (key == "body") { |
||||
|
if (!ReadNewsBody(out.body)) { |
||||
|
return false; |
||||
|
} |
||||
|
} else if (key == "contents_descriptors") { |
||||
|
if (!ReadString(out.contents_descriptors)) { |
||||
|
return false; |
||||
|
} |
||||
|
} else if (key == "interactive_elements") { |
||||
|
if (!ReadString(out.interactive_elements)) { |
||||
|
return false; |
||||
|
} |
||||
|
} else { |
||||
|
if (!SkipValue()) { |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
} // namespace Service::News
|
||||
@ -0,0 +1,149 @@ |
|||||
|
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project |
||||
|
// SPDX-License-Identifier: GPL-3.0-or-later |
||||
|
|
||||
|
#pragma once |
||||
|
|
||||
|
#include <span> |
||||
|
#include <string> |
||||
|
#include <string_view> |
||||
|
#include <type_traits> |
||||
|
#include <vector> |
||||
|
|
||||
|
#include "common/common_types.h" |
||||
|
|
||||
|
namespace Service::News { |
||||
|
|
||||
|
struct NewsStruct { |
||||
|
struct Version { |
||||
|
u64 format{}; |
||||
|
u64 semantics{}; |
||||
|
}; |
||||
|
|
||||
|
struct Subject { |
||||
|
u64 caption{}; |
||||
|
std::string text; |
||||
|
}; |
||||
|
|
||||
|
struct Footer { |
||||
|
std::string text; |
||||
|
}; |
||||
|
|
||||
|
struct Body { |
||||
|
std::string text; |
||||
|
u64 main_image_height{}; |
||||
|
std::string movie_url; |
||||
|
std::vector<u8> main_image; |
||||
|
}; |
||||
|
|
||||
|
struct More { |
||||
|
struct Browser { |
||||
|
std::string url; |
||||
|
std::string text; |
||||
|
bool present{}; |
||||
|
} browser; |
||||
|
bool has_browser{}; |
||||
|
}; |
||||
|
|
||||
|
Version version; |
||||
|
u64 news_id{}; |
||||
|
u64 published_at{}; |
||||
|
u64 pickup_limit{}; |
||||
|
u64 priority{}; |
||||
|
u64 deletion_priority{}; |
||||
|
std::string language; |
||||
|
std::vector<std::string> supported_languages; |
||||
|
std::string display_type; |
||||
|
std::string topic_id; |
||||
|
u64 no_photography{}; |
||||
|
u64 surprise{}; |
||||
|
u64 bashotorya{}; |
||||
|
u64 movie{}; |
||||
|
Subject subject; |
||||
|
std::string topic_name; |
||||
|
std::vector<u8> list_image; |
||||
|
Footer footer; |
||||
|
std::string allow_domains; |
||||
|
More more; |
||||
|
Body body; |
||||
|
std::string contents_descriptors; |
||||
|
std::string interactive_elements; |
||||
|
}; |
||||
|
|
||||
|
class MsgPack { |
||||
|
public: |
||||
|
class Writer { |
||||
|
public: |
||||
|
void WriteFixMap(size_t count); |
||||
|
void WriteMap32(size_t count); |
||||
|
void WriteKey(std::string_view key); |
||||
|
void WriteString(std::string_view value); |
||||
|
void WriteInt64(s64 value); |
||||
|
void WriteUInt(u64 value); |
||||
|
void WriteNil(); |
||||
|
void WriteFixArray(size_t count); |
||||
|
void WriteBinary(const std::vector<u8>& data); |
||||
|
void WriteBool(bool value); |
||||
|
|
||||
|
std::vector<u8> Take(); |
||||
|
const std::vector<u8>& Buffer() const { return out; } |
||||
|
void Clear() { out.clear(); } |
||||
|
|
||||
|
private: |
||||
|
void WriteBytes(std::span<const u8> bytes); |
||||
|
|
||||
|
std::vector<u8> out; |
||||
|
}; |
||||
|
|
||||
|
class Reader { |
||||
|
public: |
||||
|
explicit Reader(std::span<const u8> buffer); |
||||
|
|
||||
|
template <typename T> |
||||
|
bool Read(T& out) { |
||||
|
if constexpr (std::is_same_v<T, NewsStruct>) { |
||||
|
return ReadNewsStruct(out); |
||||
|
} else { |
||||
|
static_assert(sizeof(T) == 0, "Unsupported MsgPack::Reader::Read type"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
bool SkipValue(); |
||||
|
bool SkipAll(); |
||||
|
|
||||
|
bool ReadMapHeader(size_t& count); |
||||
|
bool ReadArrayHeader(size_t& count); |
||||
|
bool ReadUInt(u64& value); |
||||
|
bool ReadInt(s64& value); |
||||
|
bool ReadBool(bool& value); |
||||
|
bool ReadString(std::string& value); |
||||
|
bool ReadBinary(std::vector<u8>& value); |
||||
|
|
||||
|
bool End() const { return offset >= data.size(); } |
||||
|
std::string_view Error() const { return error; } |
||||
|
|
||||
|
private: |
||||
|
bool Fail(const char* msg); |
||||
|
bool Ensure(size_t n) const; |
||||
|
u8 Peek() const; |
||||
|
u8 ReadByte(); |
||||
|
bool ReadSize(size_t byte_count, size_t& out_size); |
||||
|
bool SkipBytes(size_t n); |
||||
|
bool SkipContainer(size_t count, bool map_mode); |
||||
|
bool ReadNewsStruct(NewsStruct& out); |
||||
|
bool ReadNewsVersion(NewsStruct::Version& out); |
||||
|
bool ReadNewsSubject(NewsStruct::Subject& out); |
||||
|
bool ReadNewsFooter(NewsStruct::Footer& out); |
||||
|
bool ReadNewsBody(NewsStruct::Body& out); |
||||
|
bool ReadNewsMore(NewsStruct::More& out); |
||||
|
bool ReadNewsBrowser(NewsStruct::More::Browser& out); |
||||
|
bool ReadStringArray(std::vector<std::string>& out); |
||||
|
bool ReadBinaryCompat(std::vector<u8>& out); |
||||
|
bool ReadByteArray(std::vector<u8>& out); |
||||
|
|
||||
|
std::span<const u8> data; |
||||
|
size_t offset{0}; |
||||
|
std::string error; |
||||
|
}; |
||||
|
}; |
||||
|
|
||||
|
} // namespace Service::News |
||||
@ -1,25 +1,125 @@ |
|||||
|
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
|
||||
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
|
||||
#include "core/hle/service/bcat/news/news_data_service.h"
|
#include "core/hle/service/bcat/news/news_data_service.h"
|
||||
|
#include "core/hle/service/bcat/news/builtin_news.h"
|
||||
|
#include "core/hle/service/bcat/news/news_storage.h"
|
||||
|
#include "core/hle/service/cmif_serialization.h"
|
||||
|
|
||||
|
#include "common/logging/log.h"
|
||||
|
|
||||
|
#include <cstring>
|
||||
|
|
||||
namespace Service::News { |
namespace Service::News { |
||||
|
namespace { |
||||
|
|
||||
|
std::string_view ToStringView(std::span<const char> buf) { |
||||
|
const std::string_view sv{buf.data(), buf.size()}; |
||||
|
const auto nul = sv.find('\0'); |
||||
|
return nul == std::string_view::npos ? sv : sv.substr(0, nul); |
||||
|
} |
||||
|
|
||||
|
} // namespace
|
||||
|
|
||||
INewsDataService::INewsDataService(Core::System& system_) |
INewsDataService::INewsDataService(Core::System& system_) |
||||
: ServiceFramework{system_, "INewsDataService"} { |
: ServiceFramework{system_, "INewsDataService"} { |
||||
// clang-format off
|
|
||||
static const FunctionInfo functions[] = { |
static const FunctionInfo functions[] = { |
||||
{0, nullptr, "Open"}, |
|
||||
{1, nullptr, "OpenWithNewsRecordV1"}, |
|
||||
{2, nullptr, "Read"}, |
|
||||
{3, nullptr, "GetSize"}, |
|
||||
{1001, nullptr, "OpenWithNewsRecord"}, |
|
||||
|
{0, D<&INewsDataService::Open>, "Open"}, |
||||
|
{1, D<&INewsDataService::OpenWithNewsRecordV1>, "OpenWithNewsRecordV1"}, |
||||
|
{2, D<&INewsDataService::Read>, "Read"}, |
||||
|
{3, D<&INewsDataService::GetSize>, "GetSize"}, |
||||
|
{1001, D<&INewsDataService::OpenWithNewsRecord>, "OpenWithNewsRecord"}, |
||||
}; |
}; |
||||
// clang-format on
|
|
||||
|
|
||||
RegisterHandlers(functions); |
RegisterHandlers(functions); |
||||
} |
} |
||||
|
|
||||
INewsDataService::~INewsDataService() = default; |
INewsDataService::~INewsDataService() = default; |
||||
|
|
||||
|
bool INewsDataService::TryOpen(std::string_view key, std::string_view user) { |
||||
|
opened_payload.clear(); |
||||
|
|
||||
|
if (auto found = NewsStorage::Instance().FindByNewsId(key, user)) { |
||||
|
opened_payload = std::move(found->payload); |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
if (!user.empty()) { |
||||
|
if (auto found = NewsStorage::Instance().FindByNewsId(key)) { |
||||
|
opened_payload = std::move(found->payload); |
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const auto list = NewsStorage::Instance().ListAll(); |
||||
|
if (!list.empty()) { |
||||
|
if (auto found = NewsStorage::Instance().FindByNewsId(ToStringView(list.front().news_id))) { |
||||
|
opened_payload = std::move(found->payload); |
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
Result INewsDataService::Open(InBuffer<BufferAttr_HipcMapAlias> name) { |
||||
|
EnsureBuiltinNewsLoaded(); |
||||
|
|
||||
|
const auto key = ToStringView({reinterpret_cast<const char*>(name.data()), name.size()}); |
||||
|
|
||||
|
if (TryOpen(key, {})) { |
||||
|
R_SUCCEED(); |
||||
|
} |
||||
|
|
||||
|
R_RETURN(ResultUnknown); |
||||
|
} |
||||
|
|
||||
|
Result INewsDataService::OpenWithNewsRecordV1(NewsRecordV1 record) { |
||||
|
EnsureBuiltinNewsLoaded(); |
||||
|
|
||||
|
const auto key = ToStringView(record.news_id); |
||||
|
const auto user = ToStringView(record.user_id); |
||||
|
|
||||
|
if (TryOpen(key, user)) { |
||||
|
R_SUCCEED(); |
||||
|
} |
||||
|
|
||||
|
R_RETURN(ResultUnknown); |
||||
|
} |
||||
|
|
||||
|
Result INewsDataService::OpenWithNewsRecord(NewsRecord record) { |
||||
|
EnsureBuiltinNewsLoaded(); |
||||
|
|
||||
|
const auto key = ToStringView(record.news_id); |
||||
|
const auto user = ToStringView(record.user_id); |
||||
|
|
||||
|
if (TryOpen(key, user)) { |
||||
|
R_SUCCEED(); |
||||
|
} |
||||
|
|
||||
|
R_RETURN(ResultUnknown); |
||||
|
} |
||||
|
|
||||
|
Result INewsDataService::Read(Out<u64> out_size, s64 offset, |
||||
|
OutBuffer<BufferAttr_HipcMapAlias> out_buffer) { |
||||
|
const auto off = static_cast<size_t>(std::max<s64>(0, offset)); |
||||
|
|
||||
|
if (off >= opened_payload.size()) { |
||||
|
*out_size = 0; |
||||
|
R_SUCCEED(); |
||||
|
} |
||||
|
|
||||
|
const size_t len = std::min(out_buffer.size(), opened_payload.size() - off); |
||||
|
std::memcpy(out_buffer.data(), opened_payload.data() + off, len); |
||||
|
*out_size = len; |
||||
|
R_SUCCEED(); |
||||
|
} |
||||
|
|
||||
|
Result INewsDataService::GetSize(Out<s64> out_size) { |
||||
|
*out_size = static_cast<s64>(opened_payload.size()); |
||||
|
R_SUCCEED(); |
||||
|
} |
||||
|
|
||||
} // namespace Service::News
|
} // namespace Service::News
|
||||
@ -1,53 +1,195 @@ |
|||||
|
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
|
||||
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
|
||||
#include "core/hle/service/bcat/news/news_database_service.h"
|
#include "core/hle/service/bcat/news/news_database_service.h"
|
||||
|
#include "core/hle/service/bcat/news/builtin_news.h"
|
||||
|
#include "core/hle/service/bcat/news/news_storage.h"
|
||||
#include "core/hle/service/cmif_serialization.h"
|
#include "core/hle/service/cmif_serialization.h"
|
||||
|
|
||||
|
#include <algorithm>
|
||||
|
#include <cstring>
|
||||
|
|
||||
namespace Service::News { |
namespace Service::News { |
||||
|
namespace { |
||||
|
|
||||
|
std::string_view ToStringView(std::span<const u8> buf) { |
||||
|
if (buf.empty()) return {}; |
||||
|
auto data = reinterpret_cast<const char*>(buf.data()); |
||||
|
return {data, strnlen(data, buf.size())}; |
||||
|
} |
||||
|
|
||||
|
std::string_view ToStringView(std::span<const char> buf) { |
||||
|
if (buf.empty()) return {}; |
||||
|
return {buf.data(), strnlen(buf.data(), buf.size())}; |
||||
|
} |
||||
|
|
||||
|
bool UpdateField(NewsRecord& rec, std::string_view column, s32 value, bool additive) { |
||||
|
auto apply = [&](s32& field) { |
||||
|
field = additive ? field + value : value; |
||||
|
return true; |
||||
|
}; |
||||
|
|
||||
|
if (column == "read") return apply(rec.read); |
||||
|
if (column == "newly") return apply(rec.newly); |
||||
|
if (column == "displayed") return apply(rec.displayed); |
||||
|
if (column == "extra1" || column == "extra_1") return apply(rec.extra1); |
||||
|
if (column == "extra2" || column == "extra_2") return apply(rec.extra2); |
||||
|
|
||||
|
// Accept but ignore fields that don't exist in our struct
|
||||
|
return column == "priority" || column == "decoration_type" || |
||||
|
column == "feedback" || column == "category"; |
||||
|
} |
||||
|
|
||||
|
} // namespace
|
||||
|
|
||||
INewsDatabaseService::INewsDatabaseService(Core::System& system_) |
INewsDatabaseService::INewsDatabaseService(Core::System& system_) |
||||
: ServiceFramework{system_, "INewsDatabaseService"} { |
: ServiceFramework{system_, "INewsDatabaseService"} { |
||||
// clang-format off
|
|
||||
static const FunctionInfo functions[] = { |
static const FunctionInfo functions[] = { |
||||
{0, nullptr, "GetListV1"}, |
|
||||
|
{0, D<&INewsDatabaseService::GetListV1>, "GetListV1"}, |
||||
{1, D<&INewsDatabaseService::Count>, "Count"}, |
{1, D<&INewsDatabaseService::Count>, "Count"}, |
||||
{2, nullptr, "CountWithKey"}, |
|
||||
{3, nullptr, "UpdateIntegerValue"}, |
|
||||
|
{2, D<&INewsDatabaseService::CountWithKey>, "CountWithKey"}, |
||||
|
{3, D<&INewsDatabaseService::UpdateIntegerValue>, "UpdateIntegerValue"}, |
||||
{4, D<&INewsDatabaseService::UpdateIntegerValueWithAddition>, "UpdateIntegerValueWithAddition"}, |
{4, D<&INewsDatabaseService::UpdateIntegerValueWithAddition>, "UpdateIntegerValueWithAddition"}, |
||||
{5, nullptr, "UpdateStringValue"}, |
|
||||
|
{5, D<&INewsDatabaseService::UpdateStringValue>, "UpdateStringValue"}, |
||||
{1000, D<&INewsDatabaseService::GetList>, "GetList"}, |
{1000, D<&INewsDatabaseService::GetList>, "GetList"}, |
||||
}; |
}; |
||||
// clang-format on
|
|
||||
|
|
||||
RegisterHandlers(functions); |
RegisterHandlers(functions); |
||||
} |
} |
||||
|
|
||||
INewsDatabaseService::~INewsDatabaseService() = default; |
INewsDatabaseService::~INewsDatabaseService() = default; |
||||
|
|
||||
Result INewsDatabaseService::Count(Out<s32> out_count, |
|
||||
InBuffer<BufferAttr_HipcPointer> buffer_data) { |
|
||||
LOG_WARNING(Service_BCAT, "(STUBBED) called, buffer_size={}", buffer_data.size()); |
|
||||
*out_count = 0; |
|
||||
|
Result INewsDatabaseService::Count(Out<s32> out_count, InBuffer<BufferAttr_HipcPointer> where) { |
||||
|
EnsureBuiltinNewsLoaded(); |
||||
|
*out_count = static_cast<s32>(NewsStorage::Instance().ListAll().size()); |
||||
|
R_SUCCEED(); |
||||
|
} |
||||
|
|
||||
|
Result INewsDatabaseService::CountWithKey(Out<s32> out_count, |
||||
|
InBuffer<BufferAttr_HipcPointer> key, |
||||
|
InBuffer<BufferAttr_HipcPointer> where) { |
||||
|
EnsureBuiltinNewsLoaded(); |
||||
|
*out_count = static_cast<s32>(NewsStorage::Instance().ListAll().size()); |
||||
R_SUCCEED(); |
R_SUCCEED(); |
||||
} |
} |
||||
|
|
||||
Result INewsDatabaseService::UpdateIntegerValueWithAddition( |
|
||||
u32 value, InBuffer<BufferAttr_HipcPointer> buffer_data_1, |
|
||||
InBuffer<BufferAttr_HipcPointer> buffer_data_2) { |
|
||||
LOG_WARNING(Service_BCAT, "(STUBBED) called, value={}, buffer_size_1={}, buffer_data_2={}", |
|
||||
value, buffer_data_1.size(), buffer_data_2.size()); |
|
||||
|
Result INewsDatabaseService::UpdateIntegerValue(u32 value, |
||||
|
InBuffer<BufferAttr_HipcPointer> key, |
||||
|
InBuffer<BufferAttr_HipcPointer> where) { |
||||
|
const auto column = ToStringView(key); |
||||
|
for (const auto& rec : NewsStorage::Instance().ListAll()) { |
||||
|
NewsStorage::Instance().UpdateRecord( |
||||
|
ToStringView(rec.news_id), {}, |
||||
|
[&](NewsRecord& r) { UpdateField(r, column, static_cast<s32>(value), false); }); |
||||
|
} |
||||
R_SUCCEED(); |
R_SUCCEED(); |
||||
} |
} |
||||
|
|
||||
Result INewsDatabaseService::GetList(Out<s32> out_count, u32 value, |
|
||||
OutBuffer<BufferAttr_HipcMapAlias> out_buffer_data, |
|
||||
InBuffer<BufferAttr_HipcPointer> buffer_data_1, |
|
||||
InBuffer<BufferAttr_HipcPointer> buffer_data_2) { |
|
||||
LOG_WARNING(Service_BCAT, "(STUBBED) called, value={}, buffer_size_1={}, buffer_data_2={}", |
|
||||
value, buffer_data_1.size(), buffer_data_2.size()); |
|
||||
|
Result INewsDatabaseService::UpdateIntegerValueWithAddition(u32 value, |
||||
|
InBuffer<BufferAttr_HipcPointer> key, |
||||
|
InBuffer<BufferAttr_HipcPointer> where) { |
||||
|
const auto column = ToStringView(key); |
||||
|
const auto where_str = ToStringView(where); |
||||
|
|
||||
|
// Extract news_id from where clause like "N_SWITCH(news_id,'LA00000000000123456',1,0)=1"
|
||||
|
auto extract_news_id = [](std::string_view w) -> std::string { |
||||
|
auto pos = w.find("'LA"); |
||||
|
if (pos == std::string_view::npos) return {}; |
||||
|
pos++; // skip the '
|
||||
|
auto end = w.find("'", pos); |
||||
|
if (end == std::string_view::npos) return {}; |
||||
|
return std::string(w.substr(pos, end - pos)); |
||||
|
}; |
||||
|
|
||||
|
const auto news_id = extract_news_id(where_str); |
||||
|
|
||||
|
if (column == "read" && value > 0 && !news_id.empty()) { |
||||
|
NewsStorage::Instance().MarkAsRead(news_id); |
||||
|
} else if (!news_id.empty()) { |
||||
|
NewsStorage::Instance().UpdateRecord(news_id, {}, |
||||
|
[&](NewsRecord& r) { UpdateField(r, column, static_cast<s32>(value), true); }); |
||||
|
} |
||||
|
|
||||
|
R_SUCCEED(); |
||||
|
} |
||||
|
|
||||
|
Result INewsDatabaseService::UpdateStringValue(InBuffer<BufferAttr_HipcPointer> key, |
||||
|
InBuffer<BufferAttr_HipcPointer> value, |
||||
|
InBuffer<BufferAttr_HipcPointer> where) { |
||||
|
LOG_WARNING(Service_BCAT, "(STUBBED) UpdateStringValue"); |
||||
|
R_SUCCEED(); |
||||
|
} |
||||
|
|
||||
|
Result INewsDatabaseService::GetListV1(Out<s32> out_count, |
||||
|
OutBuffer<BufferAttr_HipcMapAlias> out_buffer, |
||||
|
InBuffer<BufferAttr_HipcPointer> where, |
||||
|
InBuffer<BufferAttr_HipcPointer> order, |
||||
|
s32 offset) { |
||||
|
EnsureBuiltinNewsLoaded(); |
||||
|
|
||||
|
auto record_size = sizeof(NewsRecordV1); |
||||
|
|
||||
|
if (out_buffer.size() < record_size) { |
||||
*out_count = 0; |
*out_count = 0; |
||||
R_SUCCEED(); |
R_SUCCEED(); |
||||
|
} |
||||
|
|
||||
|
std::memset(out_buffer.data(), 0, out_buffer.size()); |
||||
|
|
||||
|
const auto list = NewsStorage::Instance().ListAll(); |
||||
|
const size_t start = static_cast<size_t>(std::max(0, offset)); |
||||
|
const size_t max_records = out_buffer.size() / record_size; |
||||
|
const size_t available = start < list.size() ? list.size() - start : 0; |
||||
|
const size_t count = std::min(max_records, available); |
||||
|
|
||||
|
for (size_t i = 0; i < count; ++i) { |
||||
|
const auto& src = list[start + i]; |
||||
|
NewsRecordV1 dst{}; |
||||
|
std::memcpy(dst.news_id.data(), src.news_id.data(), dst.news_id.size()); |
||||
|
std::memcpy(dst.user_id.data(), src.user_id.data(), dst.user_id.size()); |
||||
|
dst.received_time = src.received_time; |
||||
|
dst.read = src.read; |
||||
|
dst.newly = src.newly; |
||||
|
dst.displayed = src.displayed; |
||||
|
dst.extra1 = src.extra1; |
||||
|
std::memcpy(out_buffer.data() + i * record_size, &dst, record_size); |
||||
|
} |
||||
|
|
||||
|
*out_count = static_cast<s32>(count); |
||||
|
R_SUCCEED(); |
||||
|
} |
||||
|
|
||||
|
Result INewsDatabaseService::GetList(Out<s32> out_count, |
||||
|
OutBuffer<BufferAttr_HipcMapAlias> out_buffer, |
||||
|
InBuffer<BufferAttr_HipcPointer> where, |
||||
|
InBuffer<BufferAttr_HipcPointer> order, |
||||
|
s32 offset) { |
||||
|
EnsureBuiltinNewsLoaded(); |
||||
|
NewsStorage::Instance().ResetOpenCounter(); |
||||
|
|
||||
|
auto record_size = sizeof(NewsRecord); |
||||
|
|
||||
|
if (out_buffer.size() < record_size) { |
||||
|
*out_count = 0; |
||||
|
R_SUCCEED(); |
||||
|
} |
||||
|
|
||||
|
std::memset(out_buffer.data(), 0, out_buffer.size()); |
||||
|
|
||||
|
const auto list = NewsStorage::Instance().ListAll(); |
||||
|
const size_t start = static_cast<size_t>(std::max(0, offset)); |
||||
|
const size_t max_records = out_buffer.size() / record_size; |
||||
|
const size_t available = start < list.size() ? list.size() - start : 0; |
||||
|
const size_t count = std::min(max_records, available); |
||||
|
|
||||
|
if (count > 0) { |
||||
|
std::memcpy(out_buffer.data(), list.data() + start, count * record_size); |
||||
|
} |
||||
|
|
||||
|
*out_count = static_cast<s32>(count); |
||||
|
R_SUCCEED(); |
||||
} |
} |
||||
|
|
||||
} // namespace Service::News
|
} // namespace Service::News
|
||||
@ -0,0 +1,169 @@ |
|||||
|
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
|
||||
|
#include "core/hle/service/bcat/news/news_storage.h"
|
||||
|
|
||||
|
#include "common/fs/path_util.h"
|
||||
|
|
||||
|
#include <algorithm>
|
||||
|
#include <chrono>
|
||||
|
#include <cstring>
|
||||
|
#include <filesystem>
|
||||
|
#include <fstream>
|
||||
|
#include <set>
|
||||
|
|
||||
|
namespace Service::News { |
||||
|
namespace { |
||||
|
|
||||
|
std::filesystem::path GetReadCachePath() { |
||||
|
return Common::FS::GetEdenPath(Common::FS::EdenPath::CacheDir) / "news" / "news_read"; |
||||
|
} |
||||
|
|
||||
|
std::set<std::string> LoadReadIds() { |
||||
|
std::set<std::string> ids; |
||||
|
std::ifstream f(GetReadCachePath()); |
||||
|
std::string line; |
||||
|
while (std::getline(f, line)) { |
||||
|
if (!line.empty()) ids.insert(line); |
||||
|
} |
||||
|
return ids; |
||||
|
} |
||||
|
|
||||
|
void SaveReadIds(const std::set<std::string>& ids) { |
||||
|
const auto path = GetReadCachePath(); |
||||
|
std::error_code ec; |
||||
|
std::filesystem::create_directories(path.parent_path(), ec); |
||||
|
std::ofstream f(path); |
||||
|
for (const auto& id : ids) { |
||||
|
f << id << '\n'; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} // namespace
|
||||
|
|
||||
|
NewsStorage& NewsStorage::Instance() { |
||||
|
static NewsStorage s; |
||||
|
return s; |
||||
|
} |
||||
|
|
||||
|
void NewsStorage::Clear() { |
||||
|
std::scoped_lock lk{mtx}; |
||||
|
items.clear(); |
||||
|
} |
||||
|
|
||||
|
void NewsStorage::CopyZ(std::span<char> dst, std::string_view src) { |
||||
|
std::memset(dst.data(), 0, dst.size()); |
||||
|
std::memcpy(dst.data(), src.data(), std::min(dst.size() - 1, src.size())); |
||||
|
} |
||||
|
|
||||
|
std::string NewsStorage::MakeKey(std::string_view news_id, std::string_view user_id) { |
||||
|
return std::string(news_id) + "|" + std::string(user_id); |
||||
|
} |
||||
|
|
||||
|
s64 NewsStorage::Now() { |
||||
|
using namespace std::chrono; |
||||
|
return duration_cast<seconds>(system_clock::now().time_since_epoch()).count(); |
||||
|
} |
||||
|
|
||||
|
StoredNews& NewsStorage::Upsert(std::string_view news_id, std::string_view user_id, |
||||
|
std::string_view topic_id, s64 time, std::vector<u8> payload) { |
||||
|
std::scoped_lock lk{mtx}; |
||||
|
|
||||
|
const auto key = MakeKey(news_id, user_id); |
||||
|
auto it = items.find(key); |
||||
|
const bool exists = it != items.end(); |
||||
|
|
||||
|
// implemented a little "read" file. All in cache.
|
||||
|
const auto read_ids = LoadReadIds(); |
||||
|
const bool was_read = read_ids.contains(std::string(news_id)); |
||||
|
|
||||
|
NewsRecord rec{}; |
||||
|
CopyZ(rec.news_id, news_id); |
||||
|
CopyZ(rec.user_id, user_id); |
||||
|
// there is nx_notice and nx_promotion for tags pre-existing
|
||||
|
CopyZ(rec.topic_id, topic_id.empty() ? "nx_notice" : topic_id); |
||||
|
|
||||
|
rec.received_time = exists ? it->second.record.received_time : (time ? time : Now()); |
||||
|
rec.read = was_read ? 1 : (exists ? it->second.record.read : 0); |
||||
|
rec.newly = was_read ? 0 : 1; |
||||
|
rec.displayed = exists ? it->second.record.displayed : 0; |
||||
|
rec.extra1 = exists ? it->second.record.extra1 : 0; |
||||
|
rec.extra2 = exists ? it->second.record.extra2 : 0; |
||||
|
|
||||
|
auto& entry = items[key]; |
||||
|
entry.record = rec; |
||||
|
entry.payload = std::move(payload); |
||||
|
return entry; |
||||
|
} |
||||
|
|
||||
|
StoredNews& NewsStorage::UpsertRaw(const GithubNewsMeta& meta, std::vector<u8> payload) { |
||||
|
return Upsert(meta.news_id, "", meta.topic_id, static_cast<s64>(meta.published_at), std::move(payload)); |
||||
|
} |
||||
|
|
||||
|
std::vector<NewsRecord> NewsStorage::ListAll() const { |
||||
|
std::scoped_lock lk{mtx}; |
||||
|
|
||||
|
std::vector<NewsRecord> out; |
||||
|
out.reserve(items.size()); |
||||
|
for (const auto& [_, v] : items) { |
||||
|
out.push_back(v.record); |
||||
|
} |
||||
|
|
||||
|
std::sort(out.begin(), out.end(), [](const auto& a, const auto& b) { |
||||
|
return a.received_time > b.received_time; |
||||
|
}); |
||||
|
return out; |
||||
|
} |
||||
|
|
||||
|
std::optional<StoredNews> NewsStorage::FindByNewsId(std::string_view news_id, |
||||
|
std::string_view user_id) const { |
||||
|
std::scoped_lock lk{mtx}; |
||||
|
|
||||
|
if (auto it = items.find(MakeKey(news_id, user_id)); it != items.end()) { |
||||
|
return it->second; |
||||
|
} |
||||
|
if (!user_id.empty()) { |
||||
|
if (auto it = items.find(MakeKey(news_id, "")); it != items.end()) { |
||||
|
return it->second; |
||||
|
} |
||||
|
} |
||||
|
return std::nullopt; |
||||
|
} |
||||
|
|
||||
|
bool NewsStorage::UpdateRecord(std::string_view news_id, std::string_view user_id, |
||||
|
const std::function<void(NewsRecord&)>& updater) { |
||||
|
std::scoped_lock lk{mtx}; |
||||
|
|
||||
|
if (auto it = items.find(MakeKey(news_id, user_id)); it != items.end()) { |
||||
|
updater(it->second.record); |
||||
|
return true; |
||||
|
} |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
void NewsStorage::MarkAsRead(std::string_view news_id) { |
||||
|
std::scoped_lock lk{mtx}; |
||||
|
for (auto& [_, entry] : items) { |
||||
|
if (std::string_view(entry.record.news_id.data()) == news_id) { |
||||
|
entry.record.read = 1; |
||||
|
entry.record.newly = 0; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
auto ids = LoadReadIds(); |
||||
|
ids.insert(std::string(news_id)); |
||||
|
SaveReadIds(ids); |
||||
|
} |
||||
|
|
||||
|
size_t NewsStorage::GetAndIncrementOpenCounter() { |
||||
|
std::scoped_lock lk{mtx}; |
||||
|
return open_counter++; |
||||
|
} |
||||
|
|
||||
|
void NewsStorage::ResetOpenCounter() { |
||||
|
std::scoped_lock lk{mtx}; |
||||
|
open_counter = 0; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
} // namespace Service::News
|
||||
@ -0,0 +1,100 @@ |
|||||
|
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project |
||||
|
// SPDX-License-Identifier: GPL-3.0-or-later |
||||
|
|
||||
|
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project |
||||
|
// SPDX-License-Identifier: GPL-3.0-or-later |
||||
|
|
||||
|
#pragma once |
||||
|
|
||||
|
#include <array> |
||||
|
#include <functional> |
||||
|
#include <mutex> |
||||
|
#include <optional> |
||||
|
#include <span> |
||||
|
#include <string> |
||||
|
#include <unordered_map> |
||||
|
#include <vector> |
||||
|
|
||||
|
#include "common/common_types.h" |
||||
|
|
||||
|
namespace Service::News { |
||||
|
|
||||
|
struct GithubNewsMeta { |
||||
|
std::string news_id; |
||||
|
std::string topic_id; |
||||
|
u64 published_at{}; |
||||
|
u64 pickup_limit{}; |
||||
|
u64 essential_pickup_limit{}; |
||||
|
u64 expire_at{}; |
||||
|
u32 priority{}; |
||||
|
u32 deletion_priority{100}; |
||||
|
u32 decoration_type{1}; |
||||
|
u32 opted_in{1}; |
||||
|
u32 essential_pickup_limit_flag{1}; |
||||
|
u32 category{}; |
||||
|
u32 language_mask{1}; |
||||
|
}; |
||||
|
|
||||
|
struct NewsRecordV1 { |
||||
|
std::array<char, 24> news_id{}; |
||||
|
std::array<char, 24> user_id{}; |
||||
|
s64 received_time{}; |
||||
|
s32 read{}; |
||||
|
s32 newly{}; |
||||
|
s32 displayed{}; |
||||
|
s32 extra1{}; |
||||
|
}; |
||||
|
static_assert(sizeof(NewsRecordV1) == 72); |
||||
|
|
||||
|
struct NewsRecord { |
||||
|
std::array<char, 24> news_id{}; |
||||
|
std::array<char, 24> user_id{}; |
||||
|
std::array<char, 32> topic_id{}; |
||||
|
s64 received_time{}; |
||||
|
std::array<u8, 12> _pad1{}; |
||||
|
s32 read{}; |
||||
|
s32 newly{}; |
||||
|
s32 displayed{}; |
||||
|
std::array<u8, 8> _pad2{}; |
||||
|
s32 extra1{}; |
||||
|
s32 extra2{}; |
||||
|
}; |
||||
|
static_assert(sizeof(NewsRecord) == 128); |
||||
|
|
||||
|
struct StoredNews { |
||||
|
NewsRecord record{}; |
||||
|
std::vector<u8> payload; |
||||
|
}; |
||||
|
|
||||
|
class NewsStorage { |
||||
|
public: |
||||
|
static NewsStorage& Instance(); |
||||
|
|
||||
|
void Clear(); |
||||
|
StoredNews& Upsert(std::string_view news_id, std::string_view user_id, |
||||
|
std::string_view topic_id, s64 time, std::vector<u8> payload); |
||||
|
StoredNews& UpsertRaw(const GithubNewsMeta& meta, std::vector<u8> payload); |
||||
|
|
||||
|
std::vector<NewsRecord> ListAll() const; |
||||
|
std::optional<StoredNews> FindByNewsId(std::string_view news_id, |
||||
|
std::string_view user_id = {}) const; |
||||
|
bool UpdateRecord(std::string_view news_id, std::string_view user_id, |
||||
|
const std::function<void(NewsRecord&)>& updater); |
||||
|
void MarkAsRead(std::string_view news_id); |
||||
|
|
||||
|
size_t GetAndIncrementOpenCounter(); |
||||
|
void ResetOpenCounter(); |
||||
|
|
||||
|
private: |
||||
|
NewsStorage() = default; |
||||
|
|
||||
|
static std::string MakeKey(std::string_view news_id, std::string_view user_id); |
||||
|
static void CopyZ(std::span<char> dst, std::string_view src); |
||||
|
static s64 Now(); |
||||
|
|
||||
|
mutable std::mutex mtx; |
||||
|
std::unordered_map<std::string, StoredNews> items; |
||||
|
size_t open_counter{}; |
||||
|
}; |
||||
|
|
||||
|
} // namespace Service::News |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue