[bcat/news/web/am] Implement news applet, proper TLV return, external web browser URL and qlaunch app sorting (#3308)
This pulls eden releases changelog & text from our github releases. We don't store the msgpack file but rather generate them in-memory for the News Applet. Uses cache folder. Files generated are: - cache/news/github_releases.json - cache/news/eden_logo.jpg - cache/news/news_read Additional changes: - Proper TLV returning for online web applet, to open external URL - Add applet type `LHub` to properly close, as it also uses TLV return - qlaunch app sorting, adds another cached .json to track last launched app timestamps and sort them accordingly Co-authored-by: crueter <crueter@eden-emu.dev> Co-authored-by: DraVee <dravee@eden-emu.dev> Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3308 Reviewed-by: MaranBr <maranbr@eden-emu.dev> Reviewed-by: DraVee <dravee@eden-emu.dev> Reviewed-by: Lizzie <lizzie@eden-emu.dev> Co-authored-by: Maufeat <sahyno1996@gmail.com> Co-committed-by: Maufeat <sahyno1996@gmail.com>
This commit is contained in:
parent
b7417f68ce
commit
ae501e256e
35 changed files with 2979 additions and 316 deletions
|
|
@ -14,8 +14,7 @@ else()
|
|||
pkg_search_module(ZSTD QUIET IMPORTED_TARGET libzstd)
|
||||
find_package_handle_standard_args(zstd
|
||||
REQUIRED_VARS ZSTD_LINK_LIBRARIES
|
||||
VERSION_VAR ZSTD_VERSION
|
||||
)
|
||||
VERSION_VAR ZSTD_VERSION)
|
||||
endif()
|
||||
|
||||
if (zstd_FOUND AND NOT TARGET zstd::zstd)
|
||||
|
|
@ -36,4 +35,7 @@ if (NOT TARGET zstd::libzstd)
|
|||
else()
|
||||
add_library(zstd::libzstd ALIAS zstd::zstd)
|
||||
endif()
|
||||
elseif(YUZU_STATIC_BUILD AND TARGET zstd::libzstd_static)
|
||||
# zstd::libzstd links to shared zstd by default
|
||||
set_target_properties(zstd::libzstd PROPERTIES INTERFACE_LINK_LIBRARIES zstd::libzstd_static)
|
||||
endif()
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import org.yuzu.yuzu_emu.model.InstallResult
|
|||
import org.yuzu.yuzu_emu.model.Patch
|
||||
import org.yuzu.yuzu_emu.model.GameVerificationResult
|
||||
import org.yuzu.yuzu_emu.network.NetPlayManager
|
||||
import org.yuzu.yuzu_emu.applets.web.WebBrowser
|
||||
|
||||
/**
|
||||
* Class which contains methods that interact
|
||||
|
|
@ -457,6 +458,17 @@ object NativeLibrary {
|
|||
*/
|
||||
external fun setCurrentAppletId(appletId: Int)
|
||||
|
||||
/**
|
||||
* Launch external URL when Web applet and opens browser
|
||||
*
|
||||
* @param String URL
|
||||
*/
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun openExternalUrl(url: String) {
|
||||
WebBrowser.openExternal(url)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the cabinet mode for launching the cabinet applet.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.applets.web
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.annotation.Keep
|
||||
import org.yuzu.yuzu_emu.NativeLibrary
|
||||
import org.yuzu.yuzu_emu.utils.Log
|
||||
|
||||
/**
|
||||
Should run WebBrowser as a new intent.
|
||||
*/
|
||||
|
||||
@Keep
|
||||
object WebBrowser {
|
||||
@JvmStatic
|
||||
fun openExternal(url: String) {
|
||||
val activity = NativeLibrary.sEmulationActivity.get() ?: run {
|
||||
return
|
||||
}
|
||||
|
||||
activity.runOnUiThread {
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
activity.startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
Log.error("WebBrowser failed to launch $url: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -66,6 +66,7 @@
|
|||
#include "core/frontend/applets/profile_select.h"
|
||||
#include "core/frontend/applets/software_keyboard.h"
|
||||
#include "core/frontend/applets/web_browser.h"
|
||||
#include "common/android/applets/web_browser.h"
|
||||
#include "core/hle/service/am/applet_manager.h"
|
||||
#include "core/hle/service/am/frontend/applets.h"
|
||||
#include "core/hle/service/filesystem/filesystem.h"
|
||||
|
|
@ -275,6 +276,7 @@ Core::SystemResultStatus EmulationSession::InitializeEmulation(const std::string
|
|||
|
||||
// Initialize system.
|
||||
jauto android_keyboard = std::make_unique<Common::Android::SoftwareKeyboard::AndroidKeyboard>();
|
||||
jauto android_webapplet = std::make_unique<Common::Android::WebBrowser::AndroidWebBrowser>();
|
||||
m_software_keyboard = android_keyboard.get();
|
||||
m_system.SetShuttingDown(false);
|
||||
m_system.ApplySettings();
|
||||
|
|
@ -289,7 +291,7 @@ Core::SystemResultStatus EmulationSession::InitializeEmulation(const std::string
|
|||
nullptr, // Photo Viewer
|
||||
nullptr, // Profile Selector
|
||||
std::move(android_keyboard), // Software Keyboard
|
||||
nullptr, // Web Browser
|
||||
std::move(android_webapplet),// Web Browser
|
||||
nullptr, // Net Connect
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -5,243 +5,242 @@
|
|||
# GPL-2.0-or-later
|
||||
|
||||
if(DEFINED ENV{AZURECIREPO})
|
||||
set(BUILD_REPOSITORY $ENV{AZURECIREPO})
|
||||
set(BUILD_REPOSITORY $ENV{AZURECIREPO})
|
||||
endif()
|
||||
if(DEFINED ENV{TITLEBARFORMATIDLE})
|
||||
set(TITLE_BAR_FORMAT_IDLE $ENV{TITLEBARFORMATIDLE})
|
||||
set(TITLE_BAR_FORMAT_IDLE $ENV{TITLEBARFORMATIDLE})
|
||||
endif()
|
||||
if(DEFINED ENV{TITLEBARFORMATRUNNING})
|
||||
set(TITLE_BAR_FORMAT_RUNNING $ENV{TITLEBARFORMATRUNNING})
|
||||
set(TITLE_BAR_FORMAT_RUNNING $ENV{TITLEBARFORMATRUNNING})
|
||||
endif()
|
||||
if(DEFINED ENV{DISPLAYVERSION})
|
||||
set(DISPLAY_VERSION $ENV{DISPLAYVERSION})
|
||||
set(DISPLAY_VERSION $ENV{DISPLAYVERSION})
|
||||
endif()
|
||||
|
||||
include(GenerateSCMRev)
|
||||
|
||||
add_library(
|
||||
common STATIC
|
||||
address_space.cpp
|
||||
address_space.h
|
||||
algorithm.h
|
||||
alignment.h
|
||||
announce_multiplayer_room.h
|
||||
assert.cpp
|
||||
assert.h
|
||||
atomic_ops.h
|
||||
bit_field.h
|
||||
bit_util.h
|
||||
bounded_threadsafe_queue.h
|
||||
cityhash.cpp
|
||||
cityhash.h
|
||||
common_funcs.h
|
||||
common_types.h
|
||||
concepts.h
|
||||
container_hash.h
|
||||
demangle.cpp
|
||||
demangle.h
|
||||
detached_tasks.cpp
|
||||
detached_tasks.h
|
||||
device_power_state.cpp
|
||||
device_power_state.h
|
||||
div_ceil.h
|
||||
dynamic_library.cpp
|
||||
dynamic_library.h
|
||||
elf.h
|
||||
error.cpp
|
||||
error.h
|
||||
expected.h
|
||||
fiber.cpp
|
||||
fiber.h
|
||||
fixed_point.h
|
||||
free_region_manager.h
|
||||
fs/file.cpp
|
||||
fs/file.h
|
||||
fs/fs.cpp
|
||||
fs/fs.h
|
||||
fs/fs_paths.h
|
||||
fs/fs_types.h
|
||||
fs/fs_util.cpp
|
||||
fs/fs_util.h
|
||||
fs/path_util.cpp
|
||||
fs/path_util.h
|
||||
hash.h
|
||||
heap_tracker.cpp
|
||||
heap_tracker.h
|
||||
hex_util.cpp
|
||||
hex_util.h
|
||||
host_memory.cpp
|
||||
host_memory.h
|
||||
input.h
|
||||
intrusive_red_black_tree.h
|
||||
literals.h
|
||||
logging/backend.cpp
|
||||
logging/backend.h
|
||||
logging/filter.cpp
|
||||
logging/filter.h
|
||||
logging/formatter.h
|
||||
logging/log.h
|
||||
logging/log_entry.h
|
||||
logging/text_formatter.cpp
|
||||
logging/text_formatter.h
|
||||
logging/types.h
|
||||
lz4_compression.cpp
|
||||
lz4_compression.h
|
||||
make_unique_for_overwrite.h
|
||||
math_util.h
|
||||
memory_detect.cpp
|
||||
memory_detect.h
|
||||
multi_level_page_table.cpp
|
||||
multi_level_page_table.h
|
||||
overflow.h
|
||||
page_table.cpp
|
||||
page_table.h
|
||||
param_package.cpp
|
||||
param_package.h
|
||||
parent_of_member.h
|
||||
point.h
|
||||
quaternion.h
|
||||
range_map.h
|
||||
range_mutex.h
|
||||
range_sets.h
|
||||
range_sets.inc
|
||||
ring_buffer.h
|
||||
${CMAKE_CURRENT_BINARY_DIR}/scm_rev.cpp
|
||||
scm_rev.h
|
||||
scope_exit.h
|
||||
scratch_buffer.h
|
||||
settings.cpp
|
||||
settings.h
|
||||
settings_common.cpp
|
||||
settings_common.h
|
||||
settings_enums.h
|
||||
settings_input.cpp
|
||||
settings_input.h
|
||||
settings_setting.h
|
||||
slot_vector.h
|
||||
socket_types.h
|
||||
spin_lock.h
|
||||
stb.cpp
|
||||
stb.h
|
||||
steady_clock.cpp
|
||||
steady_clock.h
|
||||
stream.cpp
|
||||
stream.h
|
||||
string_util.cpp
|
||||
string_util.h
|
||||
swap.h
|
||||
thread.cpp
|
||||
thread.h
|
||||
thread_queue_list.h
|
||||
thread_worker.h
|
||||
threadsafe_queue.h
|
||||
time_zone.cpp
|
||||
time_zone.h
|
||||
tiny_mt.h
|
||||
tree.h
|
||||
typed_address.h
|
||||
uint128.h
|
||||
unique_function.h
|
||||
uuid.cpp
|
||||
uuid.h
|
||||
vector_math.h
|
||||
virtual_buffer.cpp
|
||||
virtual_buffer.h
|
||||
wall_clock.cpp
|
||||
wall_clock.h
|
||||
zstd_compression.cpp
|
||||
zstd_compression.h
|
||||
fs/ryujinx_compat.h fs/ryujinx_compat.cpp
|
||||
fs/symlink.h fs/symlink.cpp
|
||||
)
|
||||
common STATIC
|
||||
address_space.cpp
|
||||
address_space.h
|
||||
algorithm.h
|
||||
alignment.h
|
||||
announce_multiplayer_room.h
|
||||
assert.cpp
|
||||
assert.h
|
||||
atomic_ops.h
|
||||
bit_field.h
|
||||
bit_util.h
|
||||
bounded_threadsafe_queue.h
|
||||
cityhash.cpp
|
||||
cityhash.h
|
||||
common_funcs.h
|
||||
common_types.h
|
||||
concepts.h
|
||||
container_hash.h
|
||||
demangle.cpp
|
||||
demangle.h
|
||||
detached_tasks.cpp
|
||||
detached_tasks.h
|
||||
device_power_state.cpp
|
||||
device_power_state.h
|
||||
div_ceil.h
|
||||
dynamic_library.cpp
|
||||
dynamic_library.h
|
||||
elf.h
|
||||
error.cpp
|
||||
error.h
|
||||
expected.h
|
||||
fiber.cpp
|
||||
fiber.h
|
||||
fixed_point.h
|
||||
free_region_manager.h
|
||||
fs/file.cpp
|
||||
fs/file.h
|
||||
fs/fs.cpp
|
||||
fs/fs.h
|
||||
fs/fs_paths.h
|
||||
fs/fs_types.h
|
||||
fs/fs_util.cpp
|
||||
fs/fs_util.h
|
||||
fs/path_util.cpp
|
||||
fs/path_util.h
|
||||
hash.h
|
||||
heap_tracker.cpp
|
||||
heap_tracker.h
|
||||
hex_util.cpp
|
||||
hex_util.h
|
||||
host_memory.cpp
|
||||
host_memory.h
|
||||
input.h
|
||||
intrusive_red_black_tree.h
|
||||
literals.h
|
||||
logging/backend.cpp
|
||||
logging/backend.h
|
||||
logging/filter.cpp
|
||||
logging/filter.h
|
||||
logging/formatter.h
|
||||
logging/log.h
|
||||
logging/log_entry.h
|
||||
logging/text_formatter.cpp
|
||||
logging/text_formatter.h
|
||||
logging/types.h
|
||||
lz4_compression.cpp
|
||||
lz4_compression.h
|
||||
make_unique_for_overwrite.h
|
||||
math_util.h
|
||||
memory_detect.cpp
|
||||
memory_detect.h
|
||||
multi_level_page_table.cpp
|
||||
multi_level_page_table.h
|
||||
overflow.h
|
||||
page_table.cpp
|
||||
page_table.h
|
||||
param_package.cpp
|
||||
param_package.h
|
||||
parent_of_member.h
|
||||
point.h
|
||||
quaternion.h
|
||||
range_map.h
|
||||
range_mutex.h
|
||||
range_sets.h
|
||||
range_sets.inc
|
||||
ring_buffer.h
|
||||
${CMAKE_CURRENT_BINARY_DIR}/scm_rev.cpp
|
||||
scm_rev.h
|
||||
scope_exit.h
|
||||
scratch_buffer.h
|
||||
settings.cpp
|
||||
settings.h
|
||||
settings_common.cpp
|
||||
settings_common.h
|
||||
settings_enums.h
|
||||
settings_input.cpp
|
||||
settings_input.h
|
||||
settings_setting.h
|
||||
slot_vector.h
|
||||
socket_types.h
|
||||
spin_lock.h
|
||||
stb.cpp
|
||||
stb.h
|
||||
steady_clock.cpp
|
||||
steady_clock.h
|
||||
stream.cpp
|
||||
stream.h
|
||||
string_util.cpp
|
||||
string_util.h
|
||||
swap.h
|
||||
thread.cpp
|
||||
thread.h
|
||||
thread_queue_list.h
|
||||
thread_worker.h
|
||||
threadsafe_queue.h
|
||||
time_zone.cpp
|
||||
time_zone.h
|
||||
tiny_mt.h
|
||||
tree.h
|
||||
typed_address.h
|
||||
uint128.h
|
||||
unique_function.h
|
||||
uuid.cpp
|
||||
uuid.h
|
||||
vector_math.h
|
||||
virtual_buffer.cpp
|
||||
virtual_buffer.h
|
||||
wall_clock.cpp
|
||||
wall_clock.h
|
||||
zstd_compression.cpp
|
||||
zstd_compression.h
|
||||
fs/ryujinx_compat.h fs/ryujinx_compat.cpp
|
||||
fs/symlink.h fs/symlink.cpp)
|
||||
|
||||
if(WIN32)
|
||||
target_sources(common PRIVATE windows/timer_resolution.cpp
|
||||
windows/timer_resolution.h)
|
||||
target_link_libraries(common PRIVATE ntdll)
|
||||
target_sources(common PRIVATE windows/timer_resolution.cpp
|
||||
windows/timer_resolution.h)
|
||||
target_link_libraries(common PRIVATE ntdll)
|
||||
endif()
|
||||
|
||||
if(NOT WIN32)
|
||||
target_sources(common PRIVATE signal_chain.cpp signal_chain.h)
|
||||
target_sources(common PRIVATE signal_chain.cpp signal_chain.h)
|
||||
endif()
|
||||
|
||||
if(ANDROID)
|
||||
target_sources(
|
||||
common
|
||||
PUBLIC fs/fs_android.cpp
|
||||
fs/fs_android.h
|
||||
android/android_common.cpp
|
||||
android/android_common.h
|
||||
android/id_cache.cpp
|
||||
android/id_cache.h
|
||||
android/multiplayer/multiplayer.cpp
|
||||
android/multiplayer/multiplayer.h
|
||||
android/applets/software_keyboard.cpp
|
||||
android/applets/software_keyboard.h)
|
||||
target_sources(
|
||||
common
|
||||
PUBLIC fs/fs_android.cpp
|
||||
fs/fs_android.h
|
||||
android/android_common.cpp
|
||||
android/android_common.h
|
||||
android/id_cache.cpp
|
||||
android/id_cache.h
|
||||
android/multiplayer/multiplayer.cpp
|
||||
android/multiplayer/multiplayer.h
|
||||
android/applets/software_keyboard.cpp
|
||||
android/applets/software_keyboard.h
|
||||
android/applets/web_browser.cpp
|
||||
android/applets/web_browser.h)
|
||||
endif()
|
||||
|
||||
if(ARCHITECTURE_x86_64)
|
||||
target_sources(
|
||||
common
|
||||
PRIVATE x64/cpu_detect.cpp
|
||||
x64/cpu_detect.h
|
||||
x64/cpu_wait.cpp
|
||||
x64/cpu_wait.h
|
||||
x64/native_clock.cpp
|
||||
x64/native_clock.h
|
||||
x64/rdtsc.cpp
|
||||
x64/rdtsc.h
|
||||
x64/xbyak_abi.h
|
||||
x64/xbyak_util.h)
|
||||
target_link_libraries(common PRIVATE xbyak::xbyak)
|
||||
target_sources(
|
||||
common
|
||||
PRIVATE x64/cpu_detect.cpp
|
||||
x64/cpu_detect.h
|
||||
x64/cpu_wait.cpp
|
||||
x64/cpu_wait.h
|
||||
x64/native_clock.cpp
|
||||
x64/native_clock.h
|
||||
x64/rdtsc.cpp
|
||||
x64/rdtsc.h
|
||||
x64/xbyak_abi.h
|
||||
x64/xbyak_util.h)
|
||||
target_link_libraries(common PRIVATE xbyak::xbyak)
|
||||
endif()
|
||||
|
||||
if(HAS_NCE)
|
||||
target_sources(common PRIVATE arm64/native_clock.cpp arm64/native_clock.h)
|
||||
target_sources(common PRIVATE arm64/native_clock.cpp arm64/native_clock.h)
|
||||
endif()
|
||||
|
||||
if(MSVC)
|
||||
target_compile_definitions(
|
||||
common
|
||||
PRIVATE # The standard library doesn't provide any replacement for codecvt
|
||||
# yet so we can disable this deprecation warning for the time being.
|
||||
_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING)
|
||||
target_compile_options(
|
||||
common
|
||||
PRIVATE /we4242 # 'identifier': conversion from 'type1' to 'type2', possible
|
||||
# loss of data
|
||||
/we4254 # 'operator': conversion from 'type1:field_bits' to
|
||||
# 'type2:field_bits', possible loss of data
|
||||
/we4800 # Implicit conversion from 'type' to bool. Possible
|
||||
# information loss
|
||||
)
|
||||
target_compile_definitions(
|
||||
common
|
||||
PRIVATE # The standard library doesn't provide any replacement for codecvt
|
||||
# yet so we can disable this deprecation warning for the time being.
|
||||
_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING)
|
||||
target_compile_options(
|
||||
common
|
||||
PRIVATE /we4242 # 'identifier': conversion from 'type1' to 'type2', possible
|
||||
# loss of data
|
||||
/we4254 # 'operator': conversion from 'type1:field_bits' to
|
||||
# 'type2:field_bits', possible loss of data
|
||||
/we4800 # Implicit conversion from 'type' to bool. Possible
|
||||
# information loss
|
||||
)
|
||||
else()
|
||||
set_source_files_properties(
|
||||
stb.cpp
|
||||
PROPERTIES
|
||||
COMPILE_OPTIONS
|
||||
"-Wno-implicit-fallthrough;-Wno-missing-declarations;-Wno-missing-field-initializers"
|
||||
)
|
||||
set_source_files_properties(
|
||||
stb.cpp
|
||||
PROPERTIES
|
||||
COMPILE_OPTIONS
|
||||
"-Wno-implicit-fallthrough;-Wno-missing-declarations;-Wno-missing-field-initializers")
|
||||
|
||||
# Get around GCC failing with intrinsics in Debug
|
||||
if(CXX_GCC AND CMAKE_BUILD_TYPE MATCHES "Debug")
|
||||
set_property(
|
||||
SOURCE stb.cpp
|
||||
APPEND
|
||||
PROPERTY COMPILE_OPTIONS ";-O2")
|
||||
endif()
|
||||
# Get around GCC failing with intrinsics in Debug
|
||||
if(CXX_GCC AND CMAKE_BUILD_TYPE MATCHES "Debug")
|
||||
set_property(
|
||||
SOURCE stb.cpp
|
||||
APPEND
|
||||
PROPERTY COMPILE_OPTIONS ";-O2")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(CXX_CLANG)
|
||||
target_compile_options(common PRIVATE -fsized-deallocation
|
||||
-Werror=unreachable-code-aggressive)
|
||||
target_compile_definitions(
|
||||
common
|
||||
PRIVATE
|
||||
# Clang 14 and earlier have errors when explicitly instantiating
|
||||
# Settings::Setting
|
||||
$<$<VERSION_LESS:$<CXX_COMPILER_VERSION>,15>:CANNOT_EXPLICITLY_INSTANTIATE>
|
||||
)
|
||||
target_compile_options(common PRIVATE -fsized-deallocation
|
||||
-Werror=unreachable-code-aggressive)
|
||||
target_compile_definitions(
|
||||
common
|
||||
PRIVATE
|
||||
# Clang 14 and earlier have errors when explicitly instantiating
|
||||
# Settings::Setting
|
||||
$<$<VERSION_LESS:$<CXX_COMPILER_VERSION>,15>:CANNOT_EXPLICITLY_INSTANTIATE>)
|
||||
endif()
|
||||
|
||||
if (BOOST_NO_HEADERS)
|
||||
|
|
@ -260,8 +259,8 @@ target_link_libraries(common PUBLIC fmt::fmt stb::headers Threads::Threads)
|
|||
target_link_libraries(common PRIVATE lz4::lz4 LLVM::Demangle zstd::zstd)
|
||||
|
||||
if(ANDROID)
|
||||
# For ASharedMemory_create
|
||||
target_link_libraries(common PRIVATE android)
|
||||
# For ASharedMemory_create
|
||||
target_link_libraries(common PRIVATE android)
|
||||
endif()
|
||||
|
||||
create_target_directory_groups(common)
|
||||
|
|
|
|||
50
src/common/android/applets/web_browser.cpp
Normal file
50
src/common/android/applets/web_browser.cpp
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "common/android/android_common.h"
|
||||
#include "common/android/id_cache.h"
|
||||
#include "common/android/applets/web_browser.h"
|
||||
#include "common/logging/log.h"
|
||||
|
||||
static jclass s_native_library_class = nullptr;
|
||||
static jmethodID s_open_external_url = nullptr;
|
||||
|
||||
namespace Common::Android::WebBrowser {
|
||||
|
||||
void InitJNI(JNIEnv* env) {
|
||||
const jclass local = env->FindClass("org/yuzu/yuzu_emu/NativeLibrary");
|
||||
s_native_library_class = static_cast<jclass>(env->NewGlobalRef(local));
|
||||
env->DeleteLocalRef(local);
|
||||
s_open_external_url = env->GetStaticMethodID(s_native_library_class, "openExternalUrl", "(Ljava/lang/String;)V");
|
||||
}
|
||||
|
||||
void CleanupJNI(JNIEnv* env) {
|
||||
if (s_native_library_class != nullptr) {
|
||||
env->DeleteGlobalRef(s_native_library_class);
|
||||
s_native_library_class = nullptr;
|
||||
}
|
||||
s_open_external_url = nullptr;
|
||||
}
|
||||
|
||||
void AndroidWebBrowser::OpenLocalWebPage(const std::string& local_url, ExtractROMFSCallback extract_romfs_callback, OpenWebPageCallback callback) const {
|
||||
LOG_WARNING(Frontend, "(STUBBED)");
|
||||
callback(Service::AM::Frontend::WebExitReason::WindowClosed, "");
|
||||
}
|
||||
|
||||
void AndroidWebBrowser::OpenExternalWebPage(const std::string& external_url, OpenWebPageCallback callback) const {
|
||||
// do a dedicated thread, calling from the this thread crashed CPU fiber.
|
||||
Common::Android::RunJNIOnFiber<void>([&](JNIEnv* env) {
|
||||
if (env != nullptr && s_native_library_class != nullptr && s_open_external_url != nullptr) {
|
||||
const jstring j_url = Common::Android::ToJString(env, external_url);
|
||||
env->CallStaticVoidMethod(s_native_library_class, s_open_external_url, j_url);
|
||||
env->DeleteLocalRef(j_url);
|
||||
} else {
|
||||
LOG_ERROR(Frontend, "JNI not initialized, cannot open {}", external_url);
|
||||
}
|
||||
return;
|
||||
});
|
||||
|
||||
callback(Service::AM::Frontend::WebExitReason::WindowClosed, external_url);
|
||||
}
|
||||
|
||||
} // namespace Common::Android::WebBrowser
|
||||
30
src/common/android/applets/web_browser.h
Normal file
30
src/common/android/applets/web_browser.h
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <jni.h>
|
||||
#include <string>
|
||||
|
||||
#include "core/frontend/applets/web_browser.h"
|
||||
|
||||
namespace Common::Android::WebBrowser {
|
||||
|
||||
class AndroidWebBrowser final : public Core::Frontend::WebBrowserApplet {
|
||||
public:
|
||||
~AndroidWebBrowser() override = default;
|
||||
|
||||
void Close() const override {}
|
||||
|
||||
void OpenLocalWebPage(const std::string& local_url,
|
||||
ExtractROMFSCallback extract_romfs_callback,
|
||||
OpenWebPageCallback callback) const override;
|
||||
|
||||
void OpenExternalWebPage(const std::string& external_url,
|
||||
OpenWebPageCallback callback) const override;
|
||||
};
|
||||
|
||||
void InitJNI(JNIEnv* env);
|
||||
void CleanupJNI(JNIEnv* env);
|
||||
|
||||
} // namespace Common::Android::WebBrowser
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
#include <jni.h>
|
||||
|
||||
#include "applets/software_keyboard.h"
|
||||
#include "applets/web_browser.h"
|
||||
#include "common/android/id_cache.h"
|
||||
#include "common/assert.h"
|
||||
#include "common/fs/fs_android.h"
|
||||
|
|
@ -602,6 +603,7 @@ namespace Common::Android {
|
|||
|
||||
// Initialize applets
|
||||
Common::Android::SoftwareKeyboard::InitJNI(env);
|
||||
Common::Android::WebBrowser::InitJNI(env);
|
||||
|
||||
return JNI_VERSION;
|
||||
}
|
||||
|
|
@ -631,6 +633,7 @@ namespace Common::Android {
|
|||
|
||||
// UnInitialize applets
|
||||
SoftwareKeyboard::CleanupJNI(env);
|
||||
WebBrowser::CleanupJNI(env);
|
||||
|
||||
AndroidMultiplayer::NetworkShutdown();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -563,8 +563,14 @@ add_library(core STATIC
|
|||
hle/service/bcat/delivery_cache_storage_service.h
|
||||
hle/service/bcat/news/newly_arrived_event_holder.cpp
|
||||
hle/service/bcat/news/newly_arrived_event_holder.h
|
||||
hle/service/bcat/news/msgpack.cpp
|
||||
hle/service/bcat/news/msgpack.h
|
||||
hle/service/bcat/news/news_data_service.cpp
|
||||
hle/service/bcat/news/news_data_service.h
|
||||
hle/service/bcat/news/builtin_news.cpp
|
||||
hle/service/bcat/news/builtin_news.h
|
||||
hle/service/bcat/news/news_storage.cpp
|
||||
hle/service/bcat/news/news_storage.h
|
||||
hle/service/bcat/news/news_database_service.cpp
|
||||
hle/service/bcat/news/news_database_service.h
|
||||
hle/service/bcat/news/news_service.cpp
|
||||
|
|
@ -1125,6 +1131,8 @@ add_library(core STATIC
|
|||
internal_network/sockets.h
|
||||
internal_network/wifi_scanner.cpp
|
||||
internal_network/wifi_scanner.h
|
||||
launch_timestamp_cache.cpp
|
||||
launch_timestamp_cache.h
|
||||
loader/deconstructed_rom_directory.cpp
|
||||
loader/deconstructed_rom_directory.h
|
||||
loader/kip.cpp
|
||||
|
|
@ -1157,8 +1165,7 @@ add_library(core STATIC
|
|||
tools/freezer.cpp
|
||||
tools/freezer.h
|
||||
tools/renderdoc.cpp
|
||||
tools/renderdoc.h
|
||||
)
|
||||
tools/renderdoc.h)
|
||||
|
||||
if (ENABLE_WIFI_SCAN)
|
||||
# find_package(libiw REQUIRED)
|
||||
|
|
@ -1188,8 +1195,7 @@ else()
|
|||
-Werror=conversion
|
||||
-Wno-sign-conversion
|
||||
-Wno-cast-function-type
|
||||
$<$<CXX_COMPILER_ID:Clang>:-fsized-deallocation>
|
||||
)
|
||||
$<$<CXX_COMPILER_ID:Clang>:-fsized-deallocation>)
|
||||
# pre-clang19 will spam with "OH DID YOU MEAN THIS?" otherwise...
|
||||
if (CXX_CLANG AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 19)
|
||||
target_compile_options(core PRIVATE -Wno-cast-function-type-mismatch)
|
||||
|
|
@ -1205,10 +1211,13 @@ else()
|
|||
target_link_libraries(core PUBLIC Boost::headers)
|
||||
endif()
|
||||
|
||||
target_link_libraries(core PRIVATE fmt::fmt nlohmann_json::nlohmann_json RenderDoc::API MbedTLS::mbedcrypto${MBEDTLS_LIB_SUFFIX} MbedTLS::mbedtls${MBEDTLS_LIB_SUFFIX})
|
||||
# if (MINGW)
|
||||
# target_link_libraries(core PRIVATE ws2_32 mswsock wlanapi)
|
||||
# endif()
|
||||
target_link_libraries(core PRIVATE
|
||||
fmt::fmt
|
||||
nlohmann_json::nlohmann_json
|
||||
RenderDoc::API
|
||||
MbedTLS::mbedcrypto${MBEDTLS_LIB_SUFFIX}
|
||||
MbedTLS::mbedtls${MBEDTLS_LIB_SUFFIX}
|
||||
httplib::httplib)
|
||||
|
||||
if (ENABLE_WEB_SERVICE)
|
||||
target_compile_definitions(core PUBLIC ENABLE_WEB_SERVICE)
|
||||
|
|
@ -1230,8 +1239,7 @@ if (HAS_NCE)
|
|||
arm/nce/interpreter_visitor.h
|
||||
arm/nce/patcher.cpp
|
||||
arm/nce/patcher.h
|
||||
arm/nce/visitor_base.h
|
||||
)
|
||||
arm/nce/visitor_base.h)
|
||||
target_link_libraries(core PRIVATE merry::oaknut)
|
||||
endif()
|
||||
|
||||
|
|
@ -1251,8 +1259,7 @@ if (ARCHITECTURE_x86_64 OR ARCHITECTURE_arm64)
|
|||
hle/service/jit/jit_context.cpp
|
||||
hle/service/jit/jit_context.h
|
||||
hle/service/jit/jit.cpp
|
||||
hle/service/jit/jit.h
|
||||
)
|
||||
hle/service/jit/jit.h)
|
||||
target_link_libraries(core PRIVATE dynarmic::dynarmic)
|
||||
endif()
|
||||
|
||||
|
|
@ -1263,6 +1270,7 @@ if(ENABLE_OPENSSL)
|
|||
find_package(OpenSSL REQUIRED)
|
||||
|
||||
target_link_libraries(core PRIVATE OpenSSL::SSL OpenSSL::Crypto)
|
||||
target_compile_definitions(core PRIVATE CPPHTTPLIB_OPENSSL_SUPPORT)
|
||||
elseif (APPLE)
|
||||
target_sources(core PRIVATE
|
||||
hle/service/ssl/ssl_backend_securetransport.cpp)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@
|
|||
#include "common/string_util.h"
|
||||
#include "core/arm/exclusive_monitor.h"
|
||||
#include "core/core.h"
|
||||
|
||||
#include "launch_timestamp_cache.h"
|
||||
#include "core/core_timing.h"
|
||||
#include "core/cpu_manager.h"
|
||||
#include "core/debugger/debugger.h"
|
||||
|
|
@ -323,6 +325,9 @@ struct System::Impl {
|
|||
|
||||
LOG_INFO(Core, "Loading {} ({:016X}) ...", name, params.program_id);
|
||||
|
||||
// Track launch time for frontend launches
|
||||
LaunchTimestampCache::SaveLaunchTimestamp(params.program_id);
|
||||
|
||||
// Make the process created be the application
|
||||
kernel.MakeApplicationProcess(process->GetHandle());
|
||||
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ enum class AppletId : u32 {
|
|||
LoginShare = 0x18,
|
||||
WebAuth = 0x19,
|
||||
MyPage = 0x1A,
|
||||
Lhub = 0x35
|
||||
};
|
||||
|
||||
enum class AppletProgramId : u64 {
|
||||
|
|
|
|||
|
|
@ -236,10 +236,6 @@ WebBrowser::WebBrowser(Core::System& system_, std::shared_ptr<Applet> applet_,
|
|||
WebBrowser::~WebBrowser() = default;
|
||||
|
||||
void WebBrowser::Initialize() {
|
||||
if (Settings::values.disable_web_applet) {
|
||||
return;
|
||||
}
|
||||
|
||||
FrontendApplet::Initialize();
|
||||
|
||||
LOG_INFO(Service_AM, "Initializing Web Browser Applet.");
|
||||
|
|
@ -264,6 +260,12 @@ void WebBrowser::Initialize() {
|
|||
LOG_DEBUG(Service_AM, "WebArgHeader: total_tlv_entries={}, shim_kind={}",
|
||||
web_arg_header.total_tlv_entries, web_arg_header.shim_kind);
|
||||
|
||||
if (Settings::values.disable_web_applet &&
|
||||
web_arg_header.shim_kind != ShimKind::Web &&
|
||||
web_arg_header.shim_kind != ShimKind::Lhub) {
|
||||
return;
|
||||
}
|
||||
|
||||
ExtractSharedFonts(system);
|
||||
|
||||
switch (web_arg_header.shim_kind) {
|
||||
|
|
@ -288,6 +290,9 @@ void WebBrowser::Initialize() {
|
|||
case ShimKind::Lobby:
|
||||
InitializeLobby();
|
||||
break;
|
||||
case ShimKind::Lhub:
|
||||
InitializeLhub();
|
||||
break;
|
||||
default:
|
||||
ASSERT_MSG(false, "Invalid ShimKind={}", web_arg_header.shim_kind);
|
||||
break;
|
||||
|
|
@ -303,8 +308,19 @@ void WebBrowser::ExecuteInteractive() {
|
|||
}
|
||||
|
||||
void WebBrowser::Execute() {
|
||||
if (web_arg_header.shim_kind == ShimKind::Web) {
|
||||
ExecuteWeb();
|
||||
return;
|
||||
}
|
||||
|
||||
if (web_arg_header.shim_kind == ShimKind::Lhub) {
|
||||
ExecuteLhub();
|
||||
return;
|
||||
}
|
||||
|
||||
if (Settings::values.disable_web_applet) {
|
||||
LOG_WARNING(Service_AM, "(STUBBED) called, Web Browser Applet is disabled");
|
||||
LOG_WARNING(Service_AM, "(STUBBED) called, Web Browser Applet is disabled. shim_kind={}",
|
||||
web_arg_header.shim_kind);
|
||||
WebBrowserExit(WebExitReason::EndButtonPressed);
|
||||
return;
|
||||
}
|
||||
|
|
@ -331,6 +347,9 @@ void WebBrowser::Execute() {
|
|||
case ShimKind::Lobby:
|
||||
ExecuteLobby();
|
||||
break;
|
||||
case ShimKind::Lhub:
|
||||
ExecuteLhub();
|
||||
break;
|
||||
default:
|
||||
ASSERT_MSG(false, "Invalid ShimKind={}", web_arg_header.shim_kind);
|
||||
WebBrowserExit(WebExitReason::EndButtonPressed);
|
||||
|
|
@ -351,17 +370,99 @@ void WebBrowser::ExtractOfflineRomFS() {
|
|||
}
|
||||
|
||||
void WebBrowser::WebBrowserExit(WebExitReason exit_reason, std::string last_url) {
|
||||
if ((web_arg_header.shim_kind == ShimKind::Share &&
|
||||
const bool use_tlv_output =
|
||||
(web_arg_header.shim_kind == ShimKind::Share &&
|
||||
web_applet_version >= WebAppletVersion::Version196608) ||
|
||||
(web_arg_header.shim_kind == ShimKind::Web &&
|
||||
web_applet_version >= WebAppletVersion::Version524288)) {
|
||||
// TODO: Push Output TLVs instead of a WebCommonReturnValue
|
||||
web_applet_version >= WebAppletVersion::Version524288) ||
|
||||
(web_arg_header.shim_kind == ShimKind::Lhub);
|
||||
|
||||
// https://switchbrew.org/wiki/Internet_Browser#TLVs
|
||||
if (use_tlv_output) {
|
||||
LOG_DEBUG(Service_AM, "Using TLV output: exit_reason={}, last_url={}, last_url_size={}",
|
||||
exit_reason, last_url, last_url.size());
|
||||
|
||||
// storage size for TLVs is 0x2000 bytes (as per switchbrew documentation)
|
||||
constexpr size_t TLV_STORAGE_SIZE = 0x2000;
|
||||
std::vector<u8> out_data(TLV_STORAGE_SIZE, 0);
|
||||
|
||||
size_t current_offset = sizeof(WebArgHeader);
|
||||
u16 tlv_count = 0;
|
||||
|
||||
// align and matchng TLV struct alignment
|
||||
auto align_offset = [](size_t offset) -> size_t {
|
||||
return (offset + 7) & ~static_cast<size_t>(7);
|
||||
};
|
||||
|
||||
// 0x1 ShareExitReason
|
||||
{
|
||||
WebArgOutputTLV tlv{};
|
||||
tlv.output_tlv_type = WebArgOutputTLVType::ShareExitReason;
|
||||
tlv.arg_data_size = sizeof(u32);
|
||||
|
||||
std::memcpy(out_data.data() + current_offset, &tlv, sizeof(WebArgOutputTLV));
|
||||
current_offset += sizeof(WebArgOutputTLV);
|
||||
|
||||
const u32 exit_reason_value = static_cast<u32>(exit_reason);
|
||||
std::memcpy(out_data.data() + current_offset, &exit_reason_value, sizeof(u32));
|
||||
current_offset += sizeof(u32);
|
||||
|
||||
current_offset = align_offset(current_offset);
|
||||
tlv_count++;
|
||||
}
|
||||
|
||||
// 0x2 LastUrl
|
||||
{
|
||||
WebArgOutputTLV tlv{};
|
||||
tlv.output_tlv_type = WebArgOutputTLVType::LastURL;
|
||||
const u16 url_data_size = static_cast<u16>(last_url.size() + 1);
|
||||
tlv.arg_data_size = url_data_size;
|
||||
|
||||
std::memcpy(out_data.data() + current_offset, &tlv, sizeof(WebArgOutputTLV));
|
||||
current_offset += sizeof(WebArgOutputTLV);
|
||||
|
||||
// null terminator
|
||||
std::memcpy(out_data.data() + current_offset, last_url.c_str(), last_url.size() + 1);
|
||||
current_offset += url_data_size;
|
||||
current_offset = align_offset(current_offset);
|
||||
tlv_count++;
|
||||
}
|
||||
|
||||
// 0x3 LastUrlSize
|
||||
{
|
||||
WebArgOutputTLV tlv{};
|
||||
tlv.output_tlv_type = WebArgOutputTLVType::LastURLSize;
|
||||
tlv.arg_data_size = sizeof(u64);
|
||||
|
||||
std::memcpy(out_data.data() + current_offset, &tlv, sizeof(WebArgOutputTLV));
|
||||
current_offset += sizeof(WebArgOutputTLV);
|
||||
|
||||
const u64 url_size = last_url.size();
|
||||
std::memcpy(out_data.data() + current_offset, &url_size, sizeof(u64));
|
||||
current_offset += sizeof(u64);
|
||||
tlv_count++;
|
||||
}
|
||||
|
||||
WebArgHeader out_header{};
|
||||
out_header.total_tlv_entries = tlv_count;
|
||||
out_header.shim_kind = web_arg_header.shim_kind;
|
||||
std::memcpy(out_data.data(), &out_header, sizeof(WebArgHeader));
|
||||
|
||||
LOG_DEBUG(Service_AM, "TLV output: total_size={}, tlv_count={}, used_offset={}",
|
||||
out_data.size(), tlv_count, current_offset);
|
||||
|
||||
complete = true;
|
||||
PushOutData(std::make_shared<IStorage>(system, std::move(out_data)));
|
||||
Exit();
|
||||
return;
|
||||
}
|
||||
|
||||
WebCommonReturnValue web_common_return_value;
|
||||
// for old browser, keep old return, use WebCommonReturnValue
|
||||
WebCommonReturnValue web_common_return_value{};
|
||||
|
||||
web_common_return_value.exit_reason = exit_reason;
|
||||
std::memcpy(&web_common_return_value.last_url, last_url.data(), last_url.size());
|
||||
std::memcpy(&web_common_return_value.last_url, last_url.data(),
|
||||
std::min(last_url.size(), web_common_return_value.last_url.size()));
|
||||
web_common_return_value.last_url_size = last_url.size();
|
||||
|
||||
LOG_DEBUG(Service_AM, "WebCommonReturnValue: exit_reason={}, last_url={}, last_url_size={}",
|
||||
|
|
@ -516,4 +617,13 @@ void WebBrowser::ExecuteLobby() {
|
|||
LOG_WARNING(Service_AM, "(STUBBED) called, Lobby Applet is not implemented");
|
||||
WebBrowserExit(WebExitReason::EndButtonPressed);
|
||||
}
|
||||
|
||||
void WebBrowser::InitializeLhub() {}
|
||||
|
||||
void WebBrowser::ExecuteLhub() {
|
||||
LOG_INFO(Service_AM, "(STUBBED) called, Lhub Applet is not implemented");
|
||||
WebBrowserExit(WebExitReason::EndButtonPressed);
|
||||
}
|
||||
|
||||
|
||||
} // namespace Service::AM::Frontend
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
|
|
@ -53,6 +56,7 @@ private:
|
|||
void InitializeWeb();
|
||||
void InitializeWifi();
|
||||
void InitializeLobby();
|
||||
void InitializeLhub();
|
||||
|
||||
// Executors for the various types of browser applets
|
||||
void ExecuteShop();
|
||||
|
|
@ -62,6 +66,7 @@ private:
|
|||
void ExecuteWeb();
|
||||
void ExecuteWifi();
|
||||
void ExecuteLobby();
|
||||
void ExecuteLhub();
|
||||
|
||||
const Core::Frontend::WebBrowserApplet& frontend;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
|
|
@ -30,6 +33,7 @@ enum class ShimKind : u32 {
|
|||
Web = 5,
|
||||
Wifi = 6,
|
||||
Lobby = 7,
|
||||
Lhub = 8,
|
||||
};
|
||||
|
||||
enum class WebExitReason : u32 {
|
||||
|
|
|
|||
|
|
@ -237,13 +237,15 @@ std::shared_ptr<FrontendApplet> FrontendAppletHolder::GetApplet(std::shared_ptr<
|
|||
case AppletId::OfflineWeb:
|
||||
case AppletId::LoginShare:
|
||||
case AppletId::WebAuth:
|
||||
case AppletId::Lhub:
|
||||
return std::make_shared<WebBrowser>(system, applet, mode, *frontend.web_browser);
|
||||
case AppletId::PhotoViewer:
|
||||
return std::make_shared<PhotoViewer>(system, applet, mode, *frontend.photo_viewer);
|
||||
case AppletId::NetConnect:
|
||||
return std::make_shared<NetConnect>(system, applet, mode, *frontend.net_connect);
|
||||
default:
|
||||
LOG_ERROR(Service_AM, "No backend implementation exists for applet_id={:02X}. Falling back to stub applet", static_cast<u8>(id));
|
||||
LOG_ERROR(Service_AM, "No backend implementation exists for applet_id={:02X} program_id={:016X}"
|
||||
"Falling back to stub applet", static_cast<u8>(id), applet->program_id);
|
||||
return std::make_shared<StubApplet>(system, applet, id, mode);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
#include "core/hle/service/am/window_system.h"
|
||||
#include "core/hle/service/cmif_serialization.h"
|
||||
#include "core/loader/loader.h"
|
||||
#include "core/launch_timestamp_cache.h"
|
||||
|
||||
namespace Service::AM {
|
||||
|
||||
|
|
@ -72,6 +73,7 @@ IApplicationCreator::~IApplicationCreator() = default;
|
|||
Result IApplicationCreator::CreateApplication(
|
||||
Out<SharedPointer<IApplicationAccessor>> out_application_accessor, u64 application_id) {
|
||||
LOG_INFO(Service_NS, "called, application_id={:016X}", application_id);
|
||||
Core::LaunchTimestampCache::SaveLaunchTimestamp(application_id);
|
||||
R_RETURN(
|
||||
CreateGuestApplication(out_application_accessor, system, m_window_system, application_id));
|
||||
}
|
||||
|
|
@ -103,6 +105,7 @@ Result IApplicationCreator::CreateSystemApplication(
|
|||
|
||||
*out_application_accessor =
|
||||
std::make_shared<IApplicationAccessor>(system, applet, m_window_system);
|
||||
Core::LaunchTimestampCache::SaveLaunchTimestamp(application_id);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
|
|
|
|||
555
src/core/hle/service/bcat/news/builtin_news.cpp
Normal file
555
src/core/hle/service/bcat/news/builtin_news.cpp
Normal file
|
|
@ -0,0 +1,555 @@
|
|||
// 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/path_util.h"
|
||||
#include "common/logging/log.h"
|
||||
|
||||
#include <boost/algorithm/string/replace.hpp>
|
||||
#include <boost/regex.hpp>
|
||||
#include <boost/regex/v5/regex_replace.hpp>
|
||||
#include <fmt/format.h>
|
||||
#include <httplib.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
#include <httplib.h>
|
||||
#endif
|
||||
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <future>
|
||||
#include <iomanip>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
#include <thread>
|
||||
|
||||
#ifdef YUZU_BUNDLED_OPENSSL
|
||||
#include <openssl/cert.h>
|
||||
#endif
|
||||
|
||||
namespace Service::News {
|
||||
namespace {
|
||||
|
||||
constexpr const char* GitHubAPI_EdenReleases = "/repos/eden-emulator/Releases/releases";
|
||||
|
||||
// Cached logo data
|
||||
std::vector<u8> default_logo_small;
|
||||
std::vector<u8> default_logo_large;
|
||||
bool default_logos_loaded = false;
|
||||
|
||||
std::unordered_map<std::string, std::vector<u8>> news_images_small;
|
||||
std::unordered_map<std::string, std::vector<u8>> news_images_large;
|
||||
std::mutex images_mutex;
|
||||
|
||||
|
||||
std::filesystem::path GetCachePath() {
|
||||
return Common::FS::GetEdenPath(Common::FS::EdenPath::CacheDir) / "news" / "github_releases.json";
|
||||
}
|
||||
|
||||
std::filesystem::path GetDefaultLogoPath(bool large) {
|
||||
return Common::FS::GetEdenPath(Common::FS::EdenPath::CacheDir) / "news" /
|
||||
(large ? "eden_logo_large.jpg" : "eden_logo_small.jpg");
|
||||
}
|
||||
|
||||
std::filesystem::path GetNewsImagePath(std::string_view news_id, bool large) {
|
||||
const std::string filename = fmt::format("{}_{}.jpg", news_id, large ? "large" : "small");
|
||||
return Common::FS::GetEdenPath(Common::FS::EdenPath::CacheDir) / "news" / "images" / filename;
|
||||
}
|
||||
|
||||
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> TryLoadFromDisk(const std::filesystem::path& path) {
|
||||
if (!std::filesystem::exists(path)) return {};
|
||||
|
||||
std::ifstream f(path, std::ios::binary | std::ios::ate);
|
||||
if (!f) return {};
|
||||
|
||||
const auto file_size = static_cast<std::streamsize>(f.tellg());
|
||||
if (file_size <= 0 || file_size > 10 * 1024 * 1024) return {};
|
||||
|
||||
f.seekg(0);
|
||||
std::vector<u8> data(static_cast<size_t>(file_size));
|
||||
if (!f.read(reinterpret_cast<char*>(data.data()), file_size)) return {};
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
std::vector<u8> DownloadImage(const std::string& url_path, const std::filesystem::path& cache_path) {
|
||||
LOG_INFO(Service_BCAT, "Downloading image: https://eden-emu.dev{}", url_path);
|
||||
|
||||
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
try {
|
||||
httplib::Client cli("https://eden-emu.dev");
|
||||
cli.set_follow_location(true);
|
||||
cli.set_connection_timeout(std::chrono::seconds(10));
|
||||
cli.set_read_timeout(std::chrono::seconds(30));
|
||||
|
||||
#ifdef YUZU_BUNDLED_OPENSSL
|
||||
cli.load_ca_cert_store(kCert, sizeof(kCert));
|
||||
#endif
|
||||
|
||||
if (auto res = cli.Get(url_path); res && res->status == 200 && !res->body.empty()) {
|
||||
std::vector<u8> data(res->body.begin(), res->body.end());
|
||||
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(cache_path.parent_path(), ec);
|
||||
if (std::ofstream out(cache_path, std::ios::binary); out) {
|
||||
out.write(res->body.data(), static_cast<std::streamsize>(res->body.size()));
|
||||
}
|
||||
return data;
|
||||
}
|
||||
} catch (...) {
|
||||
LOG_WARNING(Service_BCAT, "Failed to download: {}", url_path);
|
||||
}
|
||||
#endif
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<u8> LoadDefaultLogo(bool large) {
|
||||
const auto path = GetDefaultLogoPath(large);
|
||||
const std::string url = large ? "/news/eden_logo_large.jpg" : "/news/eden_logo_small.jpg";
|
||||
|
||||
auto data = TryLoadFromDisk(path);
|
||||
if (!data.empty()) return data;
|
||||
|
||||
return DownloadImage(url, path);
|
||||
}
|
||||
|
||||
void LoadDefaultLogos() {
|
||||
if (default_logos_loaded) return;
|
||||
default_logos_loaded = true;
|
||||
|
||||
default_logo_small = LoadDefaultLogo(false);
|
||||
default_logo_large = LoadDefaultLogo(true);
|
||||
}
|
||||
|
||||
std::vector<u8> GetNewsImage(std::string_view news_id, bool large) {
|
||||
const std::string id_str{news_id};
|
||||
|
||||
{
|
||||
std::lock_guard lock{images_mutex};
|
||||
auto& cache = large ? news_images_large : news_images_small;
|
||||
if (auto it = cache.find(id_str); it != cache.end()) {
|
||||
return it->second;
|
||||
}
|
||||
}
|
||||
|
||||
const auto cache_path = GetNewsImagePath(news_id, large);
|
||||
auto data = TryLoadFromDisk(cache_path);
|
||||
|
||||
if (data.empty()) {
|
||||
const std::string url = fmt::format("/news/{}_{}.jpg", id_str, large ? "large" : "small");
|
||||
data = DownloadImage(url, cache_path);
|
||||
}
|
||||
|
||||
if (data.empty()) {
|
||||
data = large ? default_logo_large : default_logo_small;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard lock{images_mutex};
|
||||
auto& cache = large ? news_images_large : news_images_small;
|
||||
cache[id_str] = data;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
void PreloadNewsImages(const std::vector<u32>& news_ids) {
|
||||
std::vector<std::future<void>> futures;
|
||||
futures.reserve(news_ids.size() * 2);
|
||||
|
||||
for (const u32 id : news_ids) {
|
||||
const std::string id_str = fmt::format("{}", id);
|
||||
|
||||
{
|
||||
std::lock_guard lock{images_mutex};
|
||||
if (news_images_small.contains(id_str) && news_images_large.contains(id_str)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const auto path_small = GetNewsImagePath(id_str, false);
|
||||
const auto path_large = GetNewsImagePath(id_str, true);
|
||||
if (std::filesystem::exists(path_small) && std::filesystem::exists(path_large)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
futures.push_back(std::async(std::launch::async, [id_str]() {
|
||||
GetNewsImage(id_str, false);
|
||||
}));
|
||||
futures.push_back(std::async(std::launch::async, [id_str]() {
|
||||
GetNewsImage(id_str, true);
|
||||
}));
|
||||
}
|
||||
|
||||
for (auto& f : futures) {
|
||||
f.wait();
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
|
||||
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
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"},
|
||||
};
|
||||
|
||||
// TODO(crueter): automate this in some way...
|
||||
#ifdef YUZU_BUNDLED_OPENSSL
|
||||
cli.load_ca_cert_store(kCert, sizeof(kCert));
|
||||
#endif
|
||||
|
||||
if (auto res = cli.Get(GitHubAPI_EdenReleases, headers); res && res->status < 400) {
|
||||
return res->body;
|
||||
}
|
||||
} catch (...) {
|
||||
LOG_WARNING(Service_BCAT, " failed to download releases");
|
||||
}
|
||||
#endif
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Fix line endings
|
||||
boost::replace_all(text, "\r", "");
|
||||
|
||||
// Remove backticks
|
||||
boost::replace_all(text, "`", "");
|
||||
|
||||
// Remove excessive newlines
|
||||
static const boost::regex newlines(R"(\n\n\n+)");
|
||||
text = boost::regex_replace(text, newlines, "\n\n");
|
||||
|
||||
// Remove markdown headers
|
||||
static const boost::regex headers(R"(^#+ )");
|
||||
text = boost::regex_replace(text, headers, "");
|
||||
|
||||
// Convert bullet points to something nicer
|
||||
static const boost::regex list1(R"(^- )");
|
||||
text = boost::regex_replace(text, list1, "• ");
|
||||
|
||||
static const boost::regex list2(R"(^ \* )");
|
||||
text = boost::regex_replace(text, list2, " • ");
|
||||
|
||||
// what
|
||||
static const boost::regex list2_dash(R"(^ - )");
|
||||
text = boost::regex_replace(text, list2_dash, " • ");
|
||||
|
||||
// Convert bold/italic text into normal text
|
||||
static const boost::regex bold(R"(\*\*(.*?)\*\*)");
|
||||
text = boost::regex_replace(text, bold, "$1");
|
||||
|
||||
static const boost::regex italic(R"(\*(.*?)\*)");
|
||||
text = boost::regex_replace(text, italic, "$1");
|
||||
|
||||
// Remove links and convert to normal text
|
||||
static const boost::regex link(R"(\[([^\]]+)\]\([^)]*\))");
|
||||
text = boost::regex_replace(text, link, "$1");
|
||||
|
||||
// Trim trailing whitespace/newlines
|
||||
while (!text.empty() && (text.back() == '\n' || text.back() == ' ')) {
|
||||
text.pop_back();
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
std::vector<u32> news_ids;
|
||||
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);
|
||||
news_ids.push_back(news_id);
|
||||
}
|
||||
|
||||
PreloadNewsImages(news_ids);
|
||||
|
||||
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 std::string news_id_str = fmt::format("{}", news_id);
|
||||
|
||||
const auto img_small = GetNewsImage(news_id_str, false);
|
||||
const auto img_large = GetNewsImage(news_id_str, true);
|
||||
|
||||
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(img_small);
|
||||
|
||||
// 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(img_large);
|
||||
|
||||
// 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, [] {
|
||||
LoadDefaultLogos();
|
||||
|
||||
if (const auto cached = ReadCachedJson()) {
|
||||
ImportReleases(*cached);
|
||||
LOG_DEBUG(Service_BCAT, "news: {} entries loaded from cache", NewsStorage::Instance().ListAll().size());
|
||||
}
|
||||
|
||||
std::thread([] {
|
||||
if (const auto fresh = DownloadReleasesJson()) {
|
||||
WriteCachedJson(*fresh);
|
||||
ImportReleases(*fresh);
|
||||
LOG_DEBUG(Service_BCAT, "news: {} entries updated from GitHub", NewsStorage::Instance().ListAll().size());
|
||||
}
|
||||
}).detach();
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace Service::News
|
||||
27
src/core/hle/service/bcat/news/builtin_news.h
Normal file
27
src/core/hle/service/bcat/news/builtin_news.h
Normal file
|
|
@ -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
|
||||
886
src/core/hle/service/bcat/news/msgpack.cpp
Normal file
886
src/core/hle/service/bcat/news/msgpack.cpp
Normal file
|
|
@ -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
|
||||
149
src/core/hle/service/bcat/news/msgpack.h
Normal file
149
src/core/hle/service/bcat/news/msgpack.h
Normal file
|
|
@ -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
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue