Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
282c6e5493 | ||
|
|
2c2f602d14 | ||
|
|
f7f1254699 | ||
|
|
90f8a62823 | ||
|
|
e2a1c8b5e3 | ||
|
|
21f6f4b74d | ||
|
|
75d3b3ee0d | ||
|
|
0dde379932 | ||
|
|
9800bbecdf | ||
|
|
60e915c255 | ||
|
|
786f8a42fa | ||
|
|
5a4a0f75f2 | ||
|
|
5aca92a2cc | ||
|
|
7471885119 | ||
|
|
5038fb0c28 | ||
|
|
ff9f493460 | ||
|
|
89e82927ee | ||
|
|
651d9fa495 | ||
|
|
3141100457 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -22,3 +22,5 @@ libs/tweeny
|
||||
|
||||
compile_commands.json
|
||||
out
|
||||
|
||||
usb_test/
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
path=/retroarch/cores/fbneo_libretro_libnx.nro
|
||||
supported_extensions=zip|7z|cue|ccd
|
||||
database=FBNeo - Arcade Games
|
||||
use_base_name=true
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
path=/retroarch/cores/mame2000_libretro_libnx.nro
|
||||
supported_extensions=zip|7z
|
||||
database=MAME 2000
|
||||
use_base_name=true
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
path=/retroarch/cores/mame2003_libretro_libnx.nro
|
||||
supported_extensions=zip
|
||||
database=MAME 2003
|
||||
use_base_name=true
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
path=/retroarch/cores/mame2003_plus_libretro_libnx.nro
|
||||
supported_extensions=zip
|
||||
database=MAME 2003-Plus
|
||||
use_base_name=true
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
path=/retroarch/cores/xrick_libretro_libnx.nro
|
||||
supported_extensions=zip
|
||||
database=Rick Dangerous
|
||||
use_base_name=true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
|
||||
set(sphaira_VERSION 0.6.3)
|
||||
set(sphaira_VERSION 0.8.0)
|
||||
|
||||
project(sphaira
|
||||
VERSION ${sphaira_VERSION}
|
||||
@@ -46,6 +46,9 @@ add_executable(sphaira
|
||||
source/ui/menus/menu_base.cpp
|
||||
source/ui/menus/themezer.cpp
|
||||
source/ui/menus/ghdl.cpp
|
||||
source/ui/menus/usb_menu.cpp
|
||||
source/ui/menus/ftp_menu.cpp
|
||||
source/ui/menus/gc_menu.cpp
|
||||
|
||||
source/ui/error_box.cpp
|
||||
source/ui/notification.cpp
|
||||
@@ -74,6 +77,22 @@ add_executable(sphaira
|
||||
source/web.cpp
|
||||
source/i18n.cpp
|
||||
source/ftpsrv_helper.cpp
|
||||
|
||||
source/yati/yati.cpp
|
||||
source/yati/container/nsp.cpp
|
||||
source/yati/container/xci.cpp
|
||||
source/yati/source/file.cpp
|
||||
source/yati/source/stdio.cpp
|
||||
source/yati/source/usb.cpp
|
||||
source/yati/source/stream.cpp
|
||||
source/yati/source/stream_file.cpp
|
||||
|
||||
source/yati/nx/es.cpp
|
||||
source/yati/nx/keys.cpp
|
||||
source/yati/nx/nca.cpp
|
||||
source/yati/nx/ncm.cpp
|
||||
source/yati/nx/ns.cpp
|
||||
source/yati/nx/nxdumptool_rsa.c
|
||||
)
|
||||
|
||||
target_compile_definitions(sphaira PRIVATE
|
||||
@@ -127,7 +146,8 @@ set(FETCHCONTENT_QUIET FALSE)
|
||||
|
||||
FetchContent_Declare(ftpsrv
|
||||
GIT_REPOSITORY https://github.com/ITotalJustice/ftpsrv.git
|
||||
GIT_TAG 1.2.2
|
||||
# GIT_TAG 1.2.2
|
||||
GIT_TAG f8a30fd
|
||||
SOURCE_SUBDIR NONE
|
||||
)
|
||||
|
||||
@@ -161,6 +181,24 @@ FetchContent_Declare(minIni
|
||||
GIT_TAG 11cac8b
|
||||
)
|
||||
|
||||
FetchContent_Declare(zstd
|
||||
GIT_REPOSITORY https://github.com/facebook/zstd.git
|
||||
GIT_TAG v1.5.7
|
||||
SOURCE_SUBDIR build/cmake
|
||||
)
|
||||
|
||||
set(USE_NEW_ZSTD ON)
|
||||
|
||||
set(ZSTD_BUILD_STATIC ON)
|
||||
set(ZSTD_BUILD_SHARED OFF)
|
||||
set(ZSTD_BUILD_COMPRESSION OFF)
|
||||
set(ZSTD_BUILD_DECOMPRESSION ON)
|
||||
set(ZSTD_BUILD_DICTBUILDER OFF)
|
||||
set(ZSTD_LEGACY_SUPPORT OFF)
|
||||
set(ZSTD_MULTITHREAD_SUPPORT OFF)
|
||||
set(ZSTD_BUILD_PROGRAMS OFF)
|
||||
set(ZSTD_BUILD_TESTS OFF)
|
||||
|
||||
set(MININI_LIB_NAME minIni)
|
||||
set(MININI_USE_STDIO ON)
|
||||
set(MININI_USE_NX ON)
|
||||
@@ -195,6 +233,7 @@ FetchContent_MakeAvailable(
|
||||
stb
|
||||
minIni
|
||||
yyjson
|
||||
zstd
|
||||
)
|
||||
|
||||
set(FTPSRV_LIB_BUILD TRUE)
|
||||
@@ -274,6 +313,11 @@ find_package(CURL REQUIRED)
|
||||
find_path(mbedtls_inc mbedtls REQUIRED)
|
||||
find_library(mbedcrypto_lib mbedcrypto REQUIRED)
|
||||
|
||||
if (NOT USE_NEW_ZSTD)
|
||||
find_path(zstd_inc zstd.h REQUIRED)
|
||||
find_library(zstd_lib zstd REQUIRED)
|
||||
endif()
|
||||
|
||||
set_target_properties(sphaira PROPERTIES
|
||||
C_STANDARD 11
|
||||
C_EXTENSIONS ON
|
||||
@@ -296,6 +340,15 @@ target_link_libraries(sphaira PRIVATE
|
||||
${mbedcrypto_lib}
|
||||
)
|
||||
|
||||
if (USE_NEW_ZSTD)
|
||||
message(STATUS "USING UPSTREAM ZSTD")
|
||||
target_link_libraries(sphaira PRIVATE libzstd_static)
|
||||
else()
|
||||
message(STATUS "USING LOCAL ZSTD")
|
||||
target_link_libraries(sphaira PRIVATE ${zstd_lib})
|
||||
target_include_directories(sphaira PRIVATE ${zstd_inc})
|
||||
endif()
|
||||
|
||||
target_include_directories(sphaira PRIVATE
|
||||
include
|
||||
${minizip_inc}
|
||||
|
||||
@@ -44,6 +44,8 @@ public:
|
||||
~App();
|
||||
void Loop();
|
||||
|
||||
static App* GetApp();
|
||||
|
||||
static void Exit();
|
||||
static void ExitRestart();
|
||||
static auto GetVg() -> NVGcontext*;
|
||||
@@ -62,7 +64,7 @@ public:
|
||||
static void SetTheme(s64 theme_index);
|
||||
static auto GetThemeIndex() -> s64;
|
||||
|
||||
static auto GetDefaultImage(int* w = nullptr, int* h = nullptr) -> int;
|
||||
static auto GetDefaultImage() -> int;
|
||||
|
||||
// returns argv[0]
|
||||
static auto GetExePath() -> fs::FsPath;
|
||||
@@ -77,7 +79,6 @@ public:
|
||||
static auto GetInstallEnable() -> bool;
|
||||
static auto GetInstallSdEnable() -> bool;
|
||||
static auto GetInstallPrompt() -> bool;
|
||||
static auto GetThemeShuffleEnable() -> bool;
|
||||
static auto GetThemeMusicEnable() -> bool;
|
||||
static auto Get12HourTimeEnable() -> bool;
|
||||
static auto GetLanguage() -> long;
|
||||
@@ -91,7 +92,6 @@ public:
|
||||
static void SetInstallEnable(bool enable);
|
||||
static void SetInstallSdEnable(bool enable);
|
||||
static void SetInstallPrompt(bool enable);
|
||||
static void SetThemeShuffleEnable(bool enable);
|
||||
static void SetThemeMusicEnable(bool enable);
|
||||
static void Set12HourTimeEnable(bool enable);
|
||||
static void SetLanguage(long index);
|
||||
@@ -102,6 +102,13 @@ public:
|
||||
|
||||
static void PlaySoundEffect(SoundEffect effect);
|
||||
|
||||
static void DisplayThemeOptions(bool left_side = true);
|
||||
// todo:
|
||||
static void DisplayNetworkOptions(bool left_side = true);
|
||||
static void DisplayMiscOptions(bool left_side = true);
|
||||
static void DisplayAdvancedOptions(bool left_side = true);
|
||||
static void DisplayInstallOptions(bool left_side = true);
|
||||
|
||||
void Draw();
|
||||
void Update();
|
||||
void Poll();
|
||||
@@ -174,13 +181,30 @@ public:
|
||||
option::OptionBool m_ftp_enabled{INI_SECTION, "ftp_enabled", false};
|
||||
option::OptionBool m_log_enabled{INI_SECTION, "log_enabled", false};
|
||||
option::OptionBool m_replace_hbmenu{INI_SECTION, "replace_hbmenu", false};
|
||||
option::OptionBool m_install{INI_SECTION, "install", false};
|
||||
option::OptionBool m_install_sd{INI_SECTION, "install_sd", true};
|
||||
option::OptionLong m_install_prompt{INI_SECTION, "install_prompt", true};
|
||||
option::OptionBool m_theme_shuffle{INI_SECTION, "theme_shuffle", false};
|
||||
option::OptionBool m_theme_music{INI_SECTION, "theme_music", true};
|
||||
option::OptionBool m_12hour_time{INI_SECTION, "12hour_time", false};
|
||||
option::OptionLong m_language{INI_SECTION, "language", 0}; // auto
|
||||
|
||||
// install options
|
||||
option::OptionBool m_install{INI_SECTION, "install", false};
|
||||
option::OptionBool m_install_sd{INI_SECTION, "install_sd", true};
|
||||
option::OptionLong m_install_prompt{INI_SECTION, "install_prompt", true};
|
||||
option::OptionBool m_allow_downgrade{INI_SECTION, "allow_downgrade", false};
|
||||
option::OptionBool m_skip_if_already_installed{INI_SECTION, "skip_if_already_installed", true};
|
||||
option::OptionBool m_ticket_only{INI_SECTION, "ticket_only", false};
|
||||
option::OptionBool m_skip_base{INI_SECTION, "skip_base", false};
|
||||
option::OptionBool m_skip_patch{INI_SECTION, "skip_patch", false};
|
||||
option::OptionBool m_skip_addon{INI_SECTION, "skip_addon", false};
|
||||
option::OptionBool m_skip_data_patch{INI_SECTION, "skip_data_patch", false};
|
||||
option::OptionBool m_skip_ticket{INI_SECTION, "skip_ticket", false};
|
||||
option::OptionBool m_skip_nca_hash_verify{INI_SECTION, "skip_nca_hash_verify", false};
|
||||
option::OptionBool m_skip_rsa_header_fixed_key_verify{INI_SECTION, "skip_rsa_header_fixed_key_verify", false};
|
||||
option::OptionBool m_skip_rsa_npdm_fixed_key_verify{INI_SECTION, "skip_rsa_npdm_fixed_key_verify", false};
|
||||
option::OptionBool m_ignore_distribution_bit{INI_SECTION, "ignore_distribution_bit", false};
|
||||
option::OptionBool m_convert_to_standard_crypto{INI_SECTION, "convert_to_standard_crypto", false};
|
||||
option::OptionBool m_lower_master_key{INI_SECTION, "lower_master_key", false};
|
||||
option::OptionBool m_lower_system_version{INI_SECTION, "lower_system_version", false};
|
||||
|
||||
// todo: move this into it's own menu
|
||||
option::OptionLong m_text_scroll_speed{"accessibility", "text_scroll_speed", 1}; // normal
|
||||
|
||||
|
||||
@@ -454,4 +454,10 @@ struct FsNativeContentStorage final : FsNative {
|
||||
}
|
||||
};
|
||||
|
||||
struct FsNativeGameCard final : FsNative {
|
||||
FsNativeGameCard(const FsGameCardHandle* handle, FsGameCardPartition partition, bool ignore_read_only = true) : FsNative{ignore_read_only} {
|
||||
m_open_result = fsOpenGameCardFileSystem(&m_fs, handle, partition);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace fs
|
||||
|
||||
@@ -1,8 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
|
||||
namespace sphaira::ftpsrv {
|
||||
|
||||
bool Init();
|
||||
void Exit();
|
||||
|
||||
using OnInstallStart = std::function<bool(void* user, const char* path)>;
|
||||
using OnInstallWrite = std::function<bool(void* user, const void* buf, size_t size)>;
|
||||
using OnInstallClose = std::function<void(void* user)>;
|
||||
|
||||
void InitInstallMode(void* user, OnInstallStart on_start, OnInstallWrite on_write, OnInstallClose on_close);
|
||||
void DisableInstallMode();
|
||||
|
||||
unsigned GetPort();
|
||||
bool IsAnon();
|
||||
const char* GetUser();
|
||||
const char* GetPass();
|
||||
|
||||
} // namespace sphaira::ftpsrv
|
||||
|
||||
@@ -1,24 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define sphaira_USE_LOG 1
|
||||
|
||||
#include <cstdarg>
|
||||
#include <stdarg.h>
|
||||
|
||||
#if sphaira_USE_LOG
|
||||
auto log_file_init() -> bool;
|
||||
auto log_nxlink_init() -> bool;
|
||||
bool log_file_init();
|
||||
bool log_nxlink_init();
|
||||
void log_file_exit();
|
||||
void log_nxlink_exit();
|
||||
void log_write(const char* s, ...) __attribute__ ((format (printf, 1, 2)));
|
||||
void log_write_arg(const char* s, std::va_list& v);
|
||||
void log_write_arg(const char* s, va_list* v);
|
||||
#else
|
||||
inline auto log_file_init() -> bool {
|
||||
inline bool log_file_init() {
|
||||
return true;
|
||||
}
|
||||
inline auto log_nxlink_init() -> bool {
|
||||
inline bool log_nxlink_init() {
|
||||
return true;
|
||||
}
|
||||
#define log_file_exit()
|
||||
#define log_nxlink_exit()
|
||||
#define log_write(...)
|
||||
#define log_write_arg(...)
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -6,11 +6,11 @@ namespace sphaira::ui {
|
||||
|
||||
struct List final : Object {
|
||||
using Callback = std::function<void(NVGcontext* vg, Theme* theme, Vec4 v, s64 index)>;
|
||||
using TouchCallback = std::function<void(s64 index)>;
|
||||
using TouchCallback = std::function<void(bool touch, s64 index)>;
|
||||
|
||||
List(s64 row, s64 page, const Vec4& pos, const Vec4& v, const Vec2& pad = {});
|
||||
|
||||
void OnUpdate(Controller* controller, TouchInfo* touch, s64 count, TouchCallback callback);
|
||||
void OnUpdate(Controller* controller, TouchInfo* touch, s64 index, s64 count, TouchCallback callback);
|
||||
|
||||
void Draw(NVGcontext* vg, Theme* theme, s64 count, Callback callback) const;
|
||||
|
||||
|
||||
@@ -89,6 +89,19 @@ struct FileAssocEntry {
|
||||
std::string name{}; // ini name
|
||||
std::vector<std::string> ext{}; // list of ext
|
||||
std::vector<std::string> database{}; // list of systems
|
||||
bool use_base_name{}; // if set, uses base name (rom.zip) otherwise uses internal name (rom.gba)
|
||||
|
||||
auto IsExtension(std::string_view extension, std::string_view internal_extension) const -> bool {
|
||||
for (const auto& assoc_ext : ext) {
|
||||
if (extension.length() == assoc_ext.length() && !strncasecmp(assoc_ext.data(), extension.data(), assoc_ext.length())) {
|
||||
return true;
|
||||
}
|
||||
if (internal_extension.length() == assoc_ext.length() && !strncasecmp(assoc_ext.data(), internal_extension.data(), assoc_ext.length())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
struct LastFile {
|
||||
@@ -122,6 +135,8 @@ struct Menu final : MenuBase {
|
||||
private:
|
||||
void SetIndex(s64 index);
|
||||
void InstallForwarder();
|
||||
void InstallFile(const FileEntry& target);
|
||||
void InstallFiles(const std::vector<FileEntry>& targets);
|
||||
auto Scan(const fs::FsPath& new_path, bool is_walk_up = false) -> Result;
|
||||
|
||||
void LoadAssocEntriesPath(const fs::FsPath& path);
|
||||
|
||||
59
sphaira/include/ui/menus/ftp_menu.hpp
Normal file
59
sphaira/include/ui/menus/ftp_menu.hpp
Normal file
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
#include "ui/menus/menu_base.hpp"
|
||||
#include "yati/source/stream.hpp"
|
||||
|
||||
namespace sphaira::ui::menu::ftp {
|
||||
|
||||
enum class State {
|
||||
// not connected.
|
||||
None,
|
||||
// just connected, starts the transfer.
|
||||
Connected,
|
||||
// set whilst transfer is in progress.
|
||||
Progress,
|
||||
// set when the transfer is finished.
|
||||
Done,
|
||||
// failed to connect.
|
||||
Failed,
|
||||
};
|
||||
|
||||
struct StreamFtp final : yati::source::Stream {
|
||||
StreamFtp(const fs::FsPath& path, std::stop_token token);
|
||||
|
||||
Result ReadChunk(void* buf, s64 size, u64* bytes_read) override;
|
||||
bool Push(const void* buf, s64 size);
|
||||
void Disable();
|
||||
|
||||
// private:
|
||||
fs::FsPath m_path{};
|
||||
std::stop_token m_token{};
|
||||
std::vector<u8> m_buffer{};
|
||||
Mutex m_mutex{};
|
||||
bool m_active{};
|
||||
// bool m_push_exit{};
|
||||
};
|
||||
|
||||
struct Menu final : MenuBase {
|
||||
Menu();
|
||||
~Menu();
|
||||
|
||||
void Update(Controller* controller, TouchInfo* touch) override;
|
||||
void Draw(NVGcontext* vg, Theme* theme) override;
|
||||
void OnFocusGained() override;
|
||||
|
||||
// this should be private
|
||||
// private:
|
||||
std::shared_ptr<StreamFtp> m_source;
|
||||
Thread m_thread{};
|
||||
Mutex m_mutex{};
|
||||
// the below are shared across threads, lock with the above mutex!
|
||||
State m_state{State::None};
|
||||
|
||||
const char* m_user{};
|
||||
const char* m_pass{};
|
||||
unsigned m_port{};
|
||||
bool m_anon{};
|
||||
};
|
||||
|
||||
} // namespace sphaira::ui::menu::ftp
|
||||
75
sphaira/include/ui/menus/gc_menu.hpp
Normal file
75
sphaira/include/ui/menus/gc_menu.hpp
Normal file
@@ -0,0 +1,75 @@
|
||||
#pragma once
|
||||
|
||||
#include "ui/menus/menu_base.hpp"
|
||||
#include "yati/container/base.hpp"
|
||||
#include "yati/source/base.hpp"
|
||||
#include "ui/list.hpp"
|
||||
#include <span>
|
||||
#include <memory>
|
||||
|
||||
namespace sphaira::ui::menu::gc {
|
||||
|
||||
struct GcCollection : yati::container::CollectionEntry {
|
||||
GcCollection(const char* _name, s64 _size, u8 _type) {
|
||||
name = _name;
|
||||
size = _size;
|
||||
type = _type;
|
||||
}
|
||||
|
||||
// NcmContentType
|
||||
u8 type{};
|
||||
};
|
||||
|
||||
using GcCollections = std::vector<GcCollection>;
|
||||
|
||||
struct ApplicationEntry {
|
||||
u64 app_id{};
|
||||
u32 version{};
|
||||
u8 key_gen{};
|
||||
|
||||
std::vector<GcCollections> application{};
|
||||
std::vector<GcCollections> patch{};
|
||||
std::vector<GcCollections> add_on{};
|
||||
std::vector<GcCollections> data_patch{};
|
||||
yati::container::Collections tickets{};
|
||||
|
||||
auto GetSize() const -> s64;
|
||||
auto GetSize(const std::vector<GcCollections>& entries) const -> s64;
|
||||
};
|
||||
|
||||
struct Menu final : MenuBase {
|
||||
Menu();
|
||||
~Menu();
|
||||
|
||||
void Update(Controller* controller, TouchInfo* touch) override;
|
||||
void Draw(NVGcontext* vg, Theme* theme) override;
|
||||
|
||||
private:
|
||||
Result GcMount();
|
||||
void GcUnmount();
|
||||
Result GcPoll(bool* inserted);
|
||||
Result UpdateStorageSize();
|
||||
|
||||
void FreeImage();
|
||||
void OnChangeIndex(s64 new_index);
|
||||
|
||||
private:
|
||||
FsDeviceOperator m_dev_op{};
|
||||
FsGameCardHandle m_handle{};
|
||||
std::unique_ptr<fs::FsNativeGameCard> m_fs{};
|
||||
|
||||
std::vector<ApplicationEntry> m_entries{};
|
||||
std::unique_ptr<List> m_list{};
|
||||
s64 m_entry_index{};
|
||||
s64 m_option_index{};
|
||||
|
||||
s64 m_size_free_sd{};
|
||||
s64 m_size_total_sd{};
|
||||
s64 m_size_free_nand{};
|
||||
s64 m_size_total_nand{};
|
||||
NacpLanguageEntry m_lang_entry{};
|
||||
int m_icon{};
|
||||
bool m_mounted{};
|
||||
};
|
||||
|
||||
} // namespace sphaira::ui::menu::gc
|
||||
@@ -29,6 +29,7 @@ private:
|
||||
std::string m_title_sub_heading{};
|
||||
std::string m_sub_heading{};
|
||||
|
||||
protected:
|
||||
struct tm m_tm{};
|
||||
TimeStamp m_poll_timestamp{};
|
||||
u32 m_battery_percetange{};
|
||||
|
||||
43
sphaira/include/ui/menus/usb_menu.hpp
Normal file
43
sphaira/include/ui/menus/usb_menu.hpp
Normal file
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include "ui/menus/menu_base.hpp"
|
||||
#include "yati/source/usb.hpp"
|
||||
|
||||
namespace sphaira::ui::menu::usb {
|
||||
|
||||
enum class State {
|
||||
// not connected.
|
||||
None,
|
||||
// just connected, starts the transfer.
|
||||
Connected,
|
||||
// set whilst transfer is in progress.
|
||||
Progress,
|
||||
// set when the transfer is finished.
|
||||
Done,
|
||||
// failed to connect.
|
||||
Failed,
|
||||
};
|
||||
|
||||
struct Menu final : MenuBase {
|
||||
Menu();
|
||||
~Menu();
|
||||
|
||||
void Update(Controller* controller, TouchInfo* touch) override;
|
||||
void Draw(NVGcontext* vg, Theme* theme) override;
|
||||
void OnFocusGained() override;
|
||||
|
||||
// this should be private
|
||||
// private:
|
||||
std::shared_ptr<yati::source::Usb> m_usb_source{};
|
||||
bool m_was_mtp_enabled{};
|
||||
|
||||
Thread m_thread{};
|
||||
Mutex m_mutex{};
|
||||
// the below are shared across threads, lock with the above mutex!
|
||||
State m_state{State::None};
|
||||
bool m_usb_has_connection{};
|
||||
u32 m_usb_speed{};
|
||||
u32 m_usb_count{};
|
||||
};
|
||||
|
||||
} // namespace sphaira::ui::menu::usb
|
||||
@@ -5,17 +5,15 @@
|
||||
|
||||
namespace sphaira::ui::gfx {
|
||||
|
||||
void drawImage(NVGcontext*, float x, float y, float w, float h, int texture);
|
||||
void drawImage(NVGcontext*, const Vec4& v, int texture);
|
||||
void drawImageRounded(NVGcontext*, float x, float y, float w, float h, int texture);
|
||||
void drawImageRounded(NVGcontext*, const Vec4& v, int texture);
|
||||
void drawImage(NVGcontext*, float x, float y, float w, float h, int texture, float rounded = 0.F);
|
||||
void drawImage(NVGcontext*, const Vec4& v, int texture, float rounded = 0.F);
|
||||
|
||||
void dimBackground(NVGcontext*);
|
||||
|
||||
void drawRect(NVGcontext*, float x, float y, float w, float h, const NVGcolor& c, bool rounded = false);
|
||||
void drawRect(NVGcontext*, const Vec4& v, const NVGcolor& c, bool rounded = false);
|
||||
void drawRect(NVGcontext*, float x, float y, float w, float h, const NVGpaint& p, bool rounded = false);
|
||||
void drawRect(NVGcontext*, const Vec4& v, const NVGpaint& p, bool rounded = false);
|
||||
void drawRect(NVGcontext*, float x, float y, float w, float h, const NVGcolor& c, float rounding = 0.F);
|
||||
void drawRect(NVGcontext*, const Vec4& v, const NVGcolor& c, float rounding = 0.F);
|
||||
void drawRect(NVGcontext*, float x, float y, float w, float h, const NVGpaint& p, float rounding = 0.F);
|
||||
void drawRect(NVGcontext*, const Vec4& v, const NVGpaint& p, float rounding = 0.F);
|
||||
|
||||
void drawRectOutline(NVGcontext*, const Theme*, float size, float x, float y, float w, float h);
|
||||
void drawRectOutline(NVGcontext*, const Theme*, float size, const Vec4& v);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "widget.hpp"
|
||||
#include "fs.hpp"
|
||||
#include <functional>
|
||||
#include <span>
|
||||
|
||||
namespace sphaira::ui {
|
||||
|
||||
@@ -12,6 +13,8 @@ using ProgressBoxDoneCallback = std::function<void(bool success)>;
|
||||
|
||||
struct ProgressBox final : Widget {
|
||||
ProgressBox(
|
||||
int image,
|
||||
const std::string& action,
|
||||
const std::string& title,
|
||||
ProgressBoxCallback callback, ProgressBoxDoneCallback done = [](bool success){},
|
||||
int cpuid = 1, int prio = 0x2C, int stack_size = 1024*1024
|
||||
@@ -21,8 +24,12 @@ struct ProgressBox final : Widget {
|
||||
auto Update(Controller* controller, TouchInfo* touch) -> void override;
|
||||
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
||||
|
||||
auto SetTitle(const std::string& title) -> ProgressBox&;
|
||||
auto NewTransfer(const std::string& transfer) -> ProgressBox&;
|
||||
auto UpdateTransfer(s64 offset, s64 size) -> ProgressBox&;
|
||||
// not const in order to avoid copy by using std::swap
|
||||
auto SetImageData(std::vector<u8>& data) -> ProgressBox&;
|
||||
auto SetImageDataConst(std::span<const u8> data) -> ProgressBox&;
|
||||
void RequestExit();
|
||||
auto ShouldExit() -> bool;
|
||||
|
||||
@@ -40,6 +47,9 @@ struct ProgressBox final : Widget {
|
||||
};
|
||||
}
|
||||
|
||||
private:
|
||||
void FreeImage();
|
||||
|
||||
public:
|
||||
struct ThreadData {
|
||||
ProgressBox* pbox{};
|
||||
@@ -51,12 +61,22 @@ private:
|
||||
Mutex m_mutex{};
|
||||
Thread m_thread{};
|
||||
ThreadData m_thread_data{};
|
||||
|
||||
ProgressBoxDoneCallback m_done{};
|
||||
|
||||
// shared data start.
|
||||
std::string m_action{};
|
||||
std::string m_title{};
|
||||
std::string m_transfer{};
|
||||
s64 m_size{};
|
||||
s64 m_offset{};
|
||||
s64 m_last_offset{};
|
||||
s64 m_speed{};
|
||||
TimeStamp m_timestamp{};
|
||||
std::vector<u8> m_image_data{};
|
||||
// shared data end.
|
||||
|
||||
int m_image{};
|
||||
bool m_own_image{};
|
||||
};
|
||||
|
||||
// this is a helper function that does many things.
|
||||
|
||||
43
sphaira/include/yati/container/base.hpp
Normal file
43
sphaira/include/yati/container/base.hpp
Normal file
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include "yati/source/base.hpp"
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <switch.h>
|
||||
|
||||
namespace sphaira::yati::container {
|
||||
|
||||
enum class CollectionType {
|
||||
CollectionType_NCA,
|
||||
CollectionType_NCZ,
|
||||
CollectionType_TIK,
|
||||
CollectionType_CERT,
|
||||
};
|
||||
|
||||
struct CollectionEntry {
|
||||
// collection name within file.
|
||||
std::string name{};
|
||||
// collection offset within file.
|
||||
s64 offset{};
|
||||
// collection size within file, may be compressed size.
|
||||
s64 size{};
|
||||
};
|
||||
|
||||
using Collections = std::vector<CollectionEntry>;
|
||||
|
||||
struct Base {
|
||||
using Source = source::Base;
|
||||
|
||||
Base(std::shared_ptr<Source> source) : m_source{source} { }
|
||||
virtual ~Base() = default;
|
||||
virtual Result GetCollections(Collections& out) = 0;
|
||||
auto GetSource() const {
|
||||
return m_source;
|
||||
}
|
||||
|
||||
protected:
|
||||
std::shared_ptr<Source> m_source;
|
||||
};
|
||||
|
||||
} // namespace sphaira::yati::container
|
||||
13
sphaira/include/yati/container/nsp.hpp
Normal file
13
sphaira/include/yati/container/nsp.hpp
Normal file
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include "base.hpp"
|
||||
#include <switch.h>
|
||||
|
||||
namespace sphaira::yati::container {
|
||||
|
||||
struct Nsp final : Base {
|
||||
using Base::Base;
|
||||
Result GetCollections(Collections& out) override;
|
||||
};
|
||||
|
||||
} // namespace sphaira::yati::container
|
||||
15
sphaira/include/yati/container/xci.hpp
Normal file
15
sphaira/include/yati/container/xci.hpp
Normal file
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include "base.hpp"
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <switch.h>
|
||||
|
||||
namespace sphaira::yati::container {
|
||||
|
||||
struct Xci final : Base {
|
||||
using Base::Base;
|
||||
Result GetCollections(Collections& out) override;
|
||||
};
|
||||
|
||||
} // namespace sphaira::yati::container
|
||||
57
sphaira/include/yati/nx/crypto.hpp
Normal file
57
sphaira/include/yati/nx/crypto.hpp
Normal file
@@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
|
||||
#include <switch.h>
|
||||
|
||||
namespace sphaira::crypto {
|
||||
|
||||
struct Aes128 {
|
||||
Aes128(const void *key, bool is_encryptor) {
|
||||
m_is_encryptor = is_encryptor;
|
||||
aes128ContextCreate(&m_ctx, key, is_encryptor);
|
||||
}
|
||||
|
||||
void Run(void *dst, const void *src) {
|
||||
if (m_is_encryptor) {
|
||||
aes128EncryptBlock(&m_ctx, dst, src);
|
||||
} else {
|
||||
aes128DecryptBlock(&m_ctx, dst, src);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
Aes128Context m_ctx;
|
||||
bool m_is_encryptor;
|
||||
};
|
||||
|
||||
struct Aes128Xts {
|
||||
Aes128Xts(const u8 *key, bool is_encryptor) : Aes128Xts{key, key + 0x10, is_encryptor} { }
|
||||
Aes128Xts(const void *key0, const void *key1, bool is_encryptor) {
|
||||
m_is_encryptor = is_encryptor;
|
||||
aes128XtsContextCreate(&m_ctx, key0, key1, is_encryptor);
|
||||
}
|
||||
|
||||
void Run(void *dst, const void *src, u64 sector, u64 sector_size, u64 data_size) {
|
||||
for (u64 pos = 0; pos < data_size; pos += sector_size) {
|
||||
aes128XtsContextResetSector(&m_ctx, sector++, true);
|
||||
if (m_is_encryptor) {
|
||||
aes128XtsEncrypt(&m_ctx, static_cast<u8*>(dst) + pos, static_cast<const u8*>(src) + pos, sector_size);
|
||||
} else {
|
||||
aes128XtsDecrypt(&m_ctx, static_cast<u8*>(dst) + pos, static_cast<const u8*>(src) + pos, sector_size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
Aes128XtsContext m_ctx;
|
||||
bool m_is_encryptor;
|
||||
};
|
||||
|
||||
static inline void cryptoAes128(const void *in, void *out, const void* key, bool is_encryptor) {
|
||||
Aes128(key, is_encryptor).Run(out, in);
|
||||
}
|
||||
|
||||
static inline void cryptoAes128Xts(const void* in, void* out, const u8* key, u64 sector, u64 sector_size, u64 data_size, bool is_encryptor) {
|
||||
Aes128Xts(key, is_encryptor).Run(out, in, sector, sector_size, data_size);
|
||||
}
|
||||
|
||||
} // namespace sphaira::crypto
|
||||
83
sphaira/include/yati/nx/es.hpp
Normal file
83
sphaira/include/yati/nx/es.hpp
Normal file
@@ -0,0 +1,83 @@
|
||||
#pragma once
|
||||
|
||||
#include <switch.h>
|
||||
#include <span>
|
||||
#include "ncm.hpp"
|
||||
#include "keys.hpp"
|
||||
|
||||
namespace sphaira::es {
|
||||
|
||||
enum { TicketModule = 522 };
|
||||
|
||||
enum : Result {
|
||||
// found ticket has missmatching rights_id from it's name.
|
||||
Result_InvalidTicketBadRightsId = MAKERESULT(TicketModule, 71),
|
||||
Result_InvalidTicketVersion = MAKERESULT(TicketModule, 72),
|
||||
Result_InvalidTicketKeyType = MAKERESULT(TicketModule, 73),
|
||||
Result_InvalidTicketKeyRevision = MAKERESULT(TicketModule, 74),
|
||||
};
|
||||
|
||||
enum TicketSigantureType {
|
||||
TicketSigantureType_RSA_4096_SHA1 = 0x010000,
|
||||
TicketSigantureType_RSA_2048_SHA1 = 0x010001,
|
||||
TicketSigantureType_ECDSA_SHA1 = 0x010002,
|
||||
TicketSigantureType_RSA_4096_SHA256 = 0x010003,
|
||||
TicketSigantureType_RSA_2048_SHA256 = 0x010004,
|
||||
TicketSigantureType_ECDSA_SHA256 = 0x010005,
|
||||
TicketSigantureType_HMAC_SHA1_160 = 0x010006,
|
||||
};
|
||||
|
||||
enum TicketTitleKeyType {
|
||||
TicketTitleKeyType_Common = 0,
|
||||
TicketTitleKeyType_Personalized = 1,
|
||||
};
|
||||
|
||||
enum TicketPropertiesBitfield {
|
||||
TicketPropertiesBitfield_None = 0,
|
||||
// temporary ticket, removed on restart
|
||||
TicketPropertiesBitfield_Temporary = 1 << 4,
|
||||
};
|
||||
|
||||
struct TicketData {
|
||||
u8 issuer[0x40];
|
||||
u8 title_key_block[0x100];
|
||||
u8 ticket_version1;
|
||||
u8 title_key_type;
|
||||
u16 ticket_version2;
|
||||
u8 license_type;
|
||||
u8 master_key_revision;
|
||||
u16 properties_bitfield;
|
||||
u8 _0x148[0x8];
|
||||
u64 ticket_id;
|
||||
u64 device_id;
|
||||
FsRightsId rights_id;
|
||||
u32 account_id;
|
||||
u8 _0x174[0xC];
|
||||
u8 _0x180[0x140];
|
||||
};
|
||||
static_assert(sizeof(TicketData) == 0x2C0);
|
||||
|
||||
struct EticketRsaDeviceKey {
|
||||
u8 ctr[AES_128_KEY_SIZE];
|
||||
u8 private_exponent[0x100];
|
||||
u8 modulus[0x100];
|
||||
u32 public_exponent; ///< Stored using big endian byte order. Must match ETICKET_RSA_DEVICE_KEY_PUBLIC_EXPONENT.
|
||||
u8 padding[0x14];
|
||||
u64 device_id;
|
||||
u8 ghash[0x10];
|
||||
};
|
||||
static_assert(sizeof(EticketRsaDeviceKey) == 0x240);
|
||||
|
||||
// es functions.
|
||||
Result ImportTicket(Service* srv, const void* tik_buf, u64 tik_size, const void* cert_buf, u64 cert_size);
|
||||
|
||||
// ticket functions.
|
||||
Result GetTicketDataOffset(std::span<const u8> ticket, u64& out);
|
||||
Result GetTicketData(std::span<const u8> ticket, es::TicketData* out);
|
||||
Result SetTicketData(std::span<u8> ticket, const es::TicketData* in);
|
||||
|
||||
Result GetTitleKey(keys::KeyEntry& out, const TicketData& data, const keys::Keys& keys);
|
||||
Result DecryptTitleKey(keys::KeyEntry& out, u8 key_gen, const keys::Keys& keys);
|
||||
Result PatchTicket(std::span<u8> ticket, const keys::Keys& keys);
|
||||
|
||||
} // namespace sphaira::es
|
||||
70
sphaira/include/yati/nx/keys.hpp
Normal file
70
sphaira/include/yati/nx/keys.hpp
Normal file
@@ -0,0 +1,70 @@
|
||||
#pragma once
|
||||
|
||||
#include <switch.h>
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include "defines.hpp"
|
||||
|
||||
namespace sphaira::keys {
|
||||
|
||||
struct KeyEntry {
|
||||
u8 key[AES_128_KEY_SIZE]{};
|
||||
|
||||
auto IsValid() const -> bool {
|
||||
const KeyEntry empty{};
|
||||
return std::memcmp(key, &empty, sizeof(key));
|
||||
}
|
||||
};
|
||||
|
||||
using KeySection = std::array<KeyEntry, 0x20>;
|
||||
struct Keys {
|
||||
u8 header_key[0x20]{};
|
||||
// the below are only found if read_from_file=true
|
||||
KeySection key_area_key[0x3]{}; // index
|
||||
KeySection titlekek{};
|
||||
KeySection master_key{};
|
||||
KeyEntry eticket_rsa_kek{};
|
||||
SetCalRsa2048DeviceKey eticket_device_key{};
|
||||
|
||||
static auto FixKey(u8 key) -> u8 {
|
||||
if (key) {
|
||||
return key - 1;
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
auto HasNcaKeyArea(u8 key, u8 index) const -> bool {
|
||||
return key_area_key[index][FixKey(key)].IsValid();
|
||||
}
|
||||
|
||||
auto HasTitleKek(u8 key) const -> bool {
|
||||
return titlekek[FixKey(key)].IsValid();
|
||||
}
|
||||
|
||||
auto HasMasterKey(u8 key) const -> bool {
|
||||
return master_key[FixKey(key)].IsValid();
|
||||
}
|
||||
|
||||
auto GetNcaKeyArea(KeyEntry* out, u8 key, u8 index) const -> Result {
|
||||
R_UNLESS(HasNcaKeyArea(key, index), 0x1);
|
||||
*out = key_area_key[index][FixKey(key)];
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
auto GetTitleKek(KeyEntry* out, u8 key) const -> Result {
|
||||
R_UNLESS(HasTitleKek(key), 0x1);
|
||||
*out = titlekek[FixKey(key)];
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
auto GetMasterKey(KeyEntry* out, u8 key) const -> Result {
|
||||
R_UNLESS(HasMasterKey(key), 0x1);
|
||||
*out = master_key[FixKey(key)];
|
||||
R_SUCCEED();
|
||||
}
|
||||
};
|
||||
|
||||
void parse_hex_key(void* key, const char* hex);
|
||||
Result parse_keys(Keys& out, bool read_from_file);
|
||||
|
||||
} // namespace sphaira::keys
|
||||
220
sphaira/include/yati/nx/nca.hpp
Normal file
220
sphaira/include/yati/nx/nca.hpp
Normal file
@@ -0,0 +1,220 @@
|
||||
#pragma once
|
||||
|
||||
#include <switch.h>
|
||||
#include "keys.hpp"
|
||||
|
||||
namespace sphaira::nca {
|
||||
|
||||
#define NCA0_MAGIC 0x3041434E
|
||||
#define NCA2_MAGIC 0x3241434E
|
||||
#define NCA3_MAGIC 0x3341434E
|
||||
|
||||
#define NCA_SECTOR_SIZE 0x200
|
||||
#define NCA_XTS_SECTION_SIZE 0xC00
|
||||
#define NCA_SECTION_TOTAL 0x4
|
||||
#define NCA_MEDIA_REAL(x)((x * 0x200))
|
||||
|
||||
#define NCA_PROGRAM_LOGO_OFFSET 0x8000
|
||||
#define NCA_META_CNMT_OFFSET 0xC20
|
||||
|
||||
enum KeyGenerationOld {
|
||||
KeyGenerationOld_100 = 0x0,
|
||||
KeyGenerationOld_Unused = 0x1,
|
||||
KeyGenerationOld_300 = 0x2,
|
||||
};
|
||||
|
||||
enum KeyGeneration {
|
||||
KeyGeneration_301 = 0x3,
|
||||
KeyGeneration_400 = 0x4,
|
||||
KeyGeneration_500 = 0x5,
|
||||
KeyGeneration_600 = 0x6,
|
||||
KeyGeneration_620 = 0x7,
|
||||
KeyGeneration_700 = 0x8,
|
||||
KeyGeneration_810 = 0x9,
|
||||
KeyGeneration_900 = 0x0A,
|
||||
KeyGeneration_910 = 0x0B,
|
||||
KeyGeneration_1210 = 0x0C,
|
||||
KeyGeneration_1300 = 0x0D,
|
||||
KeyGeneration_1400 = 0x0E,
|
||||
KeyGeneration_1500 = 0x0F,
|
||||
KeyGeneration_1600 = 0x10,
|
||||
KeyGeneration_1700 = 0x11,
|
||||
KeyGeneration_1800 = 0x12,
|
||||
KeyGeneration_1900 = 0x13,
|
||||
KeyGeneration_Invalid = 0xFF,
|
||||
};
|
||||
|
||||
enum KeyAreaEncryptionKeyIndex {
|
||||
KeyAreaEncryptionKeyIndex_Application = 0x0,
|
||||
KeyAreaEncryptionKeyIndex_Ocean = 0x1,
|
||||
KeyAreaEncryptionKeyIndex_System = 0x2
|
||||
};
|
||||
|
||||
enum DistributionType {
|
||||
DistributionType_System = 0x0,
|
||||
DistributionType_GameCard = 0x1
|
||||
};
|
||||
|
||||
enum ContentType {
|
||||
ContentType_Program = 0x0,
|
||||
ContentType_Meta = 0x1,
|
||||
ContentType_Control = 0x2,
|
||||
ContentType_Manual = 0x3,
|
||||
ContentType_Data = 0x4,
|
||||
ContentType_PublicData = 0x5,
|
||||
};
|
||||
|
||||
enum FileSystemType {
|
||||
FileSystemType_RomFS = 0x0,
|
||||
FileSystemType_PFS0 = 0x1
|
||||
};
|
||||
|
||||
enum HashType {
|
||||
HashType_Auto = 0x0,
|
||||
HashType_HierarchicalSha256 = 0x2,
|
||||
HashType_HierarchicalIntegrity = 0x3
|
||||
};
|
||||
|
||||
enum EncryptionType {
|
||||
EncryptionType_Auto = 0x0,
|
||||
EncryptionType_None = 0x1,
|
||||
EncryptionType_AesXts = 0x2,
|
||||
EncryptionType_AesCtr = 0x3,
|
||||
EncryptionType_AesCtrEx = 0x4,
|
||||
EncryptionType_AesCtrSkipLayerHash = 0x5, // [14.0.0+]
|
||||
EncryptionType_AesCtrExSkipLayerHash = 0x6, // [14.0.0+]
|
||||
};
|
||||
|
||||
struct SectionTableEntry {
|
||||
u32 media_start_offset; // divided by 0x200.
|
||||
u32 media_end_offset; // divided by 0x200.
|
||||
u8 _0x8[0x4]; // unknown.
|
||||
u8 _0xC[0x4]; // unknown.
|
||||
};
|
||||
|
||||
struct LayerRegion {
|
||||
u64 offset;
|
||||
u64 size;
|
||||
};
|
||||
|
||||
struct HierarchicalSha256Data {
|
||||
u8 master_hash[0x20];
|
||||
u32 block_size;
|
||||
u32 layer_count;
|
||||
LayerRegion hash_layer;
|
||||
LayerRegion pfs0_layer;
|
||||
LayerRegion unused_layers[3];
|
||||
u8 _0x78[0x80];
|
||||
};
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct HierarchicalIntegrityVerificationLevelInformation {
|
||||
u64 logical_offset;
|
||||
u64 hash_data_size;
|
||||
u32 block_size; // log2
|
||||
u32 _0x14; // reserved
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
struct InfoLevelHash {
|
||||
u32 max_layers;
|
||||
HierarchicalIntegrityVerificationLevelInformation levels[6];
|
||||
u8 signature_salt[0x20];
|
||||
};
|
||||
|
||||
struct IntegrityMetaInfo {
|
||||
u32 magic; // IVFC
|
||||
u32 version;
|
||||
u32 master_hash_size;
|
||||
InfoLevelHash info_level_hash;
|
||||
u8 master_hash[0x20];
|
||||
u8 _0xE0[0x18];
|
||||
};
|
||||
|
||||
static_assert(sizeof(HierarchicalSha256Data) == 0xF8);
|
||||
static_assert(sizeof(IntegrityMetaInfo) == 0xF8);
|
||||
static_assert(sizeof(HierarchicalSha256Data) == sizeof(IntegrityMetaInfo));
|
||||
|
||||
struct FsHeader {
|
||||
u16 version; // always 2.
|
||||
u8 fs_type; // see FileSystemType.
|
||||
u8 hash_type; // see HashType.
|
||||
u8 encryption_type; // see EncryptionType.
|
||||
u8 metadata_hash_type;
|
||||
u8 _0x6[0x2]; // empty.
|
||||
|
||||
union {
|
||||
HierarchicalSha256Data hierarchical_sha256_data;
|
||||
IntegrityMetaInfo integrity_meta_info; // used for romfs
|
||||
} hash_data;
|
||||
|
||||
u8 patch_info[0x40];
|
||||
u64 section_ctr;
|
||||
u8 spares_info[0x30];
|
||||
u8 compression_info[0x28];
|
||||
u8 meta_data_hash_data_info[0x30];
|
||||
u8 reserved[0x30];
|
||||
};
|
||||
static_assert(sizeof(FsHeader) == 0x200);
|
||||
static_assert(sizeof(FsHeader::hash_data) == 0xF8);
|
||||
|
||||
struct SectionHeaderHash {
|
||||
u8 sha256[0x20];
|
||||
};
|
||||
|
||||
struct KeyArea {
|
||||
u8 area[0x10];
|
||||
};
|
||||
|
||||
struct Header {
|
||||
u8 rsa_fixed_key[0x100];
|
||||
u8 rsa_npdm[0x100]; // key from npdm.
|
||||
u32 magic;
|
||||
u8 distribution_type; // see DistributionType.
|
||||
u8 content_type; // see ContentType.
|
||||
u8 old_key_gen; // see KeyGenerationOld.
|
||||
u8 kaek_index; // see KeyAreaEncryptionKeyIndex.
|
||||
u64 size;
|
||||
u64 title_id;
|
||||
u32 context_id;
|
||||
u32 sdk_version;
|
||||
u8 key_gen; // see KeyGeneration.
|
||||
u8 sig_key_gen;
|
||||
u8 _0x222[0xE]; // empty.
|
||||
FsRightsId rights_id;
|
||||
|
||||
SectionTableEntry fs_table[NCA_SECTION_TOTAL];
|
||||
SectionHeaderHash fs_header_hash[NCA_SECTION_TOTAL];
|
||||
KeyArea key_area[NCA_SECTION_TOTAL];
|
||||
|
||||
u8 _0x340[0xC0]; // empty.
|
||||
|
||||
FsHeader fs_header[NCA_SECTION_TOTAL];
|
||||
|
||||
auto GetKeyGeneration() const -> u8 {
|
||||
if (old_key_gen < key_gen) {
|
||||
return key_gen;
|
||||
} else {
|
||||
return old_key_gen;
|
||||
}
|
||||
}
|
||||
|
||||
void SetKeyGeneration(u8 key_generation) {
|
||||
if (key_generation <= 0x2) {
|
||||
old_key_gen = key_generation;
|
||||
key_gen = 0;
|
||||
} else {
|
||||
old_key_gen = 0x2;
|
||||
key_gen = key_generation;
|
||||
}
|
||||
}
|
||||
};
|
||||
static_assert(sizeof(Header) == 0xC00);
|
||||
|
||||
Result DecryptKeak(const keys::Keys& keys, Header& header);
|
||||
Result EncryptKeak(const keys::Keys& keys, Header& header, u8 key_generation);
|
||||
Result VerifyFixedKey(const Header& header);
|
||||
|
||||
auto GetKeyGenStr(u8 key_gen) -> const char*;
|
||||
|
||||
} // namespace sphaira::nca
|
||||
41
sphaira/include/yati/nx/ncm.hpp
Normal file
41
sphaira/include/yati/nx/ncm.hpp
Normal file
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
#include <switch.h>
|
||||
|
||||
namespace sphaira::ncm {
|
||||
|
||||
struct PackagedContentMeta {
|
||||
u64 title_id;
|
||||
u32 title_version;
|
||||
u8 meta_type; // NcmContentMetaType
|
||||
u8 content_meta_platform; // [17.0.0+]
|
||||
NcmContentMetaHeader meta_header;
|
||||
u8 install_type; // NcmContentInstallType
|
||||
u8 _0x17;
|
||||
u32 required_sys_version;
|
||||
u8 _0x1C[0x4];
|
||||
};
|
||||
static_assert(sizeof(PackagedContentMeta) == 0x20);
|
||||
|
||||
struct ContentStorageRecord {
|
||||
NcmContentMetaKey key;
|
||||
u8 storage_id; // NcmStorageId
|
||||
u8 padding[0x7];
|
||||
};
|
||||
|
||||
union ExtendedHeader {
|
||||
NcmApplicationMetaExtendedHeader application;
|
||||
NcmPatchMetaExtendedHeader patch;
|
||||
NcmAddOnContentMetaExtendedHeader addon;
|
||||
NcmLegacyAddOnContentMetaExtendedHeader addon_legacy;
|
||||
NcmDataPatchMetaExtendedHeader data_patch;
|
||||
};
|
||||
|
||||
auto GetAppId(u8 meta_type, u64 id) -> u64;
|
||||
auto GetAppId(const NcmContentMetaKey& key) -> u64;
|
||||
auto GetAppId(const PackagedContentMeta& meta) -> u64;
|
||||
|
||||
Result Delete(NcmContentStorage* cs, const NcmContentId *content_id);
|
||||
Result Register(NcmContentStorage* cs, const NcmContentId *content_id, const NcmPlaceHolderId *placeholder_id);
|
||||
|
||||
} // namespace sphaira::ncm
|
||||
54
sphaira/include/yati/nx/ncz.hpp
Normal file
54
sphaira/include/yati/nx/ncz.hpp
Normal file
@@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
|
||||
#include <switch.h>
|
||||
|
||||
namespace sphaira::ncz {
|
||||
|
||||
#define NCZ_SECTION_MAGIC 0x4E544345535A434EUL
|
||||
// todo: byteswap this
|
||||
#define NCZ_BLOCK_MAGIC std::byteswap(0x4E435A424C4F434BUL)
|
||||
|
||||
#define NCZ_SECTION_OFFSET (0x4000 + sizeof(ncz::Header))
|
||||
|
||||
struct Header {
|
||||
u64 magic; // NCZ_SECTION_MAGIC
|
||||
u64 total_sections;
|
||||
};
|
||||
|
||||
struct BlockHeader {
|
||||
u64 magic; // NCZ_BLOCK_MAGIC
|
||||
u8 version;
|
||||
u8 type;
|
||||
u8 padding;
|
||||
u8 block_size_exponent;
|
||||
u32 total_blocks;
|
||||
u64 decompressed_size;
|
||||
};
|
||||
|
||||
struct Block {
|
||||
u32 size;
|
||||
};
|
||||
|
||||
struct BlockInfo {
|
||||
u64 offset; // compressed offset.
|
||||
u64 size; // compressed size.
|
||||
|
||||
auto InRange(u64 off) const -> bool {
|
||||
return off < offset + size && off >= offset;
|
||||
}
|
||||
};
|
||||
|
||||
struct Section {
|
||||
u64 offset;
|
||||
u64 size;
|
||||
u64 crypto_type;
|
||||
u64 padding;
|
||||
u8 key[0x10];
|
||||
u8 counter[0x10];
|
||||
|
||||
auto InRange(u64 off) const -> bool {
|
||||
return off < offset + size && off >= offset;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace sphaira::ncz
|
||||
62
sphaira/include/yati/nx/npdm.hpp
Normal file
62
sphaira/include/yati/nx/npdm.hpp
Normal file
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
|
||||
#include <switch.h>
|
||||
|
||||
namespace sphaira::npdm {
|
||||
|
||||
struct Meta {
|
||||
u32 magic; // "META"
|
||||
u32 signature_key_generation; // +9.0.0
|
||||
u32 _0x8;
|
||||
u8 flags;
|
||||
u8 _0xD;
|
||||
u8 main_thread_priority;
|
||||
u8 main_thread_core_num;
|
||||
u32 _0x10;
|
||||
u32 sys_resource_size; // +3.0.0
|
||||
u32 version;
|
||||
u32 main_thread_stack_size;
|
||||
char title_name[0x10];
|
||||
char product_code[0x10];
|
||||
u8 _0x40[0x30];
|
||||
u32 aci0_offset;
|
||||
u32 aci0_size;
|
||||
u32 acid_offset;
|
||||
u32 acid_size;
|
||||
};
|
||||
|
||||
struct Acid {
|
||||
u8 rsa_sig[0x100];
|
||||
u8 rsa_pub[0x100];
|
||||
u32 magic; // "ACID"
|
||||
u32 size;
|
||||
u8 version;
|
||||
u8 _0x209[0x1];
|
||||
u8 _0x20A[0x2];
|
||||
u32 flags;
|
||||
u64 program_id_min;
|
||||
u64 program_id_max;
|
||||
u32 fac_offset;
|
||||
u32 fac_size;
|
||||
u32 sac_offset;
|
||||
u32 sac_size;
|
||||
u32 kac_offset;
|
||||
u32 kac_size;
|
||||
u8 _0x238[0x8];
|
||||
};
|
||||
|
||||
struct Aci0 {
|
||||
u32 magic; // "ACI0"
|
||||
u8 _0x4[0xC];
|
||||
u64 program_id;
|
||||
u8 _0x18[0x8];
|
||||
u32 fac_offset;
|
||||
u32 fac_size;
|
||||
u32 sac_offset;
|
||||
u32 sac_size;
|
||||
u32 kac_offset;
|
||||
u32 kac_size;
|
||||
u8 _0x38[0x8];
|
||||
};
|
||||
|
||||
} // namespace sphaira::npdm
|
||||
22
sphaira/include/yati/nx/ns.hpp
Normal file
22
sphaira/include/yati/nx/ns.hpp
Normal file
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <switch.h>
|
||||
#include "ncm.hpp"
|
||||
|
||||
namespace sphaira::ns {
|
||||
|
||||
enum ApplicationRecordType {
|
||||
// installed
|
||||
ApplicationRecordType_Installed = 0x3,
|
||||
// application is gamecard, but gamecard isn't insterted
|
||||
ApplicationRecordType_GamecardMissing = 0x5,
|
||||
// archived
|
||||
ApplicationRecordType_Archived = 0xB,
|
||||
};
|
||||
|
||||
Result PushApplicationRecord(Service* srv, u64 tid, const ncm::ContentStorageRecord* records, u32 count);
|
||||
Result ListApplicationRecordContentMeta(Service* srv, u64 offset, u64 tid, ncm::ContentStorageRecord* out_records, u32 count, s32* entries_read);
|
||||
Result DeleteApplicationRecord(Service* srv, u64 tid);
|
||||
Result InvalidateApplicationControlCache(Service* srv, u64 tid);
|
||||
|
||||
} // namespace sphaira::ns
|
||||
62
sphaira/include/yati/nx/nxdumptool_rsa.h
Normal file
62
sphaira/include/yati/nx/nxdumptool_rsa.h
Normal file
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* rsa.c
|
||||
*
|
||||
* Copyright (c) 2018-2019, SciresM.
|
||||
* Copyright (c) 2020-2024, DarkMatterCore <pabloacurielz@gmail.com>.
|
||||
*
|
||||
* This file is part of nxdumptool (https://github.com/DarkMatterCore/nxdumptool).
|
||||
*
|
||||
* nxdumptool is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* nxdumptool is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifndef __RSA_H__
|
||||
#define __RSA_H__
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#define RSA2048_BYTES 0x100
|
||||
#define RSA2048_BITS (RSA2048_BYTES * 8)
|
||||
|
||||
#define RSA2048_SIG_SIZE RSA2048_BYTES
|
||||
#define RSA2048_PUBKEY_SIZE RSA2048_BYTES
|
||||
|
||||
/// Verifies a RSA-2048-PSS with SHA-256 signature.
|
||||
/// Suitable for NCA and NPDM signatures.
|
||||
/// The provided signature and modulus must have sizes of at least RSA2048_SIG_SIZE and RSA2048_PUBKEY_SIZE, respectively.
|
||||
bool rsa2048VerifySha256BasedPssSignature(const void *data, size_t data_size, const void *signature, const void *modulus, const void *public_exponent, size_t public_exponent_size);
|
||||
|
||||
/// Verifies a RSA-2048-PKCS#1 v1.5 with SHA-256 signature.
|
||||
/// Suitable for ticket and certificate chain signatures.
|
||||
/// The provided signature and modulus must have sizes of at least RSA2048_SIG_SIZE and RSA2048_PUBKEY_SIZE, respectively.
|
||||
bool rsa2048VerifySha256BasedPkcs1v15Signature(const void *data, size_t data_size, const void *signature, const void *modulus, const void *public_exponent, size_t public_exponent_size);
|
||||
|
||||
/// Performs RSA-2048-OAEP decryption.
|
||||
/// Suitable to decrypt the titlekey block from personalized tickets.
|
||||
/// The provided signature and modulus must have sizes of at least RSA2048_SIG_SIZE and RSA2048_PUBKEY_SIZE, respectively.
|
||||
/// 'label' and 'label_size' arguments are optional -- if not needed, these may be set to NULL and 0, respectively.
|
||||
bool rsa2048OaepDecrypt(void *dst, size_t dst_size, const void *signature, const void *modulus, const void *public_exponent, size_t public_exponent_size, const void *private_exponent, \
|
||||
size_t private_exponent_size, const void *label, size_t label_size, size_t *out_size);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* __RSA_H__ */
|
||||
25
sphaira/include/yati/source/base.hpp
Normal file
25
sphaira/include/yati/source/base.hpp
Normal file
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <switch.h>
|
||||
|
||||
namespace sphaira::yati::source {
|
||||
|
||||
struct Base {
|
||||
virtual ~Base() = default;
|
||||
// virtual Result Read(void* buf, s64 off, s64 size, u64* bytes_read) = 0;
|
||||
virtual Result Read(void* buf, s64 off, s64 size, u64* bytes_read) = 0;
|
||||
|
||||
virtual bool IsStream() const {
|
||||
return false;
|
||||
}
|
||||
|
||||
Result GetOpenResult() const {
|
||||
return m_open_result;
|
||||
}
|
||||
|
||||
protected:
|
||||
Result m_open_result{};
|
||||
};
|
||||
|
||||
} // namespace sphaira::yati::source
|
||||
19
sphaira/include/yati/source/file.hpp
Normal file
19
sphaira/include/yati/source/file.hpp
Normal file
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include "base.hpp"
|
||||
#include "fs.hpp"
|
||||
#include <switch.h>
|
||||
|
||||
namespace sphaira::yati::source {
|
||||
|
||||
struct File final : Base {
|
||||
File(FsFileSystem* fs, const fs::FsPath& path);
|
||||
~File();
|
||||
|
||||
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override;
|
||||
|
||||
private:
|
||||
FsFile m_file{};
|
||||
};
|
||||
|
||||
} // namespace sphaira::yati::source
|
||||
20
sphaira/include/yati/source/stdio.hpp
Normal file
20
sphaira/include/yati/source/stdio.hpp
Normal file
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include "base.hpp"
|
||||
#include "fs.hpp"
|
||||
#include <cstdio>
|
||||
#include <switch.h>
|
||||
|
||||
namespace sphaira::yati::source {
|
||||
|
||||
struct Stdio final : Base {
|
||||
Stdio(const fs::FsPath& path);
|
||||
~Stdio();
|
||||
|
||||
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override;
|
||||
|
||||
private:
|
||||
std::FILE* m_file{};
|
||||
};
|
||||
|
||||
} // namespace sphaira::yati::source
|
||||
28
sphaira/include/yati/source/stream.hpp
Normal file
28
sphaira/include/yati/source/stream.hpp
Normal file
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include "base.hpp"
|
||||
#include <vector>
|
||||
#include <switch.h>
|
||||
|
||||
namespace sphaira::yati::source {
|
||||
|
||||
// streams are for data that do not allow for random access,
|
||||
// such as FTP or MTP.
|
||||
struct Stream : Base {
|
||||
virtual ~Stream() = default;
|
||||
virtual Result ReadChunk(void* buf, s64 size, u64* bytes_read) = 0;
|
||||
|
||||
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override;
|
||||
|
||||
bool IsStream() const override {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected:
|
||||
Result m_open_result{};
|
||||
|
||||
private:
|
||||
s64 m_offset{};
|
||||
};
|
||||
|
||||
} // namespace sphaira::yati::source
|
||||
22
sphaira/include/yati/source/stream_file.hpp
Normal file
22
sphaira/include/yati/source/stream_file.hpp
Normal file
@@ -0,0 +1,22 @@
|
||||
// this is used for testing that streams work, this code isn't used in normal
|
||||
// release builds as it is slower / less feature complete than normal.
|
||||
#pragma once
|
||||
|
||||
#include "stream.hpp"
|
||||
#include "fs.hpp"
|
||||
#include <switch.h>
|
||||
|
||||
namespace sphaira::yati::source {
|
||||
|
||||
struct StreamFile final : Stream {
|
||||
StreamFile(FsFileSystem* fs, const fs::FsPath& path);
|
||||
~StreamFile();
|
||||
|
||||
Result ReadChunk(void* buf, s64 size, u64* bytes_read) override;
|
||||
|
||||
private:
|
||||
FsFile m_file{};
|
||||
s64 m_offset{};
|
||||
};
|
||||
|
||||
} // namespace sphaira::yati::source
|
||||
52
sphaira/include/yati/source/usb.hpp
Normal file
52
sphaira/include/yati/source/usb.hpp
Normal file
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include "base.hpp"
|
||||
#include "fs.hpp"
|
||||
#include <switch.h>
|
||||
|
||||
namespace sphaira::yati::source {
|
||||
|
||||
struct Usb final : Base {
|
||||
enum { USBModule = 523 };
|
||||
|
||||
enum : Result {
|
||||
Result_BadMagic = MAKERESULT(USBModule, 0),
|
||||
Result_BadVersion = MAKERESULT(USBModule, 1),
|
||||
Result_BadCount = MAKERESULT(USBModule, 2),
|
||||
Result_BadTransferSize = MAKERESULT(USBModule, 3),
|
||||
Result_BadTotalSize = MAKERESULT(USBModule, 4),
|
||||
};
|
||||
|
||||
Usb(u64 transfer_timeout);
|
||||
~Usb();
|
||||
|
||||
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override;
|
||||
Result Finished() const;
|
||||
|
||||
Result Init();
|
||||
Result WaitForConnection(u64 timeout, u32& speed, u32& count);
|
||||
Result GetFileInfo(std::string& name_out, u64& size_out);
|
||||
|
||||
private:
|
||||
enum UsbSessionEndpoint {
|
||||
UsbSessionEndpoint_In = 0,
|
||||
UsbSessionEndpoint_Out = 1,
|
||||
};
|
||||
|
||||
Result SendCommand(s64 off, s64 size) const;
|
||||
Result InternalRead(void* buf, s64 off, s64 size) const;
|
||||
|
||||
bool GetConfigured() const;
|
||||
Event *GetCompletionEvent(UsbSessionEndpoint ep) const;
|
||||
Result WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) const;
|
||||
Result TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 size, u32 *out_urb_id) const;
|
||||
Result GetTransferResult(UsbSessionEndpoint ep, u32 urb_id, u32 *out_requested_size, u32 *out_transferred_size) const;
|
||||
Result TransferPacketImpl(bool read, void *page, u32 size, u32 *out_size_transferred, u64 timeout) const;
|
||||
|
||||
private:
|
||||
UsbDsInterface* m_interface{};
|
||||
UsbDsEndpoint* m_endpoints[2]{};
|
||||
u64 m_transfer_timeout{};
|
||||
};
|
||||
|
||||
} // namespace sphaira::yati::source
|
||||
138
sphaira/include/yati/yati.hpp
Normal file
138
sphaira/include/yati/yati.hpp
Normal file
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* Notes:
|
||||
* - nca's that use title key encryption are decrypted using Tegra SE, whereas
|
||||
* standard crypto uses software decryption.
|
||||
* The latter is almost always (slightly) faster, and removed the need for es patch.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "fs.hpp"
|
||||
#include "source/base.hpp"
|
||||
#include "container/base.hpp"
|
||||
#include "nx/ncm.hpp"
|
||||
#include "ui/progress_box.hpp"
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
namespace sphaira::yati {
|
||||
|
||||
enum { YatiModule = 521 };
|
||||
|
||||
enum : Result {
|
||||
// unkown container for the source provided.
|
||||
Result_ContainerNotFound = MAKERESULT(YatiModule, 10),
|
||||
Result_Cancelled = MAKERESULT(YatiModule, 11),
|
||||
|
||||
// nca required by the cnmt but not found in collection.
|
||||
Result_NcaNotFound = MAKERESULT(YatiModule, 30),
|
||||
Result_InvalidNcaReadSize = MAKERESULT(YatiModule, 31),
|
||||
Result_InvalidNcaSigKeyGen = MAKERESULT(YatiModule, 32),
|
||||
Result_InvalidNcaMagic = MAKERESULT(YatiModule, 33),
|
||||
Result_InvalidNcaSignature0 = MAKERESULT(YatiModule, 34),
|
||||
Result_InvalidNcaSignature1 = MAKERESULT(YatiModule, 35),
|
||||
// invalid sha256 over the entire nca.
|
||||
Result_InvalidNcaSha256 = MAKERESULT(YatiModule, 36),
|
||||
|
||||
// section could not be found.
|
||||
Result_NczSectionNotFound = MAKERESULT(YatiModule, 50),
|
||||
// section count == 0.
|
||||
Result_InvalidNczSectionCount = MAKERESULT(YatiModule, 51),
|
||||
// block could not be found.
|
||||
Result_NczBlockNotFound = MAKERESULT(YatiModule, 52),
|
||||
// block version != 2.
|
||||
Result_InvalidNczBlockVersion = MAKERESULT(YatiModule, 53),
|
||||
// block type != 1.
|
||||
Result_InvalidNczBlockType = MAKERESULT(YatiModule, 54),
|
||||
// block count == 0.
|
||||
Result_InvalidNczBlockTotal = MAKERESULT(YatiModule, 55),
|
||||
// block size exponent < 14 || > 32.
|
||||
Result_InvalidNczBlockSizeExponent = MAKERESULT(YatiModule, 56),
|
||||
// zstd error while decompressing ncz.
|
||||
Result_InvalidNczZstdError = MAKERESULT(YatiModule, 57),
|
||||
|
||||
// nca has rights_id but matching ticket wasn't found.
|
||||
Result_TicketNotFound = MAKERESULT(YatiModule, 70),
|
||||
// found ticket has missmatching rights_id from it's name.
|
||||
Result_InvalidTicketBadRightsId = MAKERESULT(YatiModule, 71),
|
||||
Result_InvalidTicketVersion = MAKERESULT(YatiModule, 72),
|
||||
Result_InvalidTicketKeyType = MAKERESULT(YatiModule, 73),
|
||||
Result_InvalidTicketKeyRevision = MAKERESULT(YatiModule, 74),
|
||||
|
||||
// cert not found for the ticket.
|
||||
Result_CertNotFound = MAKERESULT(YatiModule, 90),
|
||||
|
||||
// unable to fetch header from ncm database.
|
||||
Result_NcmDbCorruptHeader = MAKERESULT(YatiModule, 110),
|
||||
// unable to total infos from ncm database.
|
||||
Result_NcmDbCorruptInfos = MAKERESULT(YatiModule, 111),
|
||||
};
|
||||
|
||||
struct Config {
|
||||
bool sd_card_install{};
|
||||
|
||||
// enables downgrading patch / data patch (dlc) version.
|
||||
bool allow_downgrade{};
|
||||
|
||||
// ignores the install if already installed.
|
||||
// checks that every nca is available.
|
||||
bool skip_if_already_installed{};
|
||||
|
||||
// installs tickets only.
|
||||
bool ticket_only{};
|
||||
|
||||
// flags to enable / disable install of specific types.
|
||||
bool skip_base{};
|
||||
bool skip_patch{};
|
||||
bool skip_addon{};
|
||||
bool skip_data_patch{};
|
||||
bool skip_ticket{};
|
||||
|
||||
// enables the option to skip sha256 verification.
|
||||
bool skip_nca_hash_verify{};
|
||||
|
||||
// enables the option to skip rsa nca fixed key verification.
|
||||
bool skip_rsa_header_fixed_key_verify{};
|
||||
|
||||
// enables the option to skip rsa npdm fixed key verification.
|
||||
bool skip_rsa_npdm_fixed_key_verify{};
|
||||
|
||||
// if set, it will ignore the distribution bit in the nca header.
|
||||
bool ignore_distribution_bit{};
|
||||
|
||||
// converts titlekey to standard crypto, also known as "ticketless".
|
||||
// this will not work with addon (dlc), so, addon tickets will be installed.
|
||||
bool convert_to_standard_crypto{};
|
||||
|
||||
// encrypts the keak with master key 0, this allows the game to be launched on every fw.
|
||||
// implicitly performs standard crypto.
|
||||
bool lower_master_key{};
|
||||
|
||||
// sets the system_firmware field in the cnmt extended header.
|
||||
// if mkey is higher than fw version, the game still won't launch
|
||||
// as the fw won't have the key to decrypt keak.
|
||||
bool lower_system_version{};
|
||||
};
|
||||
|
||||
// overridable options, set to avoid
|
||||
struct ConfigOverride {
|
||||
std::optional<bool> sd_card_install{};
|
||||
std::optional<bool> skip_nca_hash_verify{};
|
||||
std::optional<bool> skip_rsa_header_fixed_key_verify{};
|
||||
std::optional<bool> skip_rsa_npdm_fixed_key_verify{};
|
||||
std::optional<bool> ignore_distribution_bit{};
|
||||
std::optional<bool> convert_to_standard_crypto{};
|
||||
std::optional<bool> lower_master_key{};
|
||||
std::optional<bool> lower_system_version{};
|
||||
};
|
||||
|
||||
Result InstallFromFile(ui::ProgressBox* pbox, FsFileSystem* fs, const fs::FsPath& path, const ConfigOverride& override = {});
|
||||
Result InstallFromStdioFile(ui::ProgressBox* pbox, const fs::FsPath& path, const ConfigOverride& override = {});
|
||||
Result InstallFromSource(ui::ProgressBox* pbox, std::shared_ptr<source::Base> source, const fs::FsPath& path, const ConfigOverride& override = {});
|
||||
Result InstallFromContainer(ui::ProgressBox* pbox, std::shared_ptr<container::Base> container, const ConfigOverride& override = {});
|
||||
Result InstallFromCollections(ui::ProgressBox* pbox, std::shared_ptr<source::Base> source, const container::Collections& collections, const ConfigOverride& override = {});
|
||||
|
||||
Result ParseCnmtNca(const fs::FsPath& path, ncm::PackagedContentMeta& header, std::vector<u8>& extended_header, std::vector<NcmPackagedContentInfo>& infos);
|
||||
Result ParseControlNca(const fs::FsPath& path, u64 id, void* nacp_out = nullptr, s64 nacp_size = 0, std::vector<u8>* icon_out = nullptr);
|
||||
|
||||
} // namespace sphaira::yati
|
||||
@@ -1,7 +1,18 @@
|
||||
#include "ui/menus/main_menu.hpp"
|
||||
#include "ui/error_box.hpp"
|
||||
#include "ui/option_box.hpp"
|
||||
#include "ui/bubbles.hpp"
|
||||
#include "ui/sidebar.hpp"
|
||||
#include "ui/popup_list.hpp"
|
||||
#include "ui/option_box.hpp"
|
||||
#include "ui/progress_box.hpp"
|
||||
#include "ui/error_box.hpp"
|
||||
|
||||
#include "ui/menus/main_menu.hpp"
|
||||
#include "ui/menus/irs_menu.hpp"
|
||||
#include "ui/menus/themezer.hpp"
|
||||
#include "ui/menus/ghdl.hpp"
|
||||
#include "ui/menus/usb_menu.hpp"
|
||||
#include "ui/menus/ftp_menu.hpp"
|
||||
#include "ui/menus/gc_menu.hpp"
|
||||
|
||||
#include "app.hpp"
|
||||
#include "log.hpp"
|
||||
@@ -15,6 +26,7 @@
|
||||
#include "defines.hpp"
|
||||
#include "i18n.hpp"
|
||||
#include "ftpsrv_helper.hpp"
|
||||
#include "web.hpp"
|
||||
|
||||
#include <nanovg_dk.h>
|
||||
#include <minIni.h>
|
||||
@@ -544,7 +556,7 @@ auto App::GetThemeIndex() -> s64 {
|
||||
return g_app->m_theme_index;
|
||||
}
|
||||
|
||||
auto App::GetDefaultImage(int* w, int* h) -> int {
|
||||
auto App::GetDefaultImage() -> int {
|
||||
return g_app->m_default_image;
|
||||
}
|
||||
|
||||
@@ -580,10 +592,6 @@ auto App::GetInstallPrompt() -> bool {
|
||||
return g_app->m_install_prompt.Get();
|
||||
}
|
||||
|
||||
auto App::GetThemeShuffleEnable() -> bool {
|
||||
return g_app->m_theme_shuffle.Get();
|
||||
}
|
||||
|
||||
auto App::GetThemeMusicEnable() -> bool {
|
||||
return g_app->m_theme_music.Get();
|
||||
}
|
||||
@@ -741,10 +749,6 @@ void App::SetInstallPrompt(bool enable) {
|
||||
g_app->m_install_prompt.Set(enable);
|
||||
}
|
||||
|
||||
void App::SetThemeShuffleEnable(bool enable) {
|
||||
g_app->m_theme_shuffle.Set(enable);
|
||||
}
|
||||
|
||||
void App::SetThemeMusicEnable(bool enable) {
|
||||
g_app->m_theme_music.Set(enable);
|
||||
PlaySoundEffect(SoundEffect::SoundEffect_Music);
|
||||
@@ -1016,6 +1020,10 @@ void App::Draw() {
|
||||
this->queue.presentImage(this->swapchain, slot);
|
||||
}
|
||||
|
||||
auto App::GetApp() -> App* {
|
||||
return g_app;
|
||||
}
|
||||
|
||||
auto App::GetVg() -> NVGcontext* {
|
||||
return g_app->vg;
|
||||
}
|
||||
@@ -1323,11 +1331,7 @@ App::App(const char* argv0) {
|
||||
|
||||
fs::FsPath theme_path{};
|
||||
constexpr fs::FsPath default_theme_path{"romfs:/themes/abyss_theme.ini"};
|
||||
if (App::GetThemeShuffleEnable() && m_theme_meta_entries.size()) {
|
||||
theme_path = m_theme_meta_entries[randomGet64() % m_theme_meta_entries.size()].ini_path;
|
||||
} else {
|
||||
ini_gets("config", "theme", default_theme_path, theme_path, sizeof(theme_path), CONFIG_PATH);
|
||||
}
|
||||
ini_gets("config", "theme", default_theme_path, theme_path, sizeof(theme_path), CONFIG_PATH);
|
||||
|
||||
// try and load previous theme, default to previous version otherwise.
|
||||
ThemeMeta theme_meta;
|
||||
@@ -1382,15 +1386,6 @@ App::App(const char* argv0) {
|
||||
const long old_launch_count = ini_getl(GetExePath(), "launch_count", 0, App::PLAYLOG_PATH);
|
||||
ini_putl(GetExePath(), "launch_count", old_launch_count + 1, App::PLAYLOG_PATH);
|
||||
|
||||
s64 sd_free_space;
|
||||
if (R_SUCCEEDED(fs.GetFreeSpace("/", &sd_free_space))) {
|
||||
log_write("sd_free_space: %zd\n", sd_free_space);
|
||||
}
|
||||
s64 sd_total_space;
|
||||
if (R_SUCCEEDED(fs.GetTotalSpace("/", &sd_total_space))) {
|
||||
log_write("sd_total_space: %zd\n", sd_total_space);
|
||||
}
|
||||
|
||||
// load default image
|
||||
if (R_SUCCEEDED(romfsInit())) {
|
||||
ON_SCOPE_EXIT(romfsExit());
|
||||
@@ -1400,8 +1395,33 @@ App::App(const char* argv0) {
|
||||
}
|
||||
}
|
||||
|
||||
// soon (tm)
|
||||
// ui::bubble::Init();
|
||||
struct EventDay {
|
||||
u8 day;
|
||||
u8 month;
|
||||
};
|
||||
|
||||
static constexpr EventDay event_days[] = {
|
||||
{ .day = 1, .month = 1 }, // New years
|
||||
|
||||
{ .day = 3, .month = 3 }, // March 3 (switch 1)
|
||||
{ .day = 10, .month = 5 }, // June 10 (switch 2)
|
||||
{ .day = 15, .month = 5 }, // June 15
|
||||
|
||||
{ .day = 25, .month = 12 }, // Christmas
|
||||
{ .day = 26, .month = 12 },
|
||||
{ .day = 27, .month = 12 },
|
||||
{ .day = 28, .month = 12 },
|
||||
};
|
||||
|
||||
const auto time = std::time(nullptr);
|
||||
const auto tm = std::localtime(&time);
|
||||
|
||||
for (auto e : event_days) {
|
||||
if (e.day == tm->tm_mday && e.month == (tm->tm_mon + 1)) {
|
||||
ui::bubble::Init();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
App::Push(std::make_shared<ui::menu::main::MainMenu>());
|
||||
log_write("finished app constructor\n");
|
||||
@@ -1420,6 +1440,179 @@ void App::PlaySoundEffect(SoundEffect effect) {
|
||||
plsrPlayerPlay(id);
|
||||
}
|
||||
|
||||
void App::DisplayThemeOptions(bool left_side) {
|
||||
ui::SidebarEntryArray::Items theme_items{};
|
||||
const auto theme_meta = App::GetThemeMetaList();
|
||||
for (auto& p : theme_meta) {
|
||||
theme_items.emplace_back(p.name);
|
||||
}
|
||||
|
||||
auto options = std::make_shared<ui::Sidebar>("Theme Options"_i18n, left_side ? ui::Sidebar::Side::LEFT : ui::Sidebar::Side::RIGHT);
|
||||
ON_SCOPE_EXIT(App::Push(options));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryArray>("Select Theme"_i18n, theme_items, [theme_items](s64& index_out){
|
||||
App::SetTheme(index_out);
|
||||
}, App::GetThemeIndex()));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Music"_i18n, App::GetThemeMusicEnable(), [](bool& enable){
|
||||
App::SetThemeMusicEnable(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("12 Hour Time"_i18n, App::Get12HourTimeEnable(), [](bool& enable){
|
||||
App::Set12HourTimeEnable(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
}
|
||||
|
||||
void App::DisplayNetworkOptions(bool left_side) {
|
||||
|
||||
}
|
||||
|
||||
void App::DisplayMiscOptions(bool left_side) {
|
||||
auto options = std::make_shared<ui::Sidebar>("Misc Options"_i18n, left_side ? ui::Sidebar::Side::LEFT : ui::Sidebar::Side::RIGHT);
|
||||
ON_SCOPE_EXIT(App::Push(options));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryCallback>("Themezer"_i18n, [](){
|
||||
App::Push(std::make_shared<ui::menu::themezer::Menu>());
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryCallback>("GitHub"_i18n, [](){
|
||||
App::Push(std::make_shared<ui::menu::gh::Menu>());
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryCallback>("Irs"_i18n, [](){
|
||||
App::Push(std::make_shared<ui::menu::irs::Menu>());
|
||||
}));
|
||||
|
||||
if (App::IsApplication()) {
|
||||
options->Add(std::make_shared<ui::SidebarEntryCallback>("Web"_i18n, [](){
|
||||
WebShow("https://lite.duckduckgo.com/lite");
|
||||
}));
|
||||
}
|
||||
|
||||
if (App::GetApp()->m_install.Get()) {
|
||||
if (App::GetFtpEnable()) {
|
||||
options->Add(std::make_shared<ui::SidebarEntryCallback>("Ftp Install"_i18n, [](){
|
||||
App::Push(std::make_shared<ui::menu::ftp::Menu>());
|
||||
}));
|
||||
}
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryCallback>("Usb Install"_i18n, [](){
|
||||
App::Push(std::make_shared<ui::menu::usb::Menu>());
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryCallback>("GameCard Install"_i18n, [](){
|
||||
App::Push(std::make_shared<ui::menu::gc::Menu>());
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
void App::DisplayAdvancedOptions(bool left_side) {
|
||||
auto options = std::make_shared<ui::Sidebar>("Advanced Options"_i18n, left_side ? ui::Sidebar::Side::LEFT : ui::Sidebar::Side::RIGHT);
|
||||
ON_SCOPE_EXIT(App::Push(options));
|
||||
|
||||
ui::SidebarEntryArray::Items text_scroll_speed_items;
|
||||
text_scroll_speed_items.push_back("Slow"_i18n);
|
||||
text_scroll_speed_items.push_back("Normal"_i18n);
|
||||
text_scroll_speed_items.push_back("Fast"_i18n);
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Logging"_i18n, App::GetLogEnable(), [](bool& enable){
|
||||
App::SetLogEnable(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Replace hbmenu on exit"_i18n, App::GetReplaceHbmenuEnable(), [](bool& enable){
|
||||
App::SetReplaceHbmenuEnable(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryArray>("Text scroll speed"_i18n, text_scroll_speed_items, [](s64& index_out){
|
||||
App::SetTextScrollSpeed(index_out);
|
||||
}, (s64)App::GetTextScrollSpeed()));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryCallback>("Install options"_i18n, [left_side](){
|
||||
App::DisplayInstallOptions(left_side);
|
||||
}));
|
||||
}
|
||||
|
||||
void App::DisplayInstallOptions(bool left_side) {
|
||||
auto options = std::make_shared<ui::Sidebar>("Install Options"_i18n, left_side ? ui::Sidebar::Side::LEFT : ui::Sidebar::Side::RIGHT);
|
||||
ON_SCOPE_EXIT(App::Push(options));
|
||||
|
||||
ui::SidebarEntryArray::Items install_items;
|
||||
install_items.push_back("System memory"_i18n);
|
||||
install_items.push_back("microSD card"_i18n);
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Enable"_i18n, App::GetInstallEnable(), [](bool& enable){
|
||||
App::SetInstallEnable(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Show install warning"_i18n, App::GetInstallPrompt(), [](bool& enable){
|
||||
App::SetInstallPrompt(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryArray>("Install location"_i18n, install_items, [](s64& index_out){
|
||||
App::SetInstallSdEnable(index_out);
|
||||
}, (s64)App::GetInstallSdEnable()));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Allow downgrade"_i18n, App::GetApp()->m_allow_downgrade.Get(), [](bool& enable){
|
||||
App::GetApp()->m_allow_downgrade.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip if already installed"_i18n, App::GetApp()->m_skip_if_already_installed.Get(), [](bool& enable){
|
||||
App::GetApp()->m_skip_if_already_installed.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Ticket only"_i18n, App::GetApp()->m_ticket_only.Get(), [](bool& enable){
|
||||
App::GetApp()->m_ticket_only.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip base"_i18n, App::GetApp()->m_skip_base.Get(), [](bool& enable){
|
||||
App::GetApp()->m_skip_base.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip Patch"_i18n, App::GetApp()->m_skip_patch.Get(), [](bool& enable){
|
||||
App::GetApp()->m_skip_patch.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip addon"_i18n, App::GetApp()->m_skip_addon.Get(), [](bool& enable){
|
||||
App::GetApp()->m_skip_addon.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip data patch"_i18n, App::GetApp()->m_skip_data_patch.Get(), [](bool& enable){
|
||||
App::GetApp()->m_skip_data_patch.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip ticket"_i18n, App::GetApp()->m_skip_ticket.Get(), [](bool& enable){
|
||||
App::GetApp()->m_skip_ticket.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("skip NCA hash verify"_i18n, App::GetApp()->m_skip_nca_hash_verify.Get(), [](bool& enable){
|
||||
App::GetApp()->m_skip_nca_hash_verify.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip RSA header verify"_i18n, App::GetApp()->m_skip_rsa_header_fixed_key_verify.Get(), [](bool& enable){
|
||||
App::GetApp()->m_skip_rsa_header_fixed_key_verify.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip RSA NPDM verify"_i18n, App::GetApp()->m_skip_rsa_npdm_fixed_key_verify.Get(), [](bool& enable){
|
||||
App::GetApp()->m_skip_rsa_npdm_fixed_key_verify.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Ignore distribution bit"_i18n, App::GetApp()->m_ignore_distribution_bit.Get(), [](bool& enable){
|
||||
App::GetApp()->m_ignore_distribution_bit.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Convert to standard crypto"_i18n, App::GetApp()->m_convert_to_standard_crypto.Get(), [](bool& enable){
|
||||
App::GetApp()->m_convert_to_standard_crypto.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Lower master key"_i18n, App::GetApp()->m_lower_master_key.Get(), [](bool& enable){
|
||||
App::GetApp()->m_lower_master_key.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Lower system version"_i18n, App::GetApp()->m_lower_system_version.Get(), [](bool& enable){
|
||||
App::GetApp()->m_lower_system_version.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
}
|
||||
|
||||
App::~App() {
|
||||
log_write("starting to exit\n");
|
||||
|
||||
|
||||
@@ -12,14 +12,30 @@
|
||||
#include <nx/vfs_nx.h>
|
||||
#include <nx/utils.h>
|
||||
|
||||
namespace sphaira::ftpsrv {
|
||||
namespace {
|
||||
|
||||
struct InstallSharedData {
|
||||
std::mutex mutex;
|
||||
|
||||
std::deque<std::string> queued_files;
|
||||
|
||||
void* user;
|
||||
OnInstallStart on_start;
|
||||
OnInstallWrite on_write;
|
||||
OnInstallClose on_close;
|
||||
|
||||
bool in_progress;
|
||||
bool enabled;
|
||||
};
|
||||
|
||||
const char* INI_PATH = "/config/ftpsrv/config.ini";
|
||||
FtpSrvConfig g_ftpsrv_config = {0};
|
||||
volatile bool g_should_exit = false;
|
||||
bool g_is_running{false};
|
||||
Thread g_thread;
|
||||
std::mutex g_mutex{};
|
||||
InstallSharedData g_shared_data{};
|
||||
|
||||
void ftp_log_callback(enum FTP_API_LOG_TYPE type, const char* msg) {
|
||||
sphaira::App::NotifyFlashLed();
|
||||
@@ -29,6 +45,235 @@ void ftp_progress_callback(void) {
|
||||
sphaira::App::NotifyFlashLed();
|
||||
}
|
||||
|
||||
const char* SUPPORTED_EXT[] = {
|
||||
".nsp", ".xci", ".nsz", ".xcz",
|
||||
};
|
||||
|
||||
struct VfsUserData {
|
||||
char* path;
|
||||
int valid;
|
||||
};
|
||||
|
||||
// ive given up with good names.
|
||||
void on_thing() {
|
||||
log_write("[FTP] doing on_thing\n");
|
||||
std::scoped_lock lock{g_shared_data.mutex};
|
||||
log_write("[FTP] locked on_thing\n");
|
||||
|
||||
if (!g_shared_data.in_progress) {
|
||||
if (!g_shared_data.queued_files.empty()) {
|
||||
log_write("[FTP] pushing new file data\n");
|
||||
if (!g_shared_data.on_start || !g_shared_data.on_start(g_shared_data.user, g_shared_data.queued_files[0].c_str())) {
|
||||
g_shared_data.queued_files.clear();
|
||||
} else {
|
||||
log_write("[FTP] success on new file push\n");
|
||||
g_shared_data.in_progress = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int vfs_install_open(void* user, const char* path, enum FtpVfsOpenMode mode) {
|
||||
{
|
||||
std::scoped_lock lock{g_shared_data.mutex};
|
||||
auto data = static_cast<VfsUserData*>(user);
|
||||
data->valid = 0;
|
||||
|
||||
if (mode != FtpVfsOpenMode_WRITE) {
|
||||
errno = EACCES;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!g_shared_data.enabled) {
|
||||
errno = EACCES;
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char* ext = strrchr(path, '.');
|
||||
if (!ext) {
|
||||
errno = EACCES;
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool found = false;
|
||||
for (size_t i = 0; i < std::size(SUPPORTED_EXT); i++) {
|
||||
if (!strcasecmp(ext, SUPPORTED_EXT[i])) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// check if we already have this file queued.
|
||||
auto it = std::find(g_shared_data.queued_files.cbegin(), g_shared_data.queued_files.cend(), path);
|
||||
if (it != g_shared_data.queued_files.cend()) {
|
||||
errno = EEXIST;
|
||||
return -1;
|
||||
}
|
||||
|
||||
g_shared_data.queued_files.push_back(path);
|
||||
data->path = strdup(path);
|
||||
data->valid = true;
|
||||
}
|
||||
|
||||
on_thing();
|
||||
log_write("[FTP] got file: %s\n", path);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int vfs_install_read(void* user, void* buf, size_t size) {
|
||||
errno = EACCES;
|
||||
return -1;
|
||||
}
|
||||
|
||||
int vfs_install_write(void* user, const void* buf, size_t size) {
|
||||
std::scoped_lock lock{g_shared_data.mutex};
|
||||
if (!g_shared_data.enabled) {
|
||||
errno = EACCES;
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto data = static_cast<VfsUserData*>(user);
|
||||
if (!data->valid) {
|
||||
errno = EACCES;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!g_shared_data.on_write || !g_shared_data.on_write(g_shared_data.user, buf, size)) {
|
||||
errno = EIO;
|
||||
return -1;
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
int vfs_install_seek(void* user, const void* buf, size_t size, size_t off) {
|
||||
errno = ESPIPE;
|
||||
return -1;
|
||||
}
|
||||
|
||||
int vfs_install_isfile_open(void* user) {
|
||||
std::scoped_lock lock{g_shared_data.mutex};
|
||||
auto data = static_cast<VfsUserData*>(user);
|
||||
return data->valid;
|
||||
}
|
||||
|
||||
int vfs_install_isfile_ready(void* user) {
|
||||
std::scoped_lock lock{g_shared_data.mutex};
|
||||
auto data = static_cast<VfsUserData*>(user);
|
||||
const auto ready = !g_shared_data.queued_files.empty() && data->path == g_shared_data.queued_files[0];
|
||||
return ready;
|
||||
}
|
||||
|
||||
int vfs_install_close(void* user) {
|
||||
{
|
||||
log_write("[FTP] closing file\n");
|
||||
std::scoped_lock lock{g_shared_data.mutex};
|
||||
auto data = static_cast<VfsUserData*>(user);
|
||||
if (data->valid) {
|
||||
log_write("[FTP] closing valid file\n");
|
||||
|
||||
auto it = std::find(g_shared_data.queued_files.cbegin(), g_shared_data.queued_files.cend(), data->path);
|
||||
if (it != g_shared_data.queued_files.cend()) {
|
||||
if (it == g_shared_data.queued_files.cbegin()) {
|
||||
log_write("[FTP] closing current file\n");
|
||||
if (g_shared_data.on_close) {
|
||||
g_shared_data.on_close(g_shared_data.user);
|
||||
}
|
||||
|
||||
g_shared_data.in_progress = false;
|
||||
} else {
|
||||
log_write("[FTP] closing other file...\n");
|
||||
}
|
||||
|
||||
g_shared_data.queued_files.erase(it);
|
||||
} else {
|
||||
log_write("[FTP] could not find file in queue...\n");
|
||||
}
|
||||
|
||||
if (data->path) {
|
||||
free(data->path);
|
||||
}
|
||||
|
||||
data->valid = 0;
|
||||
}
|
||||
|
||||
memset(data, 0, sizeof(*data));
|
||||
}
|
||||
|
||||
on_thing();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int vfs_install_opendir(void* user, const char* path) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char* vfs_install_readdir(void* user, void* user_entry) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int vfs_install_dirlstat(void* user, const void* user_entry, const char* path, struct stat* st) {
|
||||
st->st_nlink = 1;
|
||||
st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int vfs_install_isdir_open(void* user) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
int vfs_install_closedir(void* user) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int vfs_install_stat(const char* path, struct stat* st) {
|
||||
st->st_nlink = 1;
|
||||
st->st_mode = S_IFDIR | S_IWUSR | S_IWGRP | S_IWOTH;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int vfs_install_mkdir(const char* path) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int vfs_install_unlink(const char* path) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int vfs_install_rmdir(const char* path) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int vfs_install_rename(const char* src, const char* dst) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
FtpVfs g_vfs_install = {
|
||||
.open = vfs_install_open,
|
||||
.read = vfs_install_read,
|
||||
.write = vfs_install_write,
|
||||
.seek = vfs_install_seek,
|
||||
.close = vfs_install_close,
|
||||
.isfile_open = vfs_install_isfile_open,
|
||||
.isfile_ready = vfs_install_isfile_ready,
|
||||
.opendir = vfs_install_opendir,
|
||||
.readdir = vfs_install_readdir,
|
||||
.dirlstat = vfs_install_dirlstat,
|
||||
.closedir = vfs_install_closedir,
|
||||
.isdir_open = vfs_install_isdir_open,
|
||||
.stat = vfs_install_stat,
|
||||
.lstat = vfs_install_stat,
|
||||
.mkdir = vfs_install_mkdir,
|
||||
.unlink = vfs_install_unlink,
|
||||
.rmdir = vfs_install_rmdir,
|
||||
.rename = vfs_install_rename,
|
||||
};
|
||||
|
||||
void loop(void* arg) {
|
||||
while (!g_should_exit) {
|
||||
ftpsrv_init(&g_ftpsrv_config);
|
||||
@@ -44,8 +289,6 @@ void loop(void* arg) {
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace sphaira::ftpsrv {
|
||||
|
||||
bool Init() {
|
||||
std::scoped_lock lock{g_mutex};
|
||||
if (g_is_running) {
|
||||
@@ -84,6 +327,9 @@ bool Init() {
|
||||
mount_bis = ini_getbool("Nx-App", "mount_bis", mount_bis, INI_PATH);
|
||||
save_writable = ini_getbool("Nx-App", "save_writable", save_writable, INI_PATH);
|
||||
|
||||
mount_devices = true;
|
||||
g_ftpsrv_config.timeout = 0;
|
||||
|
||||
if (!g_ftpsrv_config.port) {
|
||||
return false;
|
||||
}
|
||||
@@ -93,7 +339,13 @@ bool Init() {
|
||||
g_ftpsrv_config.anon = true;
|
||||
}
|
||||
|
||||
vfs_nx_init(mount_devices, save_writable, mount_bis);
|
||||
const VfsNxCustomPath custom = {
|
||||
.name = "install",
|
||||
.user = NULL,
|
||||
.func = &g_vfs_install,
|
||||
};
|
||||
|
||||
vfs_nx_init(&custom, mount_devices, save_writable, mount_bis);
|
||||
|
||||
Result rc;
|
||||
if (R_FAILED(rc = threadCreate(&g_thread, loop, nullptr, nullptr, 1024*16, 0x2C, 2))) {
|
||||
@@ -123,6 +375,40 @@ void Exit() {
|
||||
fsdev_wrapUnmountAll();
|
||||
}
|
||||
|
||||
void InitInstallMode(void* user, OnInstallStart on_start, OnInstallWrite on_write, OnInstallClose on_close) {
|
||||
std::scoped_lock lock{g_shared_data.mutex};
|
||||
g_shared_data.user = user;
|
||||
g_shared_data.on_start = on_start;
|
||||
g_shared_data.on_write = on_write;
|
||||
g_shared_data.on_close = on_close;
|
||||
g_shared_data.enabled = true;
|
||||
}
|
||||
|
||||
void DisableInstallMode() {
|
||||
std::scoped_lock lock{g_shared_data.mutex};
|
||||
g_shared_data.enabled = false;
|
||||
}
|
||||
|
||||
unsigned GetPort() {
|
||||
std::scoped_lock lock{g_mutex};
|
||||
return g_ftpsrv_config.port;
|
||||
}
|
||||
|
||||
bool IsAnon() {
|
||||
std::scoped_lock lock{g_mutex};
|
||||
return g_ftpsrv_config.anon;
|
||||
}
|
||||
|
||||
const char* GetUser() {
|
||||
std::scoped_lock lock{g_mutex};
|
||||
return g_ftpsrv_config.user;
|
||||
}
|
||||
|
||||
const char* GetPass() {
|
||||
std::scoped_lock lock{g_mutex};
|
||||
return g_ftpsrv_config.pass;
|
||||
}
|
||||
|
||||
} // namespace sphaira::ftpsrv
|
||||
|
||||
extern "C" {
|
||||
@@ -132,9 +418,9 @@ void log_file_write(const char* msg) {
|
||||
}
|
||||
|
||||
void log_file_fwrite(const char* fmt, ...) {
|
||||
std::va_list v{};
|
||||
va_list v{};
|
||||
va_start(v, fmt);
|
||||
log_write_arg(fmt, v);
|
||||
log_write_arg(fmt, &v);
|
||||
va_end(v);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,18 +14,20 @@ std::FILE* file{};
|
||||
int nxlink_socket{};
|
||||
std::mutex mutex{};
|
||||
|
||||
void log_write_arg_internal(const char* s, std::va_list& v) {
|
||||
void log_write_arg_internal(const char* s, std::va_list* v) {
|
||||
if (file) {
|
||||
std::vfprintf(file, s, v);
|
||||
std::vfprintf(file, s, *v);
|
||||
std::fflush(file);
|
||||
}
|
||||
if (nxlink_socket) {
|
||||
std::vprintf(s, v);
|
||||
std::vprintf(s, *v);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
|
||||
auto log_file_init() -> bool {
|
||||
std::scoped_lock lock{mutex};
|
||||
if (file) {
|
||||
@@ -70,11 +72,11 @@ void log_write(const char* s, ...) {
|
||||
|
||||
std::va_list v{};
|
||||
va_start(v, s);
|
||||
log_write_arg_internal(s, v);
|
||||
log_write_arg_internal(s, &v);
|
||||
va_end(v);
|
||||
}
|
||||
|
||||
void log_write_arg(const char* s, std::va_list& v) {
|
||||
void log_write_arg(const char* s, va_list* v) {
|
||||
std::scoped_lock lock{mutex};
|
||||
if (!file && !nxlink_socket) {
|
||||
return;
|
||||
@@ -83,4 +85,6 @@ void log_write_arg(const char* s, std::va_list& v) {
|
||||
log_write_arg_internal(s, v);
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
#endif
|
||||
|
||||
@@ -66,6 +66,9 @@ void userAppInit(void) {
|
||||
if (R_FAILED(rc = ncmInitialize()))
|
||||
diagAbortWithResult(rc);
|
||||
|
||||
// it doesn't matter if this fails.
|
||||
appletSetScreenShotPermission(AppletScreenShotPermission_Enable);
|
||||
|
||||
log_nxlink_init();
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,14 @@
|
||||
#include <string_view>
|
||||
#include <span>
|
||||
|
||||
#include "yati/nx/nca.hpp"
|
||||
#include "yati/nx/ncm.hpp"
|
||||
#include "yati/nx/npdm.hpp"
|
||||
#include "yati/nx/ns.hpp"
|
||||
#include "yati/nx/es.hpp"
|
||||
#include "yati/nx/keys.hpp"
|
||||
#include "yati/nx/crypto.hpp"
|
||||
|
||||
#include "owo.hpp"
|
||||
#include "defines.hpp"
|
||||
#include "app.hpp"
|
||||
@@ -23,52 +31,9 @@ constexpr u32 PFS0_EXEFS_HASH_BLOCK_SIZE = 0x10000;
|
||||
constexpr u32 PFS0_LOGO_HASH_BLOCK_SIZE = 0x1000;
|
||||
constexpr u32 PFS0_META_HASH_BLOCK_SIZE = 0x1000;
|
||||
constexpr u32 PFS0_PADDING_SIZE = 0x200;
|
||||
constexpr u32 NCA_SECTION_TOTAL = 0x4;
|
||||
constexpr u32 ROMFS_ENTRY_EMPTY = 0xFFFFFFFF;
|
||||
constexpr u32 ROMFS_FILEPARTITION_OFS = 0x200;
|
||||
|
||||
enum NcaDistributionType {
|
||||
NcaDistributionType_System = 0x0,
|
||||
NcaDistributionType_GameCard = 0x1
|
||||
};
|
||||
|
||||
enum NcaContentType {
|
||||
NcaContentType_Program = 0x0,
|
||||
NcaContentType_Meta = 0x1,
|
||||
NcaContentType_Control = 0x2,
|
||||
NcaContentType_Manual = 0x3,
|
||||
NcaContentType_Data = 0x4,
|
||||
NcaContentType_PublicData = 0x5,
|
||||
};
|
||||
|
||||
enum NcaFileSystemType {
|
||||
NcaFileSystemType_RomFS = 0x0,
|
||||
NcaFileSystemType_PFS0 = 0x1
|
||||
};
|
||||
|
||||
enum NcaHashType {
|
||||
NcaHashType_Auto = 0x0,
|
||||
NcaHashType_HierarchicalSha256 = 0x2,
|
||||
NcaHashType_HierarchicalIntegrity = 0x3
|
||||
};
|
||||
|
||||
enum NcaEncryptionType {
|
||||
NcaEncryptionType_Auto = 0x0,
|
||||
NcaEncryptionType_None = 0x1,
|
||||
NcaEncryptionType_AesCtrOld = 0x2,
|
||||
NcaEncryptionType_AesCtr = 0x3,
|
||||
NcaEncryptionType_AesCtrEx = 0x4
|
||||
};
|
||||
|
||||
enum NsApplicationRecordType {
|
||||
// installed
|
||||
NsApplicationRecordType_Installed = 0x3,
|
||||
// application is gamecard, but gamecard isn't insterted
|
||||
NsApplicationRecordType_GamecardMissing = 0x5,
|
||||
// archived
|
||||
NsApplicationRecordType_Archived = 0xB,
|
||||
};
|
||||
|
||||
// stdio-like wrapper for std::vector
|
||||
struct BufHelper {
|
||||
BufHelper() = default;
|
||||
@@ -124,12 +89,6 @@ struct CnmtHeader {
|
||||
};
|
||||
static_assert(sizeof(CnmtHeader) == 0x20);
|
||||
|
||||
struct NcmContentStorageRecord {
|
||||
NcmContentMetaKey key;
|
||||
u8 storage_id; //
|
||||
u8 padding[0x7];
|
||||
};
|
||||
|
||||
struct NcmContentMetaData {
|
||||
NcmContentMetaHeader header;
|
||||
NcmApplicationMetaExtendedHeader extended;
|
||||
@@ -142,7 +101,7 @@ struct NcaMetaEntry {
|
||||
NcaEntry nca_entry;
|
||||
NcmContentMetaHeader content_meta_header{};
|
||||
NcmContentMetaKey content_meta_key{};
|
||||
NcmContentStorageRecord content_storage_record{};
|
||||
ncm::ContentStorageRecord content_storage_record{};
|
||||
NcmContentMetaData content_meta_data{};
|
||||
};
|
||||
|
||||
@@ -171,61 +130,6 @@ struct FileEntry {
|
||||
|
||||
using FileEntries = std::vector<FileEntry>;
|
||||
|
||||
struct NpdmMeta {
|
||||
u32 magic; // "META"
|
||||
u32 signature_key_generation; // +9.0.0
|
||||
u32 _0x8;
|
||||
u8 flags;
|
||||
u8 _0xD;
|
||||
u8 main_thread_priority;
|
||||
u8 main_thread_core_num;
|
||||
u32 _0x10;
|
||||
u32 sys_resource_size; // +3.0.0
|
||||
u32 version;
|
||||
u32 main_thread_stack_size;
|
||||
char title_name[0x10];
|
||||
char product_code[0x10];
|
||||
u8 _0x40[0x30];
|
||||
u32 aci0_offset;
|
||||
u32 aci0_size;
|
||||
u32 acid_offset;
|
||||
u32 acid_size;
|
||||
};
|
||||
|
||||
struct NpdmAcid {
|
||||
u8 rsa_sig[0x100];
|
||||
u8 rsa_pub[0x100];
|
||||
u32 magic; // "ACID"
|
||||
u32 size;
|
||||
u8 version;
|
||||
u8 _0x209[0x1];
|
||||
u8 _0x20A[0x2];
|
||||
u32 flags;
|
||||
u64 program_id_min;
|
||||
u64 program_id_max;
|
||||
u32 fac_offset;
|
||||
u32 fac_size;
|
||||
u32 sac_offset;
|
||||
u32 sac_size;
|
||||
u32 kac_offset;
|
||||
u32 kac_size;
|
||||
u8 _0x238[0x8];
|
||||
};
|
||||
|
||||
struct NpdmAci0 {
|
||||
u32 magic; // "ACI0"
|
||||
u8 _0x4[0xC];
|
||||
u64 program_id;
|
||||
u8 _0x18[0x8];
|
||||
u32 fac_offset;
|
||||
u32 fac_size;
|
||||
u32 sac_offset;
|
||||
u32 sac_size;
|
||||
u32 kac_offset;
|
||||
u32 kac_size;
|
||||
u8 _0x38[0x8];
|
||||
};
|
||||
|
||||
struct NpdmPatch {
|
||||
char title_name[0x10]{"Application"};
|
||||
char product_code[0x10]{};
|
||||
@@ -238,56 +142,6 @@ struct NcapPatch {
|
||||
u64 tid;
|
||||
};
|
||||
|
||||
struct NcaSectionTableEntry {
|
||||
u32 media_start_offset; // divided by 0x200.
|
||||
u32 media_end_offset; // divided by 0x200.
|
||||
u8 _0x8[0x4]; // unknown.
|
||||
u8 _0xC[0x4]; // unknown.
|
||||
};
|
||||
|
||||
struct LayerRegion {
|
||||
u64 offset;
|
||||
u64 size;
|
||||
};
|
||||
|
||||
struct HierarchicalSha256Data {
|
||||
u8 master_hash[0x20];
|
||||
u32 block_size;
|
||||
u32 layer_count;
|
||||
LayerRegion hash_layer;
|
||||
LayerRegion pfs0_layer;
|
||||
LayerRegion unused_layers[3];
|
||||
u8 _0x78[0x80];
|
||||
};
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct HierarchicalIntegrityVerificationLevelInformation {
|
||||
u64 logical_offset;
|
||||
u64 hash_data_size;
|
||||
u32 block_size; // log2
|
||||
u32 _0x14; // reserved
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
struct InfoLevelHash {
|
||||
u32 max_layers;
|
||||
HierarchicalIntegrityVerificationLevelInformation levels[6];
|
||||
u8 signature_salt[0x20];
|
||||
};
|
||||
|
||||
struct IntegrityMetaInfo {
|
||||
u32 magic; // IVFC
|
||||
u32 version;
|
||||
u32 master_hash_size;
|
||||
InfoLevelHash info_level_hash;
|
||||
u8 master_hash[0x20];
|
||||
u8 _0xE0[0x18];
|
||||
};
|
||||
|
||||
static_assert(sizeof(HierarchicalSha256Data) == 0xF8);
|
||||
static_assert(sizeof(IntegrityMetaInfo) == 0xF8);
|
||||
static_assert(sizeof(HierarchicalSha256Data) == sizeof(IntegrityMetaInfo));
|
||||
|
||||
typedef struct romfs_dirent_ctx {
|
||||
u32 entry_offset;
|
||||
struct romfs_dirent_ctx *parent; /* Parent node */
|
||||
@@ -317,70 +171,6 @@ typedef struct {
|
||||
u64 file_partition_size;
|
||||
} romfs_ctx_t;
|
||||
|
||||
struct NcaFsHeader {
|
||||
u16 version; // always 2.
|
||||
u8 fs_type; // see NcaFileSystemType.
|
||||
u8 hash_type; // see NcaHashType.
|
||||
u8 encryption_type; // see NcaEncryptionType.
|
||||
u8 metadata_hash_type;
|
||||
u8 _0x6[0x2]; // empty.
|
||||
|
||||
union {
|
||||
HierarchicalSha256Data hierarchical_sha256_data;
|
||||
IntegrityMetaInfo integrity_meta_info; // used for romfs
|
||||
} hash_data;
|
||||
|
||||
u8 patch_info[0x40];
|
||||
u64 section_ctr;
|
||||
u8 spares_info[0x30];
|
||||
u8 compression_info[0x28];
|
||||
u8 meta_data_hash_data_info[0x30];
|
||||
u8 reserved[0x30];
|
||||
};
|
||||
|
||||
struct NcaSectionHeaderHash {
|
||||
u8 sha256[0x20];
|
||||
};
|
||||
|
||||
struct NcaKeyArea {
|
||||
u8 area[0x10];
|
||||
};
|
||||
|
||||
struct NcaHeader {
|
||||
u8 rsa_fixed_key[0x100];
|
||||
u8 rsa_npdm[0x100]; // key from npdm.
|
||||
u32 magic;
|
||||
u8 distribution_type; // see NcaDistributionType.
|
||||
u8 content_type; // see NcaContentType.
|
||||
u8 old_key_gen; // see NcaOldKeyGeneration.
|
||||
u8 kaek_index; // see NcaKeyAreaEncryptionKeyIndex.
|
||||
u64 size;
|
||||
u64 title_id;
|
||||
u32 context_id;
|
||||
u32 sdk_version;
|
||||
u8 key_gen; // see NcaKeyGeneration.
|
||||
u8 header_1_sig_key_gen;
|
||||
u8 _0x222[0xE]; // empty.
|
||||
FsRightsId rights_id;
|
||||
|
||||
NcaSectionTableEntry fs_table[NCA_SECTION_TOTAL];
|
||||
NcaSectionHeaderHash fs_header_hash[NCA_SECTION_TOTAL];
|
||||
NcaKeyArea key_area[NCA_SECTION_TOTAL];
|
||||
|
||||
u8 _0x340[0xC0]; // empty.
|
||||
|
||||
NcaFsHeader fs_header[NCA_SECTION_TOTAL];
|
||||
};
|
||||
|
||||
constexpr u8 HEADER_KEK_SRC[0x10] = {
|
||||
0x1F, 0x12, 0x91, 0x3A, 0x4A, 0xCB, 0xF0, 0x0D, 0x4C, 0xDE, 0x3A, 0xF6, 0xD5, 0x23, 0x88, 0x2A
|
||||
};
|
||||
|
||||
constexpr u8 HEADER_KEY_SRC[0x20] = {
|
||||
0x5A, 0x3E, 0xD8, 0x4F, 0xDE, 0xC0, 0xD8, 0x26, 0x31, 0xF7, 0xE2, 0x5D, 0x19, 0x7B, 0xF5, 0xD0,
|
||||
0x1C, 0x9B, 0x7B, 0xFA, 0xF6, 0x28, 0x18, 0x3D, 0x71, 0xF6, 0x4D, 0x73, 0xF1, 0x50, 0xB9, 0xD2
|
||||
};
|
||||
|
||||
auto write_padding(BufHelper& buf, u64 off, u64 block) -> u64 {
|
||||
const u64 size = block - (off % block);
|
||||
if (size) {
|
||||
@@ -632,9 +422,9 @@ auto npdm_patch_kc(std::vector<u8>& npdm, u32 off, u32 size, u32 bitmask, u32 va
|
||||
|
||||
// todo: manually build npdm
|
||||
void patch_npdm(std::vector<u8>& npdm, const NpdmPatch& patch) {
|
||||
NpdmMeta meta{};
|
||||
NpdmAci0 aci0{};
|
||||
NpdmAcid acid{};
|
||||
npdm::Meta meta{};
|
||||
npdm::Aci0 aci0{};
|
||||
npdm::Acid acid{};
|
||||
std::memcpy(&meta, npdm.data(), sizeof(meta));
|
||||
std::memcpy(&aci0, npdm.data() + meta.aci0_offset, sizeof(aci0));
|
||||
std::memcpy(&acid, npdm.data() + meta.acid_offset, sizeof(acid));
|
||||
@@ -805,7 +595,7 @@ void write_nca_padding(BufHelper& buf) {
|
||||
write_padding(buf, buf.tell(), 0x200);
|
||||
}
|
||||
|
||||
void nca_encrypt_header(NcaHeader* header, std::span<const u8> key) {
|
||||
void nca_encrypt_header(nca::Header* header, std::span<const u8> key) {
|
||||
Aes128XtsContext ctx{};
|
||||
aes128XtsContextCreate(&ctx, key.data(), key.data() + 0x10, true);
|
||||
|
||||
@@ -816,41 +606,41 @@ void nca_encrypt_header(NcaHeader* header, std::span<const u8> key) {
|
||||
}
|
||||
}
|
||||
|
||||
void write_nca_section(NcaHeader& nca_header, u8 index, u64 start, u64 end) {
|
||||
void write_nca_section(nca::Header& nca_header, u8 index, u64 start, u64 end) {
|
||||
auto& section = nca_header.fs_table[index];
|
||||
section.media_start_offset = start / 0x200; // 0xC00 / 0x200
|
||||
section.media_end_offset = end / 0x200; // Section end offset / 200
|
||||
section._0x8[0] = 0x1; // Always 1
|
||||
}
|
||||
|
||||
void write_nca_fs_header_pfs0(NcaHeader& nca_header, u8 index, const std::vector<u8>& master_hash, u64 hash_table_size, u32 block_size) {
|
||||
void write_nca_fs_header_pfs0(nca::Header& nca_header, u8 index, const std::vector<u8>& master_hash, u64 hash_table_size, u32 block_size) {
|
||||
auto& fs_header = nca_header.fs_header[index];
|
||||
fs_header.hash_type = NcaHashType_HierarchicalSha256;
|
||||
fs_header.fs_type = NcaFileSystemType_PFS0;
|
||||
fs_header.hash_type = nca::HashType_HierarchicalSha256;
|
||||
fs_header.fs_type = nca::FileSystemType_PFS0;
|
||||
fs_header.version = 0x2; // Always 2
|
||||
fs_header.hash_data.hierarchical_sha256_data.layer_count = 0x2;
|
||||
fs_header.hash_data.hierarchical_sha256_data.block_size = block_size;
|
||||
fs_header.encryption_type = NcaEncryptionType_None;
|
||||
fs_header.encryption_type = nca::EncryptionType_None;
|
||||
fs_header.hash_data.hierarchical_sha256_data.hash_layer.size = hash_table_size;
|
||||
std::memcpy(fs_header.hash_data.hierarchical_sha256_data.master_hash, master_hash.data(), master_hash.size());
|
||||
sha256CalculateHash(&nca_header.fs_header_hash[index], &fs_header, sizeof(fs_header));
|
||||
}
|
||||
|
||||
void write_nca_fs_header_romfs(NcaHeader& nca_header, u8 index) {
|
||||
void write_nca_fs_header_romfs(nca::Header& nca_header, u8 index) {
|
||||
auto& fs_header = nca_header.fs_header[index];
|
||||
fs_header.hash_type = NcaHashType_HierarchicalIntegrity;
|
||||
fs_header.fs_type = NcaFileSystemType_RomFS;
|
||||
fs_header.hash_type = nca::HashType_HierarchicalIntegrity;
|
||||
fs_header.fs_type = nca::FileSystemType_RomFS;
|
||||
fs_header.version = 0x2; // Always 2
|
||||
fs_header.hash_data.integrity_meta_info.magic = 0x43465649;
|
||||
fs_header.hash_data.integrity_meta_info.version = 0x20000; // Always 0x20000
|
||||
fs_header.hash_data.integrity_meta_info.master_hash_size = SHA256_HASH_SIZE;
|
||||
fs_header.hash_data.integrity_meta_info.info_level_hash.max_layers = 0x7;
|
||||
fs_header.encryption_type = NcaEncryptionType_None;
|
||||
fs_header.encryption_type = nca::EncryptionType_None;
|
||||
fs_header.hash_data.integrity_meta_info.info_level_hash.levels[5].block_size = 0x0E; // 0x4000
|
||||
sha256CalculateHash(&nca_header.fs_header_hash[index], &fs_header, sizeof(fs_header));
|
||||
}
|
||||
|
||||
void write_nca_pfs0(NcaHeader& nca_header, u8 index, const FileEntries& entries, u32 block_size, BufHelper& buf) {
|
||||
void write_nca_pfs0(nca::Header& nca_header, u8 index, const FileEntries& entries, u32 block_size, BufHelper& buf) {
|
||||
const auto pfs0 = build_pfs0(entries);
|
||||
const auto pfs0_hash_table = build_pfs0_hash_table(pfs0, block_size);
|
||||
const auto pfs0_master_hash = build_pfs0_master_hash(pfs0_hash_table);
|
||||
@@ -887,7 +677,7 @@ auto ivfc_create_level(const std::vector<u8>& src) -> std::vector<u8> {
|
||||
return buf.buf;
|
||||
}
|
||||
|
||||
void write_nca_romfs(NcaHeader& nca_header, u8 index, const FileEntries& entries, u32 block_size, BufHelper& buf) {
|
||||
void write_nca_romfs(nca::Header& nca_header, u8 index, const FileEntries& entries, u32 block_size, BufHelper& buf) {
|
||||
auto& fs_header = nca_header.fs_header[index];
|
||||
auto& meta_info = fs_header.hash_data.integrity_meta_info;
|
||||
auto& info_level_hash = meta_info.info_level_hash;
|
||||
@@ -921,22 +711,22 @@ void write_nca_romfs(NcaHeader& nca_header, u8 index, const FileEntries& entries
|
||||
write_nca_fs_header_romfs(nca_header, index);
|
||||
}
|
||||
|
||||
void write_nca_header_encypted(NcaHeader& nca_header, u64 tid, std::span<const u8> key, NcaContentType type, BufHelper& buf) {
|
||||
nca_header.magic = 0x3341434E;
|
||||
nca_header.distribution_type = NcaDistributionType_System;
|
||||
void write_nca_header_encypted(nca::Header& nca_header, u64 tid, const keys::Keys& keys, nca::ContentType type, BufHelper& buf) {
|
||||
nca_header.magic = NCA3_MAGIC;
|
||||
nca_header.distribution_type = nca::DistributionType_System;
|
||||
nca_header.content_type = type;
|
||||
nca_header.title_id = tid;
|
||||
nca_header.sdk_version = 0x000C1100;
|
||||
nca_header.size = buf.tell();
|
||||
|
||||
nca_encrypt_header(&nca_header, key);
|
||||
nca_encrypt_header(&nca_header, keys.header_key);
|
||||
buf.seek(0);
|
||||
buf.write(&nca_header, sizeof(nca_header));
|
||||
}
|
||||
|
||||
auto create_program_nca(u64 tid, std::span<const u8> key, const FileEntries& exefs, const FileEntries& romfs, const FileEntries& logo) -> NcaEntry {
|
||||
auto create_program_nca(u64 tid, const keys::Keys& keys, const FileEntries& exefs, const FileEntries& romfs, const FileEntries& logo) -> NcaEntry {
|
||||
BufHelper buf;
|
||||
NcaHeader nca_header{};
|
||||
nca::Header nca_header{};
|
||||
buf.write(&nca_header, sizeof(nca_header));
|
||||
|
||||
write_nca_pfs0(nca_header, 0, exefs, PFS0_EXEFS_HASH_BLOCK_SIZE, buf);
|
||||
@@ -945,23 +735,23 @@ auto create_program_nca(u64 tid, std::span<const u8> key, const FileEntries& exe
|
||||
if (logo.size() == 2 && !logo[0].data.empty() && !logo[1].data.empty()) {
|
||||
write_nca_pfs0(nca_header, 2, logo, PFS0_LOGO_HASH_BLOCK_SIZE, buf);
|
||||
}
|
||||
write_nca_header_encypted(nca_header, tid, key, NcaContentType_Program, buf);
|
||||
write_nca_header_encypted(nca_header, tid, keys, nca::ContentType_Program, buf);
|
||||
|
||||
return {buf, NcmContentType_Program};
|
||||
}
|
||||
|
||||
auto create_control_nca(u64 tid, std::span<const u8> key, const FileEntries& romfs) -> NcaEntry{
|
||||
NcaHeader nca_header{};
|
||||
auto create_control_nca(u64 tid, const keys::Keys& keys, const FileEntries& romfs) -> NcaEntry{
|
||||
nca::Header nca_header{};
|
||||
BufHelper buf;
|
||||
buf.write(&nca_header, sizeof(nca_header));
|
||||
|
||||
write_nca_romfs(nca_header, 0, romfs, IVFC_HASH_BLOCK_SIZE, buf);
|
||||
write_nca_header_encypted(nca_header, tid, key, NcaContentType_Control, buf);
|
||||
write_nca_header_encypted(nca_header, tid, keys, nca::ContentType_Control, buf);
|
||||
|
||||
return {buf, NcmContentType_Control};
|
||||
}
|
||||
|
||||
auto create_meta_nca(u64 tid, std::span<const u8> key, NcmStorageId storage_id, const std::vector<NcaEntry>& ncas) -> NcaMetaEntry {
|
||||
auto create_meta_nca(u64 tid, const keys::Keys& keys, NcmStorageId storage_id, const std::vector<NcaEntry>& ncas) -> NcaMetaEntry {
|
||||
CnmtHeader cnmt_header{};
|
||||
NcmApplicationMetaExtendedHeader cnmt_extended{};
|
||||
NcmPackagedContentInfo packaged_content_info[2]{};
|
||||
@@ -997,10 +787,10 @@ auto create_meta_nca(u64 tid, std::span<const u8> key, NcmStorageId storage_id,
|
||||
std::snprintf(cnmt_name, sizeof(cnmt_name), "Application_%016lX.cnmt", tid);
|
||||
add_file_entry(cnmt, cnmt_name, cnmt_buf.buf.data(), cnmt_buf.buf.size());
|
||||
|
||||
NcaHeader nca_header{};
|
||||
nca::Header nca_header{};
|
||||
buf.write(&nca_header, sizeof(nca_header));
|
||||
write_nca_pfs0(nca_header, 0, cnmt, PFS0_META_HASH_BLOCK_SIZE, buf);
|
||||
write_nca_header_encypted(nca_header, tid, key, NcaContentType_Meta, buf);
|
||||
write_nca_header_encypted(nca_header, tid, keys, nca::ContentType_Meta, buf);
|
||||
|
||||
// entry
|
||||
NcaMetaEntry entry{buf, NcmContentType_Meta};
|
||||
@@ -1040,26 +830,6 @@ auto create_meta_nca(u64 tid, std::span<const u8> key, NcmStorageId storage_id,
|
||||
return entry;
|
||||
}
|
||||
|
||||
Result nsDeleteApplicationRecord(Service* srv, u64 tid) {
|
||||
return serviceDispatchIn(srv, 27, tid);
|
||||
}
|
||||
|
||||
Result nsPushApplicationRecord(Service* srv, u64 tid, const NcmContentStorageRecord* records, u32 count) {
|
||||
const struct {
|
||||
u8 last_modified_event;
|
||||
u8 padding[0x7];
|
||||
u64 tid;
|
||||
} in = { NsApplicationRecordType_Installed, {0}, tid };
|
||||
|
||||
return serviceDispatchIn(srv, 16, in,
|
||||
.buffer_attrs = { SfBufferAttr_HipcMapAlias | SfBufferAttr_In },
|
||||
.buffers = { { records, sizeof(NcmContentStorageRecord) * count } });
|
||||
}
|
||||
|
||||
Result nsInvalidateApplicationControlCache(Service* srv, u64 tid) {
|
||||
return serviceDispatchIn(srv, 404, tid);
|
||||
}
|
||||
|
||||
auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStorageId storage_id) -> Result {
|
||||
R_UNLESS(!config.nro_path.empty(), OwoError_BadArgs);
|
||||
// R_UNLESS(!config.icon.empty(), OwoError_BadArgs);
|
||||
@@ -1073,14 +843,8 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
|
||||
R_TRY(nsInitialize());
|
||||
ON_SCOPE_EXIT(nsExit());
|
||||
|
||||
// generate header kek
|
||||
u8 header_kek[0x20];
|
||||
R_TRY(splCryptoGenerateAesKek(HEADER_KEK_SRC, 0, 0, header_kek));
|
||||
// gen header key 0
|
||||
u8 key[0x20];
|
||||
R_TRY(splCryptoGenerateAesKey(header_kek, HEADER_KEY_SRC, key));
|
||||
// gen header key 1
|
||||
R_TRY(splCryptoGenerateAesKey(header_kek, HEADER_KEY_SRC + 0x10, key + 0x10));
|
||||
keys::Keys keys;
|
||||
R_TRY(keys::parse_keys(keys, false));
|
||||
|
||||
// fix args to include nro path
|
||||
if (config.args.empty()) {
|
||||
@@ -1124,7 +888,7 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
|
||||
patch_npdm(exefs[1].data, npdm_patch);
|
||||
|
||||
nca_entries.emplace_back(
|
||||
create_program_nca(tid, key, exefs, romfs, logo)
|
||||
create_program_nca(tid, keys, exefs, romfs, logo)
|
||||
);
|
||||
} else {
|
||||
nca_entries.emplace_back(
|
||||
@@ -1147,18 +911,18 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
|
||||
add_file_entry(romfs, "/icon_AmericanEnglish.dat", config.icon);
|
||||
|
||||
nca_entries.emplace_back(
|
||||
create_control_nca(tid, key, romfs)
|
||||
create_control_nca(tid, keys, romfs)
|
||||
);
|
||||
}
|
||||
|
||||
// create meta
|
||||
NcmContentMetaHeader content_meta_header;
|
||||
NcmContentMetaKey content_meta_key;
|
||||
NcmContentStorageRecord content_storage_record;
|
||||
ncm::ContentStorageRecord content_storage_record;
|
||||
NcmContentMetaData content_meta_data;
|
||||
{
|
||||
pbox->NewTransfer("Creating Meta"_i18n).UpdateTransfer(2, 8);
|
||||
const auto meta_entry = create_meta_nca(tid, key, storage_id, nca_entries);
|
||||
const auto meta_entry = create_meta_nca(tid, keys, storage_id, nca_entries);
|
||||
|
||||
nca_entries.emplace_back(meta_entry.nca_entry);
|
||||
content_meta_header = meta_entry.content_meta_header;
|
||||
@@ -1218,15 +982,15 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
|
||||
|
||||
// remove previous application record
|
||||
if (already_installed || hosversionBefore(2,0,0)) {
|
||||
const auto rc = nsDeleteApplicationRecord(srv_ptr, tid);
|
||||
const auto rc = ns::DeleteApplicationRecord(srv_ptr, tid);
|
||||
R_UNLESS(R_SUCCEEDED(rc) || hosversionBefore(2,0,0), rc);
|
||||
}
|
||||
|
||||
R_TRY(nsPushApplicationRecord(srv_ptr, tid, &content_storage_record, 1));
|
||||
R_TRY(ns::PushApplicationRecord(srv_ptr, tid, &content_storage_record, 1));
|
||||
|
||||
// force flush
|
||||
if (already_installed || hosversionBefore(2,0,0)) {
|
||||
const auto rc = nsInvalidateApplicationControlCache(srv_ptr, tid);
|
||||
const auto rc = ns::InvalidateApplicationControlCache(srv_ptr, tid);
|
||||
R_UNLESS(R_SUCCEEDED(rc) || hosversionBefore(2,0,0), rc);
|
||||
}
|
||||
}
|
||||
@@ -1241,7 +1005,7 @@ auto install_forwarder(ui::ProgressBox* pbox, OwoConfig& config, NcmStorageId st
|
||||
}
|
||||
|
||||
auto install_forwarder(OwoConfig& config, NcmStorageId storage_id) -> Result {
|
||||
App::Push(std::make_shared<ui::ProgressBox>("Installing Forwarder"_i18n, [config, storage_id](auto pbox) mutable -> bool {
|
||||
App::Push(std::make_shared<ui::ProgressBox>(0, "Installing Forwarder"_i18n, config.name, [config, storage_id](auto pbox) mutable -> bool {
|
||||
return R_SUCCEEDED(install_forwarder(pbox, config, storage_id));
|
||||
}));
|
||||
R_SUCCEED();
|
||||
|
||||
@@ -42,14 +42,15 @@ Result ShowInternal(Config& cfg, const char* guide, const char* initial, s64 len
|
||||
} // namespace
|
||||
|
||||
Result ShowText(std::string& out, const char* guide, const char* initial, s64 len_min, s64 len_max) {
|
||||
Config cfg;
|
||||
Config cfg{};
|
||||
R_TRY(ShowInternal(cfg, guide, initial, len_min, len_max));
|
||||
out = cfg.out_text;
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result ShowNumPad(s64& out, const char* guide, const char* initial, s64 len_min, s64 len_max) {
|
||||
Config cfg;
|
||||
Config cfg{};
|
||||
cfg.numpad = true;
|
||||
R_TRY(ShowInternal(cfg, guide, initial, len_min, len_max));
|
||||
out = std::atoll(cfg.out_text);
|
||||
R_SUCCEED();
|
||||
|
||||
@@ -34,8 +34,35 @@ auto List::ClampY(float y, s64 count) const -> float {
|
||||
return y;
|
||||
}
|
||||
|
||||
void List::OnUpdate(Controller* controller, TouchInfo* touch, s64 count, TouchCallback callback) {
|
||||
if (touch->is_clicked && touch->in_range(GetPos())) {
|
||||
void List::OnUpdate(Controller* controller, TouchInfo* touch, s64 index, s64 count, TouchCallback callback) {
|
||||
const auto page_up_button = m_row == 1 ? Button::DPAD_LEFT : Button::L2;
|
||||
const auto page_down_button = m_row == 1 ? Button::DPAD_RIGHT : Button::R2;
|
||||
|
||||
if (controller->GotDown(Button::DOWN)) {
|
||||
if (ScrollDown(index, m_row, count)) {
|
||||
callback(false, index);
|
||||
}
|
||||
} else if (controller->GotDown(Button::UP)) {
|
||||
if (ScrollUp(index, m_row, count)) {
|
||||
callback(false, index);
|
||||
}
|
||||
} else if (controller->GotDown(page_down_button)) {
|
||||
if (ScrollDown(index, m_page, count)) {
|
||||
callback(false, index);
|
||||
}
|
||||
} else if (controller->GotDown(page_up_button)) {
|
||||
if (ScrollUp(index, m_page, count)) {
|
||||
callback(false, index);
|
||||
}
|
||||
} else if (m_row > 1 && controller->GotDown(Button::RIGHT)) {
|
||||
if (count && index < (count - 1) && (index + 1) % m_row != 0) {
|
||||
callback(false, index + 1);
|
||||
}
|
||||
} else if (m_row > 1 && controller->GotDown(Button::LEFT)) {
|
||||
if (count && index != 0 && (index % m_row) != 0) {
|
||||
callback(false, index - 1);
|
||||
}
|
||||
} else if (touch->is_clicked && touch->in_range(GetPos())) {
|
||||
auto v = m_v;
|
||||
v.y -= ClampY(m_yoff + m_y_prog, count);
|
||||
|
||||
@@ -63,7 +90,7 @@ void List::OnUpdate(Controller* controller, TouchInfo* touch, s64 count, TouchCa
|
||||
vv.h = std::min(v.y + v.h, m_pos.y + m_pos.h) - v.y;
|
||||
|
||||
if (touch->in_range(vv)) {
|
||||
callback(i);
|
||||
callback(true, i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,18 +236,15 @@ void DrawIcon(NVGcontext* vg, const LazyImage& l, const LazyImage& d, float x, f
|
||||
bool crop = false;
|
||||
if (iw < w || ih < h) {
|
||||
rounded_image = false;
|
||||
gfx::drawRect(vg, x, y, w, h, nvgRGB(i.first_pixel[0], i.first_pixel[1], i.first_pixel[2]), rounded);
|
||||
gfx::drawRect(vg, x, y, w, h, nvgRGB(i.first_pixel[0], i.first_pixel[1], i.first_pixel[2]), rounded ? 15 : 0);
|
||||
}
|
||||
if (iw > w || ih > h) {
|
||||
crop = true;
|
||||
nvgSave(vg);
|
||||
nvgIntersectScissor(vg, x, y, w, h);
|
||||
}
|
||||
if (rounded_image) {
|
||||
gfx::drawImageRounded(vg, ix, iy, iw, ih, i.image);
|
||||
} else {
|
||||
gfx::drawImage(vg, ix, iy, iw, ih, i.image);
|
||||
}
|
||||
|
||||
gfx::drawImage(vg, ix, iy, iw, ih, i.image, rounded_image ? 15 : 0);
|
||||
if (crop) {
|
||||
nvgRestore(vg);
|
||||
}
|
||||
@@ -769,7 +766,7 @@ void EntryMenu::UpdateOptions() {
|
||||
};
|
||||
|
||||
const auto install = [this](){
|
||||
App::Push(std::make_shared<ProgressBox>("Installing "_i18n + m_entry.title, [this](auto pbox){
|
||||
App::Push(std::make_shared<ProgressBox>(m_entry.image.image, "Downloading "_i18n, m_entry.title, [this](auto pbox){
|
||||
return InstallApp(pbox, m_entry);
|
||||
}, [this](bool success){
|
||||
if (success) {
|
||||
@@ -782,7 +779,7 @@ void EntryMenu::UpdateOptions() {
|
||||
};
|
||||
|
||||
const auto uninstall = [this](){
|
||||
App::Push(std::make_shared<ProgressBox>("Uninstalling "_i18n + m_entry.title, [this](auto pbox){
|
||||
App::Push(std::make_shared<ProgressBox>(m_entry.image.image, "Uninstalling "_i18n, m_entry.title, [this](auto pbox){
|
||||
return UninstallApp(pbox, m_entry);
|
||||
}, [this](bool success){
|
||||
if (success) {
|
||||
@@ -854,48 +851,6 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"AppStore"_i18n}
|
||||
fs.CreateDirectoryRecursively("/switch/sphaira/cache/appstore/screens");
|
||||
|
||||
this->SetActions(
|
||||
std::make_pair(Button::RIGHT, Action{[this](){
|
||||
if (m_entries_current.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_index < (m_entries_current.size() - 1) && (m_index + 1) % 3 != 0) {
|
||||
SetIndex(m_index + 1);
|
||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
||||
log_write("moved right\n");
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::LEFT, Action{[this](){
|
||||
if (m_entries_current.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_index != 0 && (m_index % 3) != 0) {
|
||||
SetIndex(m_index - 1);
|
||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
||||
log_write("moved left\n");
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::DOWN, Action{[this](){
|
||||
if (m_list->ScrollDown(m_index, 3, m_entries_current.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::UP, Action{[this](){
|
||||
if (m_list->ScrollUp(m_index, 3, m_entries_current.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::R2, Action{[this](){
|
||||
if (m_list->ScrollDown(m_index, 9, m_entries_current.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::L2, Action{[this](){
|
||||
if (m_list->ScrollUp(m_index, 9, m_entries_current.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::A, Action{"Info"_i18n, [this](){
|
||||
if (m_entries_current.empty()) {
|
||||
// log_write("pushing A when empty: size: %zu count: %zu\n", repo_json.size(), m_entries_current.size());
|
||||
@@ -983,8 +938,8 @@ Menu::~Menu() {
|
||||
|
||||
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
MenuBase::Update(controller, touch);
|
||||
m_list->OnUpdate(controller, touch, m_entries_current.size(), [this](auto i) {
|
||||
if (m_index == i) {
|
||||
m_list->OnUpdate(controller, touch, m_index, m_entries_current.size(), [this](bool touch, auto i) {
|
||||
if (touch && m_index == i) {
|
||||
FireAction(Button::A);
|
||||
} else {
|
||||
App::PlaySoundEffect(SoundEffect_Focus);
|
||||
@@ -1096,16 +1051,16 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
float i_size = 22;
|
||||
switch (e.status) {
|
||||
case EntryStatus::Get:
|
||||
gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_get.image);
|
||||
gfx::drawImage(vg, x + w - 30.f, y + 110, i_size, i_size, m_get.image, 15);
|
||||
break;
|
||||
case EntryStatus::Installed:
|
||||
gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_installed.image);
|
||||
gfx::drawImage(vg, x + w - 30.f, y + 110, i_size, i_size, m_installed.image, 15);
|
||||
break;
|
||||
case EntryStatus::Local:
|
||||
gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_local.image);
|
||||
gfx::drawImage(vg, x + w - 30.f, y + 110, i_size, i_size, m_local.image, 15);
|
||||
break;
|
||||
case EntryStatus::Update:
|
||||
gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_update.image);
|
||||
gfx::drawImage(vg, x + w - 30.f, y + 110, i_size, i_size, m_update.image, 15);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
#include "owo.hpp"
|
||||
#include "swkbd.hpp"
|
||||
#include "i18n.hpp"
|
||||
#include "yati/yati.hpp"
|
||||
#include "yati/source/file.hpp"
|
||||
|
||||
#include <minIni.h>
|
||||
#include <minizip/unzip.h>
|
||||
@@ -52,13 +54,35 @@ constexpr std::string_view VIDEO_EXTENSIONS[] = {
|
||||
constexpr std::string_view IMAGE_EXTENSIONS[] = {
|
||||
"png", "jpg", "jpeg", "bmp", "gif",
|
||||
};
|
||||
|
||||
struct RomDatabaseEntry {
|
||||
std::string_view folder;
|
||||
std::string_view database;
|
||||
constexpr std::string_view INSTALL_EXTENSIONS[] = {
|
||||
"nsp", "xci", "nsz", "xcz",
|
||||
};
|
||||
|
||||
|
||||
struct RomDatabaseEntry {
|
||||
// uses the naming scheme from retropie.
|
||||
std::string_view folder{};
|
||||
// uses the naming scheme from Retroarch.
|
||||
std::string_view database{};
|
||||
// custom alias, to make everyone else happy.
|
||||
std::array<std::string_view, 4> alias{};
|
||||
|
||||
// compares against all of the above strings.
|
||||
auto IsDatabase(std::string_view name) const {
|
||||
if (name == folder || name == database) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const auto& str : alias) {
|
||||
if (!str.empty() && name == str) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// using PathPair = std::pair<std::string_view, std::string_view>;
|
||||
constexpr RomDatabaseEntry PATHS[]{
|
||||
{ "3do", "The 3DO Company - 3DO"},
|
||||
{ "atari800", "Atari - 8-bit"},
|
||||
@@ -94,6 +118,14 @@ constexpr RomDatabaseEntry PATHS[]{
|
||||
{ "pico8", "Sega - PICO"},
|
||||
{ "wonderswan", "Bandai - WonderSwan"},
|
||||
{ "wonderswancolor", "Bandai - WonderSwan Color"},
|
||||
|
||||
{ "mame", "MAME 2000", { "MAME", "mame-libretro", } },
|
||||
{ "mame", "MAME 2003", { "MAME", "mame-libretro", } },
|
||||
{ "mame", "MAME 2003-Plus", { "MAME", "mame-libretro", } },
|
||||
|
||||
{ "neogeo", "SNK - Neo Geo Pocket" },
|
||||
{ "neogeo", "SNK - Neo Geo Pocket Color" },
|
||||
{ "neogeo", "SNK - Neo Geo CD" },
|
||||
};
|
||||
|
||||
constexpr fs::FsPath DAYBREAK_PATH{"/switch/daybreak.nro"};
|
||||
@@ -114,49 +146,49 @@ auto IsExtension(std::string_view ext1, std::string_view ext2) -> bool {
|
||||
// tries to find database path using folder name
|
||||
// names are taken from retropie
|
||||
// retroarch database names can also be used
|
||||
auto GetRomDatabaseFromPath(std::string_view path) -> int {
|
||||
using RomDatabaseIndexs = std::vector<size_t>;
|
||||
auto GetRomDatabaseFromPath(std::string_view path) -> RomDatabaseIndexs {
|
||||
if (path.length() <= 1) {
|
||||
return -1;
|
||||
return {};
|
||||
}
|
||||
|
||||
// this won't fail :)
|
||||
RomDatabaseIndexs indexs;
|
||||
const auto db_name = path.substr(path.find_last_of('/') + 1);
|
||||
// log_write("new path: %s\n", db_name.c_str());
|
||||
|
||||
for (int i = 0; i < std::size(PATHS); i++) {
|
||||
auto& p = PATHS[i];
|
||||
if ((
|
||||
p.folder.length() == db_name.length() && !strncasecmp(p.folder.data(), db_name.data(), p.folder.length())) ||
|
||||
(p.database.length() == db_name.length() && !strncasecmp(p.database.data(), db_name.data(), p.database.length()))) {
|
||||
const auto& p = PATHS[i];
|
||||
if (p.IsDatabase(db_name)) {
|
||||
log_write("found it :) %.*s\n", (int)p.database.length(), p.database.data());
|
||||
return i;
|
||||
indexs.emplace_back(i);
|
||||
}
|
||||
}
|
||||
|
||||
// if we failed, try again but with the folder about
|
||||
// "/roms/psx/scooby-doo/scooby-doo.bin", this will check psx
|
||||
const auto last_off = path.substr(0, path.find_last_of('/'));
|
||||
if (const auto off = last_off.find_last_of('/'); off != std::string_view::npos) {
|
||||
const auto db_name2 = last_off.substr(off + 1);
|
||||
// printf("got db: %s\n", db_name2.c_str());
|
||||
for (int i = 0; i < std::size(PATHS); i++) {
|
||||
auto& p = PATHS[i];
|
||||
if ((
|
||||
p.folder.length() == db_name2.length() && !strcasecmp(p.folder.data(), db_name2.data())) ||
|
||||
(p.database.length() == db_name2.length() && !strcasecmp(p.database.data(), db_name2.data()))) {
|
||||
log_write("found it :) %.*s\n", (int)p.database.length(), p.database.data());
|
||||
return i;
|
||||
if (indexs.empty()) {
|
||||
const auto last_off = path.substr(0, path.find_last_of('/'));
|
||||
if (const auto off = last_off.find_last_of('/'); off != std::string_view::npos) {
|
||||
const auto db_name2 = last_off.substr(off + 1);
|
||||
// printf("got db: %s\n", db_name2.c_str());
|
||||
for (int i = 0; i < std::size(PATHS); i++) {
|
||||
const auto& p = PATHS[i];
|
||||
if (p.IsDatabase(db_name2)) {
|
||||
log_write("found it :) %.*s\n", (int)p.database.length(), p.database.data());
|
||||
indexs.emplace_back(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
return indexs;
|
||||
}
|
||||
|
||||
//
|
||||
auto GetRomIcon(fs::FsNative* fs, ProgressBox* pbox, std::string filename, std::string extension, int db_idx, const NroEntry& nro) {
|
||||
auto GetRomIcon(fs::FsNative* fs, ProgressBox* pbox, std::string filename, const RomDatabaseIndexs& db_indexs, const NroEntry& nro) {
|
||||
// if no db entries, use nro icon
|
||||
if (db_idx < 0) {
|
||||
if (db_indexs.empty()) {
|
||||
log_write("using nro image\n");
|
||||
return nro_get_icon(nro.path, nro.icon_size, nro.icon_offset);
|
||||
}
|
||||
@@ -178,49 +210,51 @@ auto GetRomIcon(fs::FsNative* fs, ProgressBox* pbox, std::string filename, std::
|
||||
#define RA_THUMBNAIL_PATH "/retroarch/thumbnails/"
|
||||
#define RA_BOXART_EXT ".png"
|
||||
|
||||
const auto system_name = std::string{PATHS[db_idx].database.data(), PATHS[db_idx].database.length()};//GetDatabaseFromExt(database, extension);
|
||||
auto system_name_gh = system_name + "/master";
|
||||
for (auto& c : system_name_gh) {
|
||||
if (c == ' ') {
|
||||
c = '_';
|
||||
for (auto db_idx : db_indexs) {
|
||||
const auto system_name = std::string{PATHS[db_idx].database.data(), PATHS[db_idx].database.length()};//GetDatabaseFromExt(database, extension);
|
||||
auto system_name_gh = system_name + "/master";
|
||||
for (auto& c : system_name_gh) {
|
||||
if (c == ' ') {
|
||||
c = '_';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string filename_gh;
|
||||
filename_gh.reserve(filename.size());
|
||||
for (auto c : filename) {
|
||||
if (c == ' ') {
|
||||
filename_gh += "%20";
|
||||
} else {
|
||||
filename_gh.push_back(c);
|
||||
std::string filename_gh;
|
||||
filename_gh.reserve(filename.size());
|
||||
for (auto c : filename) {
|
||||
if (c == ' ') {
|
||||
filename_gh += "%20";
|
||||
} else {
|
||||
filename_gh.push_back(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const std::string thumbnail_path = system_name + RA_BOXART_NAME + filename + RA_BOXART_EXT;
|
||||
const std::string ra_thumbnail_path = RA_THUMBNAIL_PATH + thumbnail_path;
|
||||
const std::string ra_thumbnail_url = RA_BOXART_URL + thumbnail_path;
|
||||
const std::string gh_thumbnail_url = GH_BOXART_URL + system_name_gh + RA_BOXART_NAME + filename_gh + RA_BOXART_EXT;
|
||||
const std::string thumbnail_path = system_name + RA_BOXART_NAME + filename + RA_BOXART_EXT;
|
||||
const std::string ra_thumbnail_path = RA_THUMBNAIL_PATH + thumbnail_path;
|
||||
const std::string ra_thumbnail_url = RA_BOXART_URL + thumbnail_path;
|
||||
const std::string gh_thumbnail_url = GH_BOXART_URL + system_name_gh + RA_BOXART_NAME + filename_gh + RA_BOXART_EXT;
|
||||
|
||||
log_write("starting image convert on: %s\n", ra_thumbnail_path.c_str());
|
||||
// try and find icon locally
|
||||
if (!pbox->ShouldExit()) {
|
||||
pbox->NewTransfer("Trying to load "_i18n + ra_thumbnail_path);
|
||||
std::vector<u8> image_file;
|
||||
if (R_SUCCEEDED(fs->read_entire_file(ra_thumbnail_path.c_str(), image_file))) {
|
||||
return image_file;
|
||||
log_write("starting image convert on: %s\n", ra_thumbnail_path.c_str());
|
||||
// try and find icon locally
|
||||
if (!pbox->ShouldExit()) {
|
||||
pbox->NewTransfer("Trying to load "_i18n + ra_thumbnail_path);
|
||||
std::vector<u8> image_file;
|
||||
if (R_SUCCEEDED(fs->read_entire_file(ra_thumbnail_path.c_str(), image_file))) {
|
||||
return image_file;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// try and download icon
|
||||
if (!pbox->ShouldExit()) {
|
||||
pbox->NewTransfer("Downloading "_i18n + gh_thumbnail_url);
|
||||
const auto result = curl::Api().ToMemory(
|
||||
curl::Url{gh_thumbnail_url},
|
||||
curl::OnProgress{pbox->OnDownloadProgressCallback()}
|
||||
);
|
||||
// try and download icon
|
||||
if (!pbox->ShouldExit()) {
|
||||
pbox->NewTransfer("Downloading "_i18n + gh_thumbnail_url);
|
||||
const auto result = curl::Api().ToMemory(
|
||||
curl::Url{gh_thumbnail_url},
|
||||
curl::OnProgress{pbox->OnDownloadProgressCallback()}
|
||||
);
|
||||
|
||||
if (result.success && !result.data.empty()) {
|
||||
return result.data;
|
||||
if (result.success && !result.data.empty()) {
|
||||
return result.data;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,26 +279,6 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i1
|
||||
m_selected_count--;
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::DOWN, Action{[this](){
|
||||
if (m_list->ScrollDown(m_index, 1, m_entries_current.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::UP, Action{[this](){
|
||||
if (m_list->ScrollUp(m_index, 1, m_entries_current.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::DPAD_RIGHT, Action{[this](){
|
||||
if (m_list->ScrollDown(m_index, 8, m_entries_current.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::DPAD_LEFT, Action{[this](){
|
||||
if (m_list->ScrollUp(m_index, 8, m_entries_current.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::A, Action{"Open"_i18n, [this](){
|
||||
if (m_entries_current.empty()) {
|
||||
return;
|
||||
@@ -294,6 +308,8 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i1
|
||||
nro_launch(GetNewPathCurrent());
|
||||
}
|
||||
}));
|
||||
} else if (App::GetInstallEnable() && IsExtension(entry.GetExtension(), INSTALL_EXTENSIONS)) {
|
||||
InstallFile(GetEntry());
|
||||
} else {
|
||||
const auto assoc_list = FindFileAssocFor();
|
||||
if (!assoc_list.empty()) {
|
||||
@@ -406,7 +422,7 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i1
|
||||
}
|
||||
log_write("clicked on delete\n");
|
||||
App::Push(std::make_shared<OptionBox>(
|
||||
"Delete Selected files?"_i18n, "No"_i18n, "Yes"_i18n, 1, [this](auto op_index){
|
||||
"Delete Selected files?"_i18n, "No"_i18n, "Yes"_i18n, 0, [this](auto op_index){
|
||||
if (op_index && *op_index) {
|
||||
App::PopToMenu();
|
||||
OnDeleteCallback();
|
||||
@@ -421,7 +437,7 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i1
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Paste"_i18n, [this](){
|
||||
const std::string buf = "Paste "_i18n + std::to_string(m_selected_files.size()) + " file(s)?"_i18n;
|
||||
App::Push(std::make_shared<OptionBox>(
|
||||
buf, "No"_i18n, "Yes"_i18n, 1, [this](auto op_index){
|
||||
buf, "No"_i18n, "Yes"_i18n, 0, [this](auto op_index){
|
||||
if (op_index && *op_index) {
|
||||
App::PopToMenu();
|
||||
OnPasteCallback();
|
||||
@@ -459,6 +475,32 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i1
|
||||
}));
|
||||
}
|
||||
|
||||
// if install is enabled, check if all currently selected files are installable.
|
||||
if (m_entries_current.size() && App::GetInstallEnable()) {
|
||||
bool should_install = true;
|
||||
if (!m_selected_count) {
|
||||
should_install = IsExtension(GetEntry().GetExtension(), INSTALL_EXTENSIONS);
|
||||
} else {
|
||||
const auto entries = GetSelectedEntries();
|
||||
for (auto&e : entries) {
|
||||
if (!IsExtension(e.GetExtension(), INSTALL_EXTENSIONS)) {
|
||||
should_install = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (should_install) {
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Install"_i18n, [this](){
|
||||
if (!m_selected_count) {
|
||||
InstallFile(GetEntry());
|
||||
} else {
|
||||
InstallFiles(GetSelectedEntries());
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Advanced"_i18n, [this](){
|
||||
auto options = std::make_shared<Sidebar>("Advanced Options"_i18n, Sidebar::Side::RIGHT);
|
||||
ON_SCOPE_EXIT(App::Push(options));
|
||||
@@ -564,8 +606,8 @@ Menu::~Menu() {
|
||||
|
||||
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
MenuBase::Update(controller, touch);
|
||||
m_list->OnUpdate(controller, touch, m_entries_current.size(), [this](auto i) {
|
||||
if (m_index == i) {
|
||||
m_list->OnUpdate(controller, touch, m_index, m_entries_current.size(), [this](bool touch, auto i) {
|
||||
if (touch && m_index == i) {
|
||||
FireAction(Button::A);
|
||||
} else {
|
||||
App::PlaySoundEffect(SoundEffect_Focus);
|
||||
@@ -628,6 +670,9 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
icon = ThemeEntryID_ICON_VIDEO;
|
||||
} else if (IsExtension(ext, IMAGE_EXTENSIONS)) {
|
||||
icon = ThemeEntryID_ICON_IMAGE;
|
||||
} else if (IsExtension(ext, INSTALL_EXTENSIONS)) {
|
||||
// todo: maybe replace this icon with something else?
|
||||
icon = ThemeEntryID_ICON_NRO;
|
||||
} else if (IsExtension(ext, "zip")) {
|
||||
icon = ThemeEntryID_ICON_ZIP;
|
||||
} else if (IsExtension(ext, "nro")) {
|
||||
@@ -737,7 +782,7 @@ void Menu::InstallForwarder() {
|
||||
if (op_index) {
|
||||
const auto assoc = assoc_list[*op_index];
|
||||
log_write("pushing it\n");
|
||||
App::Push(std::make_shared<ProgressBox>("Installing Forwarder"_i18n, [assoc, this](auto pbox) -> bool {
|
||||
App::Push(std::make_shared<ProgressBox>(0, "Installing Forwarder"_i18n, GetEntry().name, [assoc, this](auto pbox) -> bool {
|
||||
log_write("inside callback\n");
|
||||
|
||||
NroEntry nro{};
|
||||
@@ -747,8 +792,7 @@ void Menu::InstallForwarder() {
|
||||
return false;
|
||||
}
|
||||
log_write("got nro data\n");
|
||||
std::string file_name = GetEntry().GetInternalName();
|
||||
std::string extension = GetEntry().GetInternalExtension();
|
||||
auto file_name = assoc.use_base_name ? GetEntry().GetName() : GetEntry().GetInternalName();
|
||||
|
||||
if (auto pos = file_name.find_last_of('.'); pos != std::string::npos) {
|
||||
log_write("got filename\n");
|
||||
@@ -756,7 +800,7 @@ void Menu::InstallForwarder() {
|
||||
log_write("got filename2: %s\n\n", file_name.c_str());
|
||||
}
|
||||
|
||||
const auto db_idx = GetRomDatabaseFromPath(m_path);
|
||||
const auto db_indexs = GetRomDatabaseFromPath(m_path);
|
||||
|
||||
OwoConfig config{};
|
||||
config.nro_path = assoc.path.toString();
|
||||
@@ -764,7 +808,8 @@ void Menu::InstallForwarder() {
|
||||
config.name = nro.nacp.lang[0].name + std::string{" | "} + file_name;
|
||||
// config.name = file_name;
|
||||
config.nacp = nro.nacp;
|
||||
config.icon = GetRomIcon(m_fs.get(), pbox, file_name, extension, db_idx, nro);
|
||||
config.icon = GetRomIcon(m_fs.get(), pbox, file_name, db_indexs, nro);
|
||||
pbox->SetImageDataConst(config.icon);
|
||||
|
||||
return R_SUCCEEDED(App::Install(pbox, config));
|
||||
}));
|
||||
@@ -775,6 +820,34 @@ void Menu::InstallForwarder() {
|
||||
));
|
||||
}
|
||||
|
||||
void Menu::InstallFile(const FileEntry& target) {
|
||||
std::vector<FileEntry> targets{target};
|
||||
InstallFiles(targets);
|
||||
}
|
||||
|
||||
void Menu::InstallFiles(const std::vector<FileEntry>& targets) {
|
||||
App::Push(std::make_shared<OptionBox>("Install Selected files?"_i18n, "No"_i18n, "Yes"_i18n, 0, [this, targets](auto op_index){
|
||||
if (op_index && *op_index) {
|
||||
App::PopToMenu();
|
||||
|
||||
App::Push(std::make_shared<ui::ProgressBox>(0, "Installing "_i18n, "", [this, targets](auto pbox) mutable -> bool {
|
||||
for (auto& e : targets) {
|
||||
const auto rc = yati::InstallFromFile(pbox, &m_fs->m_fs, GetNewPath(e));
|
||||
if (rc == yati::Result_Cancelled) {
|
||||
break;
|
||||
} else if (R_FAILED(rc)) {
|
||||
return false;
|
||||
} else {
|
||||
App::Notify("Installed " + e.GetName());
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}));
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
auto Menu::Scan(const fs::FsPath& new_path, bool is_walk_up) -> Result {
|
||||
log_write("new scan path: %s\n", new_path.s);
|
||||
if (!is_walk_up && !m_path.empty() && !m_entries_current.empty()) {
|
||||
@@ -847,32 +920,32 @@ auto Menu::Scan(const fs::FsPath& new_path, bool is_walk_up) -> Result {
|
||||
|
||||
auto Menu::FindFileAssocFor() -> std::vector<FileAssocEntry> {
|
||||
// only support roms in correctly named folders, sorry!
|
||||
const auto db_idx = GetRomDatabaseFromPath(m_path);
|
||||
const auto db_indexs = GetRomDatabaseFromPath(m_path);
|
||||
const auto& entry = GetEntry();
|
||||
const auto extension = entry.internal_extension.empty() ? entry.extension : entry.internal_extension;
|
||||
if (extension.empty()) {
|
||||
const auto extension = entry.extension;
|
||||
const auto internal_extension = entry.internal_extension.empty() ? entry.extension : entry.internal_extension;
|
||||
if (extension.empty() && internal_extension.empty()) {
|
||||
// log_write("failed to get extension for db: %s path: %s\n", database_entry.c_str(), m_path);
|
||||
return {};
|
||||
}
|
||||
|
||||
// log_write("got extension for db: %s path: %s\n", database_entry.c_str(), m_path);
|
||||
|
||||
|
||||
std::vector<FileAssocEntry> out_entries;
|
||||
if (db_idx >= 0) {
|
||||
if (!db_indexs.empty()) {
|
||||
// if database isn't empty, then we are in a valid folder
|
||||
// search for an entry that matches the db and ext
|
||||
for (const auto& assoc : m_assoc_entries) {
|
||||
for (const auto& assoc_db : assoc.database) {
|
||||
if (assoc_db == PATHS[db_idx].folder || assoc_db == PATHS[db_idx].database) {
|
||||
for (const auto& assoc_ext : assoc.ext) {
|
||||
if (assoc_ext == extension) {
|
||||
log_write("found ext: %s assoc_ext: %s assoc.ext: %s\n", assoc.path.s, assoc_ext.c_str(), extension.c_str());
|
||||
// if (assoc_db == PATHS[db_idx].folder || assoc_db == PATHS[db_idx].database) {
|
||||
for (auto db_idx : db_indexs) {
|
||||
if (PATHS[db_idx].IsDatabase(assoc_db)) {
|
||||
if (assoc.IsExtension(extension, internal_extension)) {
|
||||
out_entries.emplace_back(assoc);
|
||||
goto jump;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
jump:
|
||||
}
|
||||
} else {
|
||||
// otherwise, if not in a valid folder, find an entry that doesn't
|
||||
@@ -883,11 +956,9 @@ auto Menu::FindFileAssocFor() -> std::vector<FileAssocEntry> {
|
||||
// to be in the correct folder, ie psx, to know what system that .iso is for.
|
||||
for (const auto& assoc : m_assoc_entries) {
|
||||
if (assoc.database.empty()) {
|
||||
for (const auto& assoc_ext : assoc.ext) {
|
||||
if (assoc_ext == extension) {
|
||||
log_write("found ext: %s\n", assoc.path.s);
|
||||
out_entries.emplace_back(assoc);
|
||||
}
|
||||
if (assoc.IsExtension(extension, internal_extension)) {
|
||||
log_write("found ext: %s\n", assoc.path.s);
|
||||
out_entries.emplace_back(assoc);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -944,6 +1015,10 @@ void Menu::LoadAssocEntriesPath(const fs::FsPath& path) {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (!strcmp(Key, "use_base_name")) {
|
||||
if (!strcmp(Value, "true") || !strcmp(Value, "1")) {
|
||||
assoc->use_base_name = true;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}, &assoc, full_path);
|
||||
@@ -979,7 +1054,7 @@ void Menu::LoadAssocEntriesPath(const fs::FsPath& path) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// log_write("\tpath: %s\n", assoc.path.c_str());
|
||||
// log_write("\tpath: %s\n", assoc.path.s);
|
||||
// log_write("\tname: %s\n", assoc.name.c_str());
|
||||
// for (const auto& ext : assoc.ext) {
|
||||
// log_write("\t\text: %s\n", ext.c_str());
|
||||
@@ -1132,7 +1207,7 @@ void Menu::OnDeleteCallback() {
|
||||
Scan(m_path);
|
||||
log_write("did delete\n");
|
||||
} else {
|
||||
App::Push(std::make_shared<ProgressBox>("Deleting"_i18n, [this](auto pbox){
|
||||
App::Push(std::make_shared<ProgressBox>(0, "Deleting"_i18n, "", [this](auto pbox){
|
||||
FsDirCollections collections;
|
||||
|
||||
// build list of dirs / files
|
||||
@@ -1225,7 +1300,7 @@ void Menu::OnPasteCallback() {
|
||||
Scan(m_path);
|
||||
log_write("did paste\n");
|
||||
} else {
|
||||
App::Push(std::make_shared<ProgressBox>("Pasting"_i18n, [this](auto pbox){
|
||||
App::Push(std::make_shared<ProgressBox>(0, "Pasting"_i18n, "", [this](auto pbox){
|
||||
|
||||
if (m_selected_type == SelectedType::Cut) {
|
||||
for (const auto& p : m_selected_files) {
|
||||
|
||||
300
sphaira/source/ui/menus/ftp_menu.cpp
Normal file
300
sphaira/source/ui/menus/ftp_menu.cpp
Normal file
@@ -0,0 +1,300 @@
|
||||
#include "ui/menus/ftp_menu.hpp"
|
||||
#include "yati/yati.hpp"
|
||||
#include "app.hpp"
|
||||
#include "defines.hpp"
|
||||
#include "log.hpp"
|
||||
#include "ui/nvg_util.hpp"
|
||||
#include "i18n.hpp"
|
||||
#include "ftpsrv_helper.hpp"
|
||||
#include <cstring>
|
||||
|
||||
namespace sphaira::ui::menu::ftp {
|
||||
namespace {
|
||||
|
||||
constexpr u64 MAX_BUFFER_SIZE = 1024*1024*32;
|
||||
constexpr u64 SLEEPNS = 1000;
|
||||
volatile bool IN_PUSH_THREAD{};
|
||||
|
||||
bool OnInstallStart(void* user, const char* path) {
|
||||
auto menu = (Menu*)user;
|
||||
log_write("[INSTALL] inside OnInstallStart()\n");
|
||||
|
||||
for (;;) {
|
||||
mutexLock(&menu->m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&menu->m_mutex));
|
||||
|
||||
if (menu->m_state != State::Progress) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (menu->GetToken().stop_requested()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
svcSleepThread(1e+6);
|
||||
}
|
||||
|
||||
log_write("[INSTALL] OnInstallStart() got state: %u\n", (u8)menu->m_state);
|
||||
|
||||
if (menu->m_source) {
|
||||
log_write("[INSTALL] OnInstallStart() we have source\n");
|
||||
for (;;) {
|
||||
mutexLock(&menu->m_source->m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&menu->m_source->m_mutex));
|
||||
|
||||
if (!IN_PUSH_THREAD) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (menu->GetToken().stop_requested()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
svcSleepThread(1e+6);
|
||||
}
|
||||
|
||||
log_write("[INSTALL] OnInstallStart() stopped polling source\n");
|
||||
}
|
||||
|
||||
log_write("[INSTALL] OnInstallStart() doing make_shared\n");
|
||||
menu->m_source = std::make_shared<StreamFtp>(path, menu->GetToken());
|
||||
|
||||
mutexLock(&menu->m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&menu->m_mutex));
|
||||
menu->m_state = State::Connected;
|
||||
log_write("[INSTALL] OnInstallStart() done make shared\n");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OnInstallWrite(void* user, const void* buf, size_t size) {
|
||||
auto menu = (Menu*)user;
|
||||
|
||||
return menu->m_source->Push(buf, size);
|
||||
}
|
||||
|
||||
void OnInstallClose(void* user) {
|
||||
auto menu = (Menu*)user;
|
||||
menu->m_source->Disable();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
StreamFtp::StreamFtp(const fs::FsPath& path, std::stop_token token) {
|
||||
m_path = path;
|
||||
m_token = token;
|
||||
m_buffer.reserve(MAX_BUFFER_SIZE);
|
||||
m_active = true;
|
||||
}
|
||||
|
||||
Result StreamFtp::ReadChunk(void* buf, s64 size, u64* bytes_read) {
|
||||
while (!m_token.stop_requested()) {
|
||||
mutexLock(&m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||
|
||||
if (m_buffer.empty()) {
|
||||
if (!m_active) {
|
||||
break;
|
||||
}
|
||||
|
||||
svcSleepThread(SLEEPNS);
|
||||
} else {
|
||||
size = std::min<s64>(size, m_buffer.size());
|
||||
std::memcpy(buf, m_buffer.data(), size);
|
||||
m_buffer.erase(m_buffer.begin(), m_buffer.begin() + size);
|
||||
*bytes_read = size;
|
||||
R_SUCCEED();
|
||||
}
|
||||
}
|
||||
|
||||
return 0x1;
|
||||
}
|
||||
|
||||
bool StreamFtp::Push(const void* buf, s64 size) {
|
||||
IN_PUSH_THREAD = true;
|
||||
ON_SCOPE_EXIT(IN_PUSH_THREAD = false);
|
||||
|
||||
while (!m_token.stop_requested()) {
|
||||
mutexLock(&m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||
|
||||
if (!m_active) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (m_buffer.size() + size >= MAX_BUFFER_SIZE) {
|
||||
svcSleepThread(SLEEPNS);
|
||||
} else {
|
||||
const auto offset = m_buffer.size();
|
||||
m_buffer.resize(offset + size);
|
||||
std::memcpy(m_buffer.data() + offset, buf, size);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void StreamFtp::Disable() {
|
||||
mutexLock(&m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||
m_active = false;
|
||||
}
|
||||
|
||||
Menu::Menu() : MenuBase{"FTP Install (EXPERIMENTAL)"_i18n} {
|
||||
SetAction(Button::B, Action{"Back"_i18n, [this](){
|
||||
SetPop();
|
||||
}});
|
||||
|
||||
SetAction(Button::X, Action{"Options"_i18n, [this](){
|
||||
App::DisplayInstallOptions(false);
|
||||
}});
|
||||
|
||||
mutexInit(&m_mutex);
|
||||
ftpsrv::InitInstallMode(this, OnInstallStart, OnInstallWrite, OnInstallClose);
|
||||
|
||||
m_port = ftpsrv::GetPort();
|
||||
m_anon = ftpsrv::IsAnon();
|
||||
if (!m_anon) {
|
||||
m_user = ftpsrv::GetUser();
|
||||
m_pass = ftpsrv::GetPass();
|
||||
}
|
||||
}
|
||||
|
||||
Menu::~Menu() {
|
||||
// signal for thread to exit and wait.
|
||||
ftpsrv::DisableInstallMode();
|
||||
m_stop_source.request_stop();
|
||||
|
||||
if (m_source) {
|
||||
m_source->Disable();
|
||||
}
|
||||
|
||||
log_write("closing data!!!!\n");
|
||||
}
|
||||
|
||||
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
MenuBase::Update(controller, touch);
|
||||
|
||||
mutexLock(&m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||
|
||||
switch (m_state) {
|
||||
case State::None:
|
||||
break;
|
||||
|
||||
case State::Connected:
|
||||
log_write("set to progress\n");
|
||||
m_state = State::Progress;
|
||||
log_write("got connection\n");
|
||||
App::Push(std::make_shared<ui::ProgressBox>(0, "Installing "_i18n, "", [this](auto pbox) mutable -> bool {
|
||||
log_write("inside progress box\n");
|
||||
const auto rc = yati::InstallFromSource(pbox, m_source, m_source->m_path);
|
||||
if (R_FAILED(rc)) {
|
||||
m_source->Disable();
|
||||
return false;
|
||||
}
|
||||
|
||||
log_write("progress box is done\n");
|
||||
return true;
|
||||
}, [this](bool result){
|
||||
mutexLock(&m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||
|
||||
if (result) {
|
||||
App::Notify("Ftp install success!"_i18n);
|
||||
m_state = State::Done;
|
||||
} else {
|
||||
App::Notify("Ftp install failed!"_i18n);
|
||||
m_state = State::Failed;
|
||||
}
|
||||
}));
|
||||
break;
|
||||
|
||||
case State::Progress:
|
||||
case State::Done:
|
||||
case State::Failed:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
MenuBase::Draw(vg, theme);
|
||||
|
||||
mutexLock(&m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||
|
||||
if (m_ip) {
|
||||
if (m_type == NifmInternetConnectionType_WiFi) {
|
||||
SetSubHeading("Connection Type: WiFi | Strength: "_i18n + std::to_string(m_strength));
|
||||
} else {
|
||||
SetSubHeading("Connection Type: Ethernet"_i18n);
|
||||
}
|
||||
} else {
|
||||
SetSubHeading("Connection Type: None"_i18n);
|
||||
}
|
||||
|
||||
const float start_x = 80;
|
||||
const float font_size = 22;
|
||||
const float spacing = 33;
|
||||
float start_y = 125;
|
||||
float bounds[4];
|
||||
|
||||
nvgFontSize(vg, font_size);
|
||||
|
||||
// note: textbounds strips spaces...todo: use nvgTextGlyphPositions() instead.
|
||||
#define draw(key, ...) \
|
||||
gfx::textBounds(vg, start_x, start_y, bounds, key.c_str()); \
|
||||
gfx::drawTextArgs(vg, start_x, start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT), key.c_str()); \
|
||||
gfx::drawTextArgs(vg, bounds[2], start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_SELECTED), __VA_ARGS__); \
|
||||
start_y += spacing;
|
||||
|
||||
if (m_ip) {
|
||||
draw("Host:"_i18n, " %u.%u.%u.%u", m_ip&0xFF, (m_ip>>8)&0xFF, (m_ip>>16)&0xFF, (m_ip>>24)&0xFF);
|
||||
draw("Port:"_i18n, " %u", m_port);
|
||||
if (!m_anon) {
|
||||
draw("Username:"_i18n, " %s", m_user);
|
||||
draw("Password:"_i18n, " %s", m_pass);
|
||||
}
|
||||
|
||||
if (m_type == NifmInternetConnectionType_WiFi) {
|
||||
NifmNetworkProfileData profile{};
|
||||
if (R_SUCCEEDED(nifmGetCurrentNetworkProfile(&profile))) {
|
||||
const auto& settings = profile.wireless_setting_data;
|
||||
std::string passphrase;
|
||||
std::transform(std::cbegin(settings.passphrase), std::cend(settings.passphrase), passphrase.begin(), toascii);
|
||||
draw("SSID:"_i18n, " %.*s", settings.ssid_len, settings.ssid);
|
||||
draw("Passphrase:"_i18n, " %s", passphrase.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#undef draw
|
||||
|
||||
switch (m_state) {
|
||||
case State::None:
|
||||
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Waiting for connection..."_i18n.c_str());
|
||||
break;
|
||||
|
||||
case State::Connected:
|
||||
break;
|
||||
|
||||
case State::Progress:
|
||||
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Transferring data..."_i18n.c_str());
|
||||
break;
|
||||
|
||||
case State::Done:
|
||||
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Press B to exit..."_i18n.c_str());
|
||||
break;
|
||||
|
||||
case State::Failed:
|
||||
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Failed to install via FTP, press B to exit..."_i18n.c_str());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::OnFocusGained() {
|
||||
MenuBase::OnFocusGained();
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui::menu::ftp
|
||||
499
sphaira/source/ui/menus/gc_menu.cpp
Normal file
499
sphaira/source/ui/menus/gc_menu.cpp
Normal file
@@ -0,0 +1,499 @@
|
||||
#include "ui/menus/gc_menu.hpp"
|
||||
#include "yati/yati.hpp"
|
||||
#include "yati/nx/nca.hpp"
|
||||
#include "app.hpp"
|
||||
#include "defines.hpp"
|
||||
#include "log.hpp"
|
||||
#include "ui/nvg_util.hpp"
|
||||
#include "i18n.hpp"
|
||||
#include <cstring>
|
||||
|
||||
namespace sphaira::ui::menu::gc {
|
||||
namespace {
|
||||
|
||||
const char *g_option_list[] = {
|
||||
"Nand Install",
|
||||
"SD Card Install",
|
||||
"Exit",
|
||||
};
|
||||
|
||||
struct HashStr {
|
||||
char str[0x21];
|
||||
};
|
||||
|
||||
HashStr hexIdToStr(auto id) {
|
||||
HashStr str{};
|
||||
const auto id_lower = std::byteswap(*(u64*)id.c);
|
||||
const auto id_upper = std::byteswap(*(u64*)(id.c + 0x8));
|
||||
std::snprintf(str.str, 0x21, "%016lx%016lx", id_lower, id_upper);
|
||||
return str;
|
||||
}
|
||||
|
||||
// @Gc is the mount point, S is for secure partion, the remaining is the
|
||||
// the gamecard handle value in lower-case hex.
|
||||
auto BuildGcPath(const char* name, const FsGameCardHandle* handle, FsGameCardPartition partiton = FsGameCardPartition_Secure) -> fs::FsPath {
|
||||
static const char mount_parition[] = {
|
||||
[FsGameCardPartition_Update] = 'U',
|
||||
[FsGameCardPartition_Normal] = 'N',
|
||||
[FsGameCardPartition_Secure] = 'S',
|
||||
[FsGameCardPartition_Logo] = 'L',
|
||||
};
|
||||
|
||||
fs::FsPath path;
|
||||
std::snprintf(path, sizeof(path), "@Gc%c%08x://%s", mount_parition[partiton], handle->value, name);
|
||||
return path;
|
||||
}
|
||||
|
||||
auto InRange(u64 off, u64 offset, u64 size) -> bool {
|
||||
return off < offset + size && off >= offset;
|
||||
}
|
||||
|
||||
struct GcSource final : yati::source::Base {
|
||||
GcSource(const ApplicationEntry& entry, fs::FsNativeGameCard* fs, bool sd_install);
|
||||
~GcSource();
|
||||
Result Read(void* buf, s64 off, s64 size, u64* bytes_read);
|
||||
|
||||
yati::container::Collections m_collections{};
|
||||
yati::ConfigOverride m_config{};
|
||||
fs::FsNativeGameCard* m_fs{};
|
||||
FsFile m_file{};
|
||||
s64 m_offset{};
|
||||
s64 m_size{};
|
||||
};
|
||||
|
||||
GcSource::GcSource(const ApplicationEntry& entry, fs::FsNativeGameCard* fs, bool sd_install)
|
||||
: m_fs{fs} {
|
||||
m_offset = -1;
|
||||
|
||||
s64 offset{};
|
||||
const auto add_collections = [&](const auto& collections) {
|
||||
for (auto collection : collections) {
|
||||
collection.offset = offset;
|
||||
m_collections.emplace_back(collection);
|
||||
offset += collection.size;
|
||||
}
|
||||
};
|
||||
|
||||
const auto add_entries = [&](const auto& entries) {
|
||||
for (auto& e : entries) {
|
||||
add_collections(e);
|
||||
}
|
||||
};
|
||||
|
||||
// yati can handle all of this for use, however, yati lacks information
|
||||
// for ncas until it installs the cnmt and parses it.
|
||||
// as we already have this info, we can only send yati what we want to install.
|
||||
if (App::GetApp()->m_ticket_only.Get()) {
|
||||
add_collections(entry.tickets);
|
||||
} else {
|
||||
if (!App::GetApp()->m_skip_base.Get()) {
|
||||
add_entries(entry.application);
|
||||
}
|
||||
if (!App::GetApp()->m_skip_patch.Get()) {
|
||||
add_entries(entry.patch);
|
||||
}
|
||||
if (!App::GetApp()->m_skip_addon.Get()) {
|
||||
add_entries(entry.add_on);
|
||||
}
|
||||
if (!App::GetApp()->m_skip_data_patch.Get()) {
|
||||
add_entries(entry.data_patch);
|
||||
}
|
||||
if (!App::GetApp()->m_skip_ticket.Get()) {
|
||||
add_collections(entry.tickets);
|
||||
}
|
||||
}
|
||||
|
||||
// we don't need to verify the nca's, this speeds up installs.
|
||||
m_config.sd_card_install = sd_install;
|
||||
m_config.skip_nca_hash_verify = true;
|
||||
m_config.skip_rsa_header_fixed_key_verify = true;
|
||||
m_config.skip_rsa_npdm_fixed_key_verify = true;
|
||||
}
|
||||
|
||||
GcSource::~GcSource() {
|
||||
fsFileClose(&m_file);
|
||||
}
|
||||
|
||||
Result GcSource::Read(void* buf, s64 off, s64 size, u64* bytes_read) {
|
||||
// check is we need to open a new file.
|
||||
if (!InRange(off, m_offset, m_size)) {
|
||||
fsFileClose(&m_file);
|
||||
m_file = {};
|
||||
|
||||
// find new file based on the offset.
|
||||
bool found = false;
|
||||
for (auto& collection : m_collections) {
|
||||
if (InRange(off, collection.offset, collection.size)) {
|
||||
found = true;
|
||||
m_offset = collection.offset;
|
||||
m_size = collection.size;
|
||||
R_TRY(m_fs->OpenFile(fs::AppendPath("/", collection.name), FsOpenMode_Read, &m_file));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// this will never fail, unless i break something in yati.
|
||||
R_UNLESS(found, 0x1);
|
||||
}
|
||||
|
||||
return fsFileRead(&m_file, off - m_offset, buf, size, 0, bytes_read);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto ApplicationEntry::GetSize(const std::vector<GcCollections>& entries) const -> s64 {
|
||||
s64 size{};
|
||||
for (auto& e : entries) {
|
||||
for (auto& collection : e) {
|
||||
size += collection.size;
|
||||
}
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
auto ApplicationEntry::GetSize() const -> s64 {
|
||||
s64 size{};
|
||||
size += GetSize(application);
|
||||
size += GetSize(patch);
|
||||
size += GetSize(add_on);
|
||||
size += GetSize(data_patch);
|
||||
return size;
|
||||
}
|
||||
|
||||
Menu::Menu() : MenuBase{"GameCard"_i18n} {
|
||||
this->SetActions(
|
||||
std::make_pair(Button::B, Action{"Back"_i18n, [this](){
|
||||
SetPop();
|
||||
}}),
|
||||
std::make_pair(Button::X, Action{"Options"_i18n, [this](){
|
||||
App::DisplayInstallOptions(false);
|
||||
}})
|
||||
);
|
||||
|
||||
const Vec4 v{485, 275, 720, 70};
|
||||
const Vec2 pad{0, 125 - v.h};
|
||||
m_list = std::make_unique<List>(1, 3, m_pos, v, pad);
|
||||
|
||||
fsOpenDeviceOperator(std::addressof(m_dev_op));
|
||||
UpdateStorageSize();
|
||||
}
|
||||
|
||||
Menu::~Menu() {
|
||||
GcUnmount();
|
||||
fsDeviceOperatorClose(std::addressof(m_dev_op));
|
||||
}
|
||||
|
||||
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
// poll for the gamecard first before handling inputs as the gamecard
|
||||
// may have been removed, thus pressing A would fail.
|
||||
bool inserted{};
|
||||
GcPoll(&inserted);
|
||||
if (m_mounted != inserted) {
|
||||
log_write("gc state changed\n");
|
||||
m_mounted = inserted;
|
||||
if (m_mounted) {
|
||||
log_write("trying to mount\n");
|
||||
m_mounted = R_SUCCEEDED(GcMount());
|
||||
} else {
|
||||
log_write("trying to unmount\n");
|
||||
GcUnmount();
|
||||
}
|
||||
}
|
||||
|
||||
MenuBase::Update(controller, touch);
|
||||
m_list->OnUpdate(controller, touch, m_option_index, std::size(g_option_list), [this](bool touch, auto i) {
|
||||
if (touch && m_option_index == i) {
|
||||
FireAction(Button::A);
|
||||
} else {
|
||||
App::PlaySoundEffect(SoundEffect_Focus);
|
||||
m_option_index = i;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
MenuBase::Draw(vg, theme);
|
||||
|
||||
#define STORAGE_BAR_W 325
|
||||
#define STORAGE_BAR_H 14
|
||||
|
||||
const auto size_sd_gb = (double)m_size_free_sd / 0x40000000;
|
||||
const auto size_nand_gb = (double)m_size_free_nand / 0x40000000;
|
||||
|
||||
gfx::drawTextArgs(vg, 490, 135, 23.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "System memory %.1f GB", size_nand_gb);
|
||||
gfx::drawRect(vg, 480, 170, STORAGE_BAR_W, STORAGE_BAR_H, theme->GetColour(ThemeEntryID_TEXT));
|
||||
gfx::drawRect(vg, 480 + 1, 170 + 1, STORAGE_BAR_W - 2, STORAGE_BAR_H - 2, theme->GetColour(ThemeEntryID_BACKGROUND));
|
||||
gfx::drawRect(vg, 480 + 2, 170 + 2, STORAGE_BAR_W - (((double)m_size_free_nand / (double)m_size_total_nand) * STORAGE_BAR_W) - 4, STORAGE_BAR_H - 4, theme->GetColour(ThemeEntryID_TEXT));
|
||||
|
||||
gfx::drawTextArgs(vg, 870, 135, 23.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "microSD card %.1f GB", size_sd_gb);
|
||||
gfx::drawRect(vg, 860, 170, STORAGE_BAR_W, STORAGE_BAR_H, theme->GetColour(ThemeEntryID_TEXT));
|
||||
gfx::drawRect(vg, 860 + 1, 170 + 1, STORAGE_BAR_W - 2, STORAGE_BAR_H - 2, theme->GetColour(ThemeEntryID_BACKGROUND));
|
||||
gfx::drawRect(vg, 860 + 2, 170 + 2, STORAGE_BAR_W - (((double)m_size_free_sd / (double)m_size_total_sd) * STORAGE_BAR_W) - 4, STORAGE_BAR_H - 4, theme->GetColour(ThemeEntryID_TEXT));
|
||||
|
||||
gfx::drawRect(vg, 30, 90, 375, 555, theme->GetColour(ThemeEntryID_GRID));
|
||||
|
||||
if (!m_entries.empty()) {
|
||||
const auto& e = m_entries[m_entry_index];
|
||||
const auto size = e.GetSize();
|
||||
gfx::drawImage(vg, 90, 130, 256, 256, m_icon ? m_icon : App::GetDefaultImage());
|
||||
|
||||
nvgSave(vg);
|
||||
nvgIntersectScissor(vg, 50, 90, 325, 555);
|
||||
gfx::drawTextArgs(vg, 50, 415, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%s", m_lang_entry.name);
|
||||
gfx::drawTextArgs(vg, 50, 455, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%s", m_lang_entry.author);
|
||||
gfx::drawTextArgs(vg, 50, 495, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "App-ID: 0%lX", e.app_id);
|
||||
gfx::drawTextArgs(vg, 50, 535, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Key-Gen: %u (%s)", e.key_gen, nca::GetKeyGenStr(e.key_gen));
|
||||
gfx::drawTextArgs(vg, 50, 575, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Size: %.2f GB", (double)size / 0x40000000);
|
||||
gfx::drawTextArgs(vg, 50, 615, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Base: %zu Patch: %zu Addon: %zu Data: %zu", e.application.size(), e.patch.size(), e.add_on.size(), e.data_patch.size());
|
||||
nvgRestore(vg);
|
||||
}
|
||||
|
||||
m_list->Draw(vg, theme, std::size(g_option_list), [this](auto* vg, auto* theme, auto v, auto i) {
|
||||
const auto& [x, y, w, h] = v;
|
||||
const auto text_y = y + (h / 2.f);
|
||||
auto colour = ThemeEntryID_TEXT;
|
||||
if (i == m_option_index) {
|
||||
gfx::drawRectOutline(vg, theme, 4.f, v);
|
||||
// g_background.selected_bar = create_shape(Colour_Nintendo_Cyan, 90, 230, 4, 45, true);
|
||||
// draw_shape_position(&g_background.selected_bar, 485, g_options[i].text->rect.y - 10);
|
||||
gfx::drawRect(vg, 490, text_y - 45.f / 2.f, 2, 45, theme->GetColour(ThemeEntryID_TEXT_SELECTED));
|
||||
colour = ThemeEntryID_TEXT_SELECTED;
|
||||
}
|
||||
if (i != 2 && !m_mounted) {
|
||||
colour = ThemeEntryID_TEXT_INFO;
|
||||
}
|
||||
|
||||
gfx::drawTextArgs(vg, x + 15, y + (h / 2.f), 23.f, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(colour), "%s", g_option_list[i]);
|
||||
});
|
||||
}
|
||||
|
||||
Result Menu::GcMount() {
|
||||
GcUnmount();
|
||||
|
||||
R_TRY(fsDeviceOperatorGetGameCardHandle(std::addressof(m_dev_op), std::addressof(m_handle)));
|
||||
|
||||
m_fs = std::make_unique<fs::FsNativeGameCard>(std::addressof(m_handle), FsGameCardPartition_Secure, false);
|
||||
R_TRY(m_fs->GetFsOpenResult());
|
||||
|
||||
FsDir dir;
|
||||
R_TRY(m_fs->OpenDirectory("/", FsDirOpenMode_ReadFiles, std::addressof(dir)));
|
||||
ON_SCOPE_EXIT(fsDirClose(std::addressof(dir)));
|
||||
|
||||
s64 count;
|
||||
R_TRY(m_fs->DirGetEntryCount(std::addressof(dir), std::addressof(count)));
|
||||
|
||||
std::vector<FsDirectoryEntry> buf(count);
|
||||
s64 total_entries;
|
||||
R_TRY(m_fs->DirRead(std::addressof(dir), std::addressof(total_entries), buf.size(), buf.data()));
|
||||
R_UNLESS(buf.size() == total_entries, 0x1);
|
||||
|
||||
yati::container::Collections ticket_collections;
|
||||
for (const auto& e : buf) {
|
||||
if (!std::string_view(e.name).ends_with(".tik") && !std::string_view(e.name).ends_with(".cert")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ticket_collections.emplace_back(e.name, 0, e.file_size);
|
||||
}
|
||||
|
||||
for (const auto& e : buf) {
|
||||
// we could use ncm to handle finding all the ncas for us
|
||||
// however, we can parse faster than ncm.
|
||||
// not only that, the first few calls trying to mount ncm db for
|
||||
// the gamecard will fail as it has not yet been parsed (or it's locked?).
|
||||
// we could, of course, just wait until ncm is ready, which is about
|
||||
// 32ms, but i already have code for manually parsing cnmt so lets re-use it.
|
||||
if (!std::string_view(e.name).ends_with(".cnmt.nca")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// we don't yet use the header or extended header.
|
||||
ncm::PackagedContentMeta header;
|
||||
std::vector<u8> extended_header;
|
||||
std::vector<NcmPackagedContentInfo> infos;
|
||||
const auto path = BuildGcPath(e.name, &m_handle);
|
||||
R_TRY(yati::ParseCnmtNca(path, header, extended_header, infos));
|
||||
|
||||
u8 key_gen;
|
||||
FsRightsId rights_id;
|
||||
R_TRY(fsGetRightsIdAndKeyGenerationByPath(path, FsContentAttributes_All, &key_gen, &rights_id));
|
||||
|
||||
// always add tickets, yati will ignore them if not needed.
|
||||
GcCollections collections;
|
||||
// add cnmt file.
|
||||
collections.emplace_back(e.name, e.file_size, NcmContentType_Meta);
|
||||
|
||||
for (const auto& info : infos) {
|
||||
// these don't exist for gamecards, however i may copy/paste this code
|
||||
// somewhere so i'm future proofing against myself.
|
||||
if (info.info.content_type == NcmContentType_DeltaFragment) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// find the nca file, this will never fail for gamecards, see above comment.
|
||||
const auto str = hexIdToStr(info.info.content_id);
|
||||
const auto it = std::find_if(buf.cbegin(), buf.cend(), [str](auto& e){
|
||||
return !std::strncmp(str.str, e.name, std::strlen(str.str));
|
||||
});
|
||||
|
||||
R_UNLESS(it != buf.cend(), yati::Result_NcaNotFound);
|
||||
collections.emplace_back(it->name, it->file_size, info.info.content_type);
|
||||
}
|
||||
|
||||
const auto app_id = ncm::GetAppId(header);
|
||||
ApplicationEntry* app_entry{};
|
||||
for (auto& app : m_entries) {
|
||||
if (app.app_id == app_id) {
|
||||
app_entry = &app;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!app_entry) {
|
||||
app_entry = &m_entries.emplace_back(app_id, header.title_version);
|
||||
}
|
||||
|
||||
app_entry->version = std::max(app_entry->version, header.title_version);
|
||||
app_entry->key_gen = std::max(app_entry->key_gen, key_gen);
|
||||
|
||||
if (header.meta_type == NcmContentMetaType_Application) {
|
||||
app_entry->application.emplace_back(collections);
|
||||
} else if (header.meta_type == NcmContentMetaType_Patch) {
|
||||
app_entry->patch.emplace_back(collections);
|
||||
} else if (header.meta_type == NcmContentMetaType_AddOnContent) {
|
||||
app_entry->add_on.emplace_back(collections);
|
||||
} else if (header.meta_type == NcmContentMetaType_DataPatch) {
|
||||
app_entry->data_patch.emplace_back(collections);
|
||||
}
|
||||
}
|
||||
|
||||
R_UNLESS(m_entries.size(), 0x1);
|
||||
|
||||
// append tickets to every application, yati will ignore if undeeded.
|
||||
for (auto& e : m_entries) {
|
||||
e.tickets = ticket_collections;
|
||||
}
|
||||
|
||||
SetAction(Button::A, Action{"OK"_i18n, [this](){
|
||||
if (m_option_index == 2) {
|
||||
SetPop();
|
||||
} else {
|
||||
if (m_mounted) {
|
||||
App::Push(std::make_shared<ui::ProgressBox>(m_icon, "Installing "_i18n, m_lang_entry.name, [this](auto pbox) mutable -> bool {
|
||||
auto source = std::make_shared<GcSource>(m_entries[m_entry_index], m_fs.get(), m_option_index == 1);
|
||||
return R_SUCCEEDED(yati::InstallFromCollections(pbox, source, source->m_collections, source->m_config));
|
||||
}, [this](bool result){
|
||||
if (result) {
|
||||
App::Notify("Gc install success!"_i18n);
|
||||
} else {
|
||||
App::Notify("Gc install failed!"_i18n);
|
||||
}
|
||||
|
||||
UpdateStorageSize();
|
||||
}));
|
||||
}
|
||||
}
|
||||
}});
|
||||
|
||||
if (m_entries.size() > 1) {
|
||||
SetAction(Button::L, Action{"Prev"_i18n, [this](){
|
||||
if (m_entry_index != 0) {
|
||||
OnChangeIndex(m_entry_index - 1);
|
||||
}
|
||||
}});
|
||||
SetAction(Button::R, Action{"Next"_i18n, [this](){
|
||||
if (m_entry_index < m_entries.size()) {
|
||||
OnChangeIndex(m_entry_index + 1);
|
||||
}
|
||||
}});
|
||||
}
|
||||
|
||||
OnChangeIndex(0);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
void Menu::GcUnmount() {
|
||||
m_fs.reset();
|
||||
m_entries.clear();
|
||||
m_entry_index = 0;
|
||||
m_mounted = false;
|
||||
m_lang_entry = {};
|
||||
FreeImage();
|
||||
|
||||
RemoveAction(Button::L);
|
||||
RemoveAction(Button::R);
|
||||
}
|
||||
|
||||
Result Menu::GcPoll(bool* inserted) {
|
||||
R_TRY(fsDeviceOperatorIsGameCardInserted(&m_dev_op, inserted));
|
||||
|
||||
// if the handle changed, re-mount the game card.
|
||||
if (*inserted && m_mounted) {
|
||||
FsGameCardHandle handle;
|
||||
R_TRY(fsDeviceOperatorGetGameCardHandle(std::addressof(m_dev_op), std::addressof(handle)));
|
||||
if (handle.value != m_handle.value) {
|
||||
R_TRY(GcMount());
|
||||
}
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Menu::UpdateStorageSize() {
|
||||
fs::FsNativeContentStorage fs_nand{FsContentStorageId_User};
|
||||
fs::FsNativeContentStorage fs_sd{FsContentStorageId_SdCard};
|
||||
|
||||
R_TRY(fs_sd.GetFreeSpace("/", &m_size_free_sd));
|
||||
R_TRY(fs_sd.GetTotalSpace("/", &m_size_total_sd));
|
||||
R_TRY(fs_nand.GetFreeSpace("/", &m_size_free_nand));
|
||||
R_TRY(fs_nand.GetTotalSpace("/", &m_size_total_nand));
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
void Menu::FreeImage() {
|
||||
if (m_icon) {
|
||||
nvgDeleteImage(App::GetVg(), m_icon);
|
||||
m_icon = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::OnChangeIndex(s64 new_index) {
|
||||
FreeImage();
|
||||
m_entry_index = new_index;
|
||||
|
||||
const auto index = m_entries.empty() ? 0 : m_entry_index + 1;
|
||||
this->SetSubHeading(std::to_string(index) + " / " + std::to_string(m_entries.size()));
|
||||
|
||||
// nsGetApplicationControlData() will fail if it's the first time
|
||||
// mounting a gamecard if the image is not already cached.
|
||||
// waiting 1-2s after mount, then calling seems to work.
|
||||
// however, we can just manually parse the nca to get the data we need,
|
||||
// which always works and *is* faster too ;)
|
||||
for (auto& e : m_entries[m_entry_index].application) {
|
||||
for (auto& collection : e) {
|
||||
if (collection.type == NcmContentType_Control) {
|
||||
NacpStruct nacp;
|
||||
std::vector<u8> icon;
|
||||
const auto path = BuildGcPath(collection.name.c_str(), &m_handle);
|
||||
if (R_SUCCEEDED(yati::ParseControlNca(path, m_entries[m_entry_index].app_id, &nacp, sizeof(nacp), &icon))) {
|
||||
log_write("managed to parse control nca %s\n", path.s);
|
||||
NacpLanguageEntry* lang_entry{};
|
||||
nacpGetLanguageEntry(&nacp, &lang_entry);
|
||||
|
||||
if (lang_entry) {
|
||||
m_lang_entry = *lang_entry;
|
||||
}
|
||||
|
||||
m_icon = nvgCreateImageMem(App::GetVg(), 0, icon.data(), icon.size());
|
||||
if (m_icon > 0) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
log_write("\tFAILED to parse control nca %s\n", path.s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui::menu::gc
|
||||
@@ -247,27 +247,6 @@ Menu::Menu() : MenuBase{"GitHub"_i18n} {
|
||||
fs::FsNativeSd().CreateDirectoryRecursively(CACHE_PATH);
|
||||
|
||||
this->SetActions(
|
||||
std::make_pair(Button::DOWN, Action{[this](){
|
||||
if (m_list->ScrollDown(m_index, 1, m_entries.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::UP, Action{[this](){
|
||||
if (m_list->ScrollUp(m_index, 1, m_entries.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::DPAD_RIGHT, Action{[this](){
|
||||
if (m_list->ScrollDown(m_index, 8, m_entries.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::DPAD_LEFT, Action{[this](){
|
||||
if (m_list->ScrollUp(m_index, 8, m_entries.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
|
||||
std::make_pair(Button::A, Action{"Download"_i18n, [this](){
|
||||
if (m_entries.empty()) {
|
||||
return;
|
||||
@@ -277,7 +256,7 @@ Menu::Menu() : MenuBase{"GitHub"_i18n} {
|
||||
static GhApiEntry gh_entry;
|
||||
gh_entry = {};
|
||||
|
||||
App::Push(std::make_shared<ProgressBox>("Downloading "_i18n + GetEntry().repo, [this](auto pbox){
|
||||
App::Push(std::make_shared<ProgressBox>(0, "Downloading "_i18n, GetEntry().repo, [this](auto pbox){
|
||||
return DownloadAssetJson(pbox, GenerateApiUrl(GetEntry()), gh_entry);
|
||||
}, [this](bool success){
|
||||
if (success) {
|
||||
@@ -325,7 +304,7 @@ Menu::Menu() : MenuBase{"GitHub"_i18n} {
|
||||
}
|
||||
|
||||
const auto func = [this, &asset_entry, ptr](){
|
||||
App::Push(std::make_shared<ProgressBox>("Downloading "_i18n + GetEntry().repo, [this, &asset_entry, ptr](auto pbox){
|
||||
App::Push(std::make_shared<ProgressBox>(0, "Downloading "_i18n, GetEntry().repo, [this, &asset_entry, ptr](auto pbox){
|
||||
return DownloadApp(pbox, asset_entry, ptr);
|
||||
}, [this, ptr](bool success){
|
||||
if (success) {
|
||||
@@ -373,8 +352,8 @@ Menu::~Menu() {
|
||||
|
||||
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
MenuBase::Update(controller, touch);
|
||||
m_list->OnUpdate(controller, touch, m_entries.size(), [this](auto i) {
|
||||
if (m_index == i) {
|
||||
m_list->OnUpdate(controller, touch, m_index, m_entries.size(), [this](bool touch, auto i) {
|
||||
if (touch && m_index == i) {
|
||||
FireAction(Button::A);
|
||||
} else {
|
||||
App::PlaySoundEffect(SoundEffect_Focus);
|
||||
|
||||
@@ -28,40 +28,6 @@ auto GenerateStarPath(const fs::FsPath& nro_path) -> fs::FsPath {
|
||||
|
||||
Menu::Menu() : MenuBase{"Homebrew"_i18n} {
|
||||
this->SetActions(
|
||||
std::make_pair(Button::RIGHT, Action{[this](){
|
||||
if (m_index < (m_entries.size() - 1) && (m_index + 1) % 3 != 0) {
|
||||
SetIndex(m_index + 1);
|
||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
||||
log_write("moved right\n");
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::LEFT, Action{[this](){
|
||||
if (m_index != 0 && (m_index % 3) != 0) {
|
||||
SetIndex(m_index - 1);
|
||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
||||
log_write("moved left\n");
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::DOWN, Action{[this](){
|
||||
if (m_list->ScrollDown(m_index, 3, m_entries.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::UP, Action{[this](){
|
||||
if (m_list->ScrollUp(m_index, 3, m_entries.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::R2, Action{[this](){
|
||||
if (m_list->ScrollDown(m_index, 9, m_entries.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::L2, Action{[this](){
|
||||
if (m_list->ScrollUp(m_index, 9, m_entries.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::A, Action{"Launch"_i18n, [this](){
|
||||
nro_launch(m_entries[m_index].path);
|
||||
}}),
|
||||
@@ -157,8 +123,8 @@ Menu::~Menu() {
|
||||
|
||||
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
MenuBase::Update(controller, touch);
|
||||
m_list->OnUpdate(controller, touch, m_entries.size(), [this](auto i) {
|
||||
if (m_index == i) {
|
||||
m_list->OnUpdate(controller, touch, m_index, m_entries.size(), [this](bool touch, auto i) {
|
||||
if (touch && m_index == i) {
|
||||
FireAction(Button::A);
|
||||
} else {
|
||||
App::PlaySoundEffect(SoundEffect_Focus);
|
||||
@@ -202,7 +168,7 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
}
|
||||
|
||||
const float image_size = 115;
|
||||
gfx::drawImageRounded(vg, x + 20, y + 20, image_size, image_size, e.image ? e.image : App::GetDefaultImage());
|
||||
gfx::drawImage(vg, x + 20, y + 20, image_size, image_size, e.image ? e.image : App::GetDefaultImage(), 15);
|
||||
|
||||
nvgSave(vg);
|
||||
nvgIntersectScissor(vg, x, y, w - 30.f, h); // clip
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
#include "ui/menus/main_menu.hpp"
|
||||
#include "ui/menus/irs_menu.hpp"
|
||||
#include "ui/menus/themezer.hpp"
|
||||
#include "ui/menus/ghdl.hpp"
|
||||
|
||||
#include "ui/sidebar.hpp"
|
||||
#include "ui/popup_list.hpp"
|
||||
@@ -13,7 +10,6 @@
|
||||
#include "log.hpp"
|
||||
#include "download.hpp"
|
||||
#include "defines.hpp"
|
||||
#include "web.hpp"
|
||||
#include "i18n.hpp"
|
||||
|
||||
#include <cstring>
|
||||
@@ -232,52 +228,29 @@ MainMenu::MainMenu() {
|
||||
language_items.push_back("Swedish"_i18n);
|
||||
language_items.push_back("Vietnamese"_i18n);
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Theme"_i18n, [this](){
|
||||
SidebarEntryArray::Items theme_items{};
|
||||
const auto theme_meta = App::GetThemeMetaList();
|
||||
for (auto& p : theme_meta) {
|
||||
theme_items.emplace_back(p.name);
|
||||
}
|
||||
|
||||
auto options = std::make_shared<Sidebar>("Theme Options"_i18n, Sidebar::Side::LEFT);
|
||||
ON_SCOPE_EXIT(App::Push(options));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Select Theme"_i18n, theme_items, [this, theme_items](s64& index_out){
|
||||
App::SetTheme(index_out);
|
||||
}, App::GetThemeIndex()));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Shuffle"_i18n, App::GetThemeShuffleEnable(), [this](bool& enable){
|
||||
App::SetThemeShuffleEnable(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Music"_i18n, App::GetThemeMusicEnable(), [this](bool& enable){
|
||||
App::SetThemeMusicEnable(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("12 Hour Time"_i18n, App::Get12HourTimeEnable(), [this](bool& enable){
|
||||
App::Set12HourTimeEnable(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Theme"_i18n, [](){
|
||||
App::DisplayThemeOptions();
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Network"_i18n, [this](){
|
||||
auto options = std::make_shared<Sidebar>("Network Options"_i18n, Sidebar::Side::LEFT);
|
||||
ON_SCOPE_EXIT(App::Push(options));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Ftp"_i18n, App::GetFtpEnable(), [this](bool& enable){
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Ftp"_i18n, App::GetFtpEnable(), [](bool& enable){
|
||||
App::SetFtpEnable(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Mtp"_i18n, App::GetMtpEnable(), [this](bool& enable){
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Mtp"_i18n, App::GetMtpEnable(), [](bool& enable){
|
||||
App::SetMtpEnable(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Nxlink"_i18n, App::GetNxlinkEnable(), [this](bool& enable){
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Nxlink"_i18n, App::GetNxlinkEnable(), [](bool& enable){
|
||||
App::SetNxlinkEnable(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
if (m_update_state == UpdateState::Update) {
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Download update: "_i18n + m_update_version, [this](){
|
||||
App::Push(std::make_shared<ProgressBox>("Downloading "_i18n + m_update_version, [this](auto pbox){
|
||||
App::Push(std::make_shared<ProgressBox>(0, "Downloading "_i18n, "Sphaira v" + m_update_version, [this](auto pbox){
|
||||
return InstallUpdate(pbox, m_update_url, m_update_version);
|
||||
}, [this](bool success){
|
||||
if (success) {
|
||||
@@ -296,69 +269,16 @@ MainMenu::MainMenu() {
|
||||
}
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Language"_i18n, language_items, [this](s64& index_out){
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Language"_i18n, language_items, [](s64& index_out){
|
||||
App::SetLanguage(index_out);
|
||||
}, (s64)App::GetLanguage()));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Misc"_i18n, [this](){
|
||||
auto options = std::make_shared<Sidebar>("Misc Options"_i18n, Sidebar::Side::LEFT);
|
||||
ON_SCOPE_EXIT(App::Push(options));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Themezer"_i18n, [](){
|
||||
App::Push(std::make_shared<menu::themezer::Menu>());
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("GitHub"_i18n, [](){
|
||||
App::Push(std::make_shared<menu::gh::Menu>());
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Irs"_i18n, [](){
|
||||
App::Push(std::make_shared<menu::irs::Menu>());
|
||||
}));
|
||||
|
||||
if (App::IsApplication()) {
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Web"_i18n, [](){
|
||||
WebShow("https://lite.duckduckgo.com/lite");
|
||||
}));
|
||||
}
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Misc"_i18n, [](){
|
||||
App::DisplayMiscOptions();
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Advanced"_i18n, [this](){
|
||||
auto options = std::make_shared<Sidebar>("Advanced Options"_i18n, Sidebar::Side::LEFT);
|
||||
ON_SCOPE_EXIT(App::Push(options));
|
||||
|
||||
SidebarEntryArray::Items install_items;
|
||||
install_items.push_back("System memory"_i18n);
|
||||
install_items.push_back("microSD card"_i18n);
|
||||
|
||||
SidebarEntryArray::Items text_scroll_speed_items;
|
||||
text_scroll_speed_items.push_back("Slow"_i18n);
|
||||
text_scroll_speed_items.push_back("Normal"_i18n);
|
||||
text_scroll_speed_items.push_back("Fast"_i18n);
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Logging"_i18n, App::GetLogEnable(), [this](bool& enable){
|
||||
App::SetLogEnable(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Replace hbmenu on exit"_i18n, App::GetReplaceHbmenuEnable(), [this](bool& enable){
|
||||
App::SetReplaceHbmenuEnable(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Install forwarders"_i18n, App::GetInstallEnable(), [this](bool& enable){
|
||||
App::SetInstallEnable(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Install location"_i18n, install_items, [this](s64& index_out){
|
||||
App::SetInstallSdEnable(index_out);
|
||||
}, (s64)App::GetInstallSdEnable()));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Show install warning"_i18n, App::GetInstallPrompt(), [this](bool& enable){
|
||||
App::SetInstallPrompt(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Text scroll speed"_i18n, text_scroll_speed_items, [this](s64& index_out){
|
||||
App::SetTextScrollSpeed(index_out);
|
||||
}, (s64)App::GetTextScrollSpeed()));
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Advanced"_i18n, [](){
|
||||
App::DisplayAdvancedOptions();
|
||||
}));
|
||||
}})
|
||||
);
|
||||
|
||||
@@ -18,17 +18,18 @@ MenuBase::~MenuBase() {
|
||||
|
||||
void MenuBase::Update(Controller* controller, TouchInfo* touch) {
|
||||
Widget::Update(controller, touch);
|
||||
|
||||
// update every second.
|
||||
if (m_poll_timestamp.GetSeconds() >= 1) {
|
||||
UpdateVars();
|
||||
}
|
||||
}
|
||||
|
||||
void MenuBase::Draw(NVGcontext* vg, Theme* theme) {
|
||||
DrawElement(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, ThemeEntryID_BACKGROUND);
|
||||
Widget::Draw(vg, theme);
|
||||
|
||||
// update every second, do this in Draw because Update() isn't called if it
|
||||
// doesn't have focus.
|
||||
if (m_poll_timestamp.GetSeconds() >= 1) {
|
||||
UpdateVars();
|
||||
}
|
||||
|
||||
const float start_y = 70;
|
||||
const float font_size = 22;
|
||||
const float spacing = 30;
|
||||
|
||||
@@ -365,49 +365,16 @@ Menu::Menu() : MenuBase{"Themezer"_i18n} {
|
||||
fs::FsNativeSd().CreateDirectoryRecursively(CACHE_PATH);
|
||||
|
||||
SetAction(Button::B, Action{"Back"_i18n, [this]{
|
||||
SetPop();
|
||||
// if search is valid, then we are in search mode, return back to normal.
|
||||
if (!m_search.empty()) {
|
||||
m_search.clear();
|
||||
InvalidateAllPages();
|
||||
} else {
|
||||
SetPop();
|
||||
}
|
||||
}});
|
||||
|
||||
this->SetActions(
|
||||
std::make_pair(Button::RIGHT, Action{[this](){
|
||||
const auto& page = m_pages[m_page_index];
|
||||
if (m_index < (page.m_packList.size() - 1) && (m_index + 1) % 3 != 0) {
|
||||
SetIndex(m_index + 1);
|
||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
||||
log_write("moved right\n");
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::LEFT, Action{[this](){
|
||||
if (m_index != 0 && (m_index % 3) != 0) {
|
||||
SetIndex(m_index - 1);
|
||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
||||
log_write("moved left\n");
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::DOWN, Action{[this](){
|
||||
const auto& page = m_pages[m_page_index];
|
||||
if (m_list->ScrollDown(m_index, 3, page.m_packList.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::UP, Action{[this](){
|
||||
const auto& page = m_pages[m_page_index];
|
||||
if (m_list->ScrollUp(m_index, 3, page.m_packList.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::R2, Action{[this](){
|
||||
const auto& page = m_pages[m_page_index];
|
||||
if (m_list->ScrollDown(m_index, 6, page.m_packList.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::L2, Action{[this](){
|
||||
const auto& page = m_pages[m_page_index];
|
||||
if (m_list->ScrollUp(m_index, 6, page.m_packList.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::A, Action{"Download"_i18n, [this](){
|
||||
App::Push(std::make_shared<OptionBox>(
|
||||
"Download theme?"_i18n,
|
||||
@@ -418,7 +385,7 @@ Menu::Menu() : MenuBase{"Themezer"_i18n} {
|
||||
const auto& entry = page.m_packList[m_index];
|
||||
const auto url = apiBuildUrlDownloadPack(entry);
|
||||
|
||||
App::Push(std::make_shared<ProgressBox>("Installing "_i18n + entry.details.name, [this, &entry](auto pbox){
|
||||
App::Push(std::make_shared<ProgressBox>(entry.themes[0].preview.lazy_image.image, "Downloading "_i18n, entry.details.name, [this, &entry](auto pbox){
|
||||
return InstallTheme(pbox, entry);
|
||||
}, [this, &entry](bool success){
|
||||
if (success) {
|
||||
@@ -480,6 +447,8 @@ Menu::Menu() : MenuBase{"Themezer"_i18n} {
|
||||
std::string out;
|
||||
if (R_SUCCEEDED(swkbd::ShowText(out)) && !out.empty()) {
|
||||
m_search = out;
|
||||
// PackListDownload();
|
||||
InvalidateAllPages();
|
||||
}
|
||||
}));
|
||||
}}),
|
||||
@@ -524,8 +493,8 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_list->OnUpdate(controller, touch, page.m_packList.size(), [this](auto i) {
|
||||
if (m_index == i) {
|
||||
m_list->OnUpdate(controller, touch, m_index, page.m_packList.size(), [this](bool touch, auto i) {
|
||||
if (touch && m_index == i) {
|
||||
FireAction(Button::A);
|
||||
} else {
|
||||
App::PlaySoundEffect(SoundEffect_Focus);
|
||||
@@ -634,7 +603,7 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
}
|
||||
}
|
||||
|
||||
gfx::drawImageRounded(vg, x + xoff, y, 320, 180, image.image ? image.image : App::GetDefaultImage());
|
||||
gfx::drawImage(vg, x + xoff, y, 320, 180, image.image ? image.image : App::GetDefaultImage(), 15);
|
||||
}
|
||||
|
||||
nvgSave(vg);
|
||||
@@ -652,11 +621,9 @@ void Menu::OnFocusGained() {
|
||||
}
|
||||
|
||||
void Menu::InvalidateAllPages() {
|
||||
for (auto& e : m_pages) {
|
||||
e.m_packList.clear();
|
||||
e.m_ready = PageLoadState::None;
|
||||
}
|
||||
|
||||
m_pages.clear();
|
||||
m_pages.resize(1);
|
||||
m_page_index = 0;
|
||||
PackListDownload();
|
||||
}
|
||||
|
||||
|
||||
181
sphaira/source/ui/menus/usb_menu.cpp
Normal file
181
sphaira/source/ui/menus/usb_menu.cpp
Normal file
@@ -0,0 +1,181 @@
|
||||
#include "ui/menus/usb_menu.hpp"
|
||||
#include "yati/yati.hpp"
|
||||
#include "app.hpp"
|
||||
#include "defines.hpp"
|
||||
#include "log.hpp"
|
||||
#include "ui/nvg_util.hpp"
|
||||
#include "i18n.hpp"
|
||||
#include <cstring>
|
||||
|
||||
namespace sphaira::ui::menu::usb {
|
||||
namespace {
|
||||
|
||||
constexpr u64 CONNECTION_TIMEOUT = 1e+9 * 3;
|
||||
constexpr u64 TRANSFER_TIMEOUT = 1e+9 * 5;
|
||||
|
||||
void thread_func(void* user) {
|
||||
auto app = static_cast<Menu*>(user);
|
||||
|
||||
for (;;) {
|
||||
if (app->GetToken().stop_requested()) {
|
||||
break;
|
||||
}
|
||||
|
||||
const auto rc = app->m_usb_source->WaitForConnection(CONNECTION_TIMEOUT, app->m_usb_speed, app->m_usb_count);
|
||||
mutexLock(&app->m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&app->m_mutex));
|
||||
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
app->m_state = State::Connected;
|
||||
break;
|
||||
} else if (R_FAILED(rc) && R_VALUE(rc) != 0xEA01) {
|
||||
log_write("got: 0x%X value: 0x%X\n", rc, R_VALUE(rc));
|
||||
app->m_state = State::Failed;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Menu::Menu() : MenuBase{"USB"_i18n} {
|
||||
SetAction(Button::B, Action{"Back"_i18n, [this](){
|
||||
SetPop();
|
||||
}});
|
||||
|
||||
SetAction(Button::X, Action{"Options"_i18n, [this](){
|
||||
App::DisplayInstallOptions(false);
|
||||
}});
|
||||
|
||||
// if mtp is enabled, disable it for now.
|
||||
m_was_mtp_enabled = App::GetMtpEnable();
|
||||
if (m_was_mtp_enabled) {
|
||||
App::Notify("Disable MTP for usb install"_i18n);
|
||||
App::SetMtpEnable(false);
|
||||
}
|
||||
|
||||
// 3 second timeout for transfers.
|
||||
m_usb_source = std::make_shared<yati::source::Usb>(TRANSFER_TIMEOUT);
|
||||
if (R_FAILED(m_usb_source->GetOpenResult())) {
|
||||
log_write("usb init open\n");
|
||||
m_state = State::Failed;
|
||||
} else {
|
||||
if (R_FAILED(m_usb_source->Init())) {
|
||||
log_write("usb init failed\n");
|
||||
m_state = State::Failed;
|
||||
}
|
||||
}
|
||||
|
||||
mutexInit(&m_mutex);
|
||||
if (m_state != State::Failed) {
|
||||
threadCreate(&m_thread, thread_func, this, nullptr, 1024*32, 0x2C, 1);
|
||||
threadStart(&m_thread);
|
||||
}
|
||||
}
|
||||
|
||||
Menu::~Menu() {
|
||||
// signal for thread to exit and wait.
|
||||
m_stop_source.request_stop();
|
||||
threadWaitForExit(&m_thread);
|
||||
threadClose(&m_thread);
|
||||
|
||||
// free usb source before re-enabling mtp.
|
||||
log_write("closing data!!!!\n");
|
||||
m_usb_source.reset();
|
||||
|
||||
if (m_was_mtp_enabled) {
|
||||
App::Notify("Re-enabled MTP"_i18n);
|
||||
App::SetMtpEnable(true);
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
MenuBase::Update(controller, touch);
|
||||
|
||||
mutexLock(&m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||
|
||||
switch (m_state) {
|
||||
case State::None:
|
||||
break;
|
||||
|
||||
case State::Connected:
|
||||
log_write("set to progress\n");
|
||||
m_state = State::Progress;
|
||||
log_write("got connection\n");
|
||||
App::Push(std::make_shared<ui::ProgressBox>(0, "Installing "_i18n, "", [this](auto pbox) mutable -> bool {
|
||||
log_write("inside progress box\n");
|
||||
for (u32 i = 0; i < m_usb_count; i++) {
|
||||
std::string file_name;
|
||||
u64 file_size;
|
||||
if (R_FAILED(m_usb_source->GetFileInfo(file_name, file_size))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
log_write("got file name: %s size: %lX\n", file_name.c_str(), file_size);
|
||||
|
||||
const auto rc = yati::InstallFromSource(pbox, m_usb_source, file_name);
|
||||
if (R_FAILED(rc)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
App::Notify("Installed via usb"_i18n);
|
||||
m_usb_source->Finished();
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [this](bool result){
|
||||
if (result) {
|
||||
App::Notify("Usb install success!"_i18n);
|
||||
m_state = State::Done;
|
||||
} else {
|
||||
App::Notify("Usb install failed!"_i18n);
|
||||
m_state = State::Failed;
|
||||
}
|
||||
}));
|
||||
break;
|
||||
|
||||
case State::Progress:
|
||||
break;
|
||||
|
||||
case State::Done:
|
||||
break;
|
||||
|
||||
case State::Failed:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
MenuBase::Draw(vg, theme);
|
||||
|
||||
mutexLock(&m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||
|
||||
switch (m_state) {
|
||||
case State::None:
|
||||
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Waiting for connection..."_i18n.c_str());
|
||||
break;
|
||||
|
||||
case State::Connected:
|
||||
break;
|
||||
|
||||
case State::Progress:
|
||||
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Transferring data..."_i18n.c_str());
|
||||
break;
|
||||
|
||||
case State::Done:
|
||||
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Press B to exit..."_i18n.c_str());
|
||||
break;
|
||||
|
||||
case State::Failed:
|
||||
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Failed to init usb, press B to exit..."_i18n.c_str());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::OnFocusGained() {
|
||||
MenuBase::OnFocusGained();
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui::menu::usb
|
||||
@@ -34,24 +34,16 @@ constexpr std::array buttons = {
|
||||
};
|
||||
|
||||
// NEW ---------------------
|
||||
void drawRectIntenal(NVGcontext* vg, const Vec4& v, const NVGcolor& c, bool rounded) {
|
||||
void drawRectIntenal(NVGcontext* vg, const Vec4& v, const NVGcolor& c, float rounded) {
|
||||
nvgBeginPath(vg);
|
||||
if (rounded) {
|
||||
nvgRoundedRect(vg, v.x, v.y, v.w, v.h, 15);
|
||||
} else {
|
||||
nvgRect(vg, v.x, v.y, v.w, v.h);
|
||||
}
|
||||
nvgRoundedRect(vg, v.x, v.y, v.w, v.h, rounded);
|
||||
nvgFillColor(vg, c);
|
||||
nvgFill(vg);
|
||||
}
|
||||
|
||||
void drawRectIntenal(NVGcontext* vg, const Vec4& v, const NVGpaint& p, bool rounded) {
|
||||
void drawRectIntenal(NVGcontext* vg, const Vec4& v, const NVGpaint& p, float rounded) {
|
||||
nvgBeginPath(vg);
|
||||
if (rounded) {
|
||||
nvgRoundedRect(vg, v.x, v.y, v.w, v.h, 15);
|
||||
} else {
|
||||
nvgRect(vg, v.x, v.y, v.w, v.h);
|
||||
}
|
||||
nvgRoundedRect(vg, v.x, v.y, v.w, v.h, rounded);
|
||||
nvgFillPaint(vg, p);
|
||||
nvgFill(vg);
|
||||
}
|
||||
@@ -164,25 +156,13 @@ void drawTextArgs(NVGcontext* vg, float x, float y, float size, int align, const
|
||||
drawText(vg, x, y, size, buffer, nullptr, align, c);
|
||||
}
|
||||
|
||||
void drawImage(NVGcontext* vg, const Vec4& v, int texture) {
|
||||
void drawImage(NVGcontext* vg, const Vec4& v, int texture, float rounded) {
|
||||
const auto paint = nvgImagePattern(vg, v.x, v.y, v.w, v.h, 0, texture, 1.f);
|
||||
drawRect(vg, v, paint, false);
|
||||
drawRect(vg, v, paint, rounded);
|
||||
}
|
||||
|
||||
void drawImage(NVGcontext* vg, float x, float y, float w, float h, int texture) {
|
||||
drawImage(vg, Vec4(x, y, w, h), texture);
|
||||
}
|
||||
|
||||
void drawImageRounded(NVGcontext* vg, const Vec4& v, int texture) {
|
||||
const auto paint = nvgImagePattern(vg, v.x, v.y, v.w, v.h, 0, texture, 1.f);
|
||||
nvgBeginPath(vg);
|
||||
nvgRoundedRect(vg, v.x, v.y, v.w, v.h, 15);
|
||||
nvgFillPaint(vg, paint);
|
||||
nvgFill(vg);
|
||||
}
|
||||
|
||||
void drawImageRounded(NVGcontext* vg, float x, float y, float w, float h, int texture) {
|
||||
drawImageRounded(vg, Vec4(x, y, w, h), texture);
|
||||
void drawImage(NVGcontext* vg, float x, float y, float w, float h, int texture, float rounded) {
|
||||
drawImage(vg, Vec4(x, y, w, h), texture, rounded);
|
||||
}
|
||||
|
||||
void drawTextBox(NVGcontext* vg, float x, float y, float size, float bound, const NVGcolor& c, const char* str, int align, const char* end) {
|
||||
@@ -208,19 +188,19 @@ void dimBackground(NVGcontext* vg) {
|
||||
drawRectIntenal(vg, {0.f,0.f,SCREEN_WIDTH,SCREEN_HEIGHT}, nvgRGBA(0, 0, 0, 180), false);
|
||||
}
|
||||
|
||||
void drawRect(NVGcontext* vg, float x, float y, float w, float h, const NVGcolor& c, bool rounded) {
|
||||
void drawRect(NVGcontext* vg, float x, float y, float w, float h, const NVGcolor& c, float rounded) {
|
||||
drawRectIntenal(vg, {x,y,w,h}, c, rounded);
|
||||
}
|
||||
|
||||
void drawRect(NVGcontext* vg, const Vec4& v, const NVGcolor& c, bool rounded) {
|
||||
void drawRect(NVGcontext* vg, const Vec4& v, const NVGcolor& c, float rounded) {
|
||||
drawRectIntenal(vg, v, c, rounded);
|
||||
}
|
||||
|
||||
void drawRect(NVGcontext* vg, float x, float y, float w, float h, const NVGpaint& p, bool rounded) {
|
||||
void drawRect(NVGcontext* vg, float x, float y, float w, float h, const NVGpaint& p, float rounded) {
|
||||
drawRectIntenal(vg, {x,y,w,h}, p, rounded);
|
||||
}
|
||||
|
||||
void drawRect(NVGcontext* vg, const Vec4& v, const NVGpaint& p, bool rounded) {
|
||||
void drawRect(NVGcontext* vg, const Vec4& v, const NVGpaint& p, float rounded) {
|
||||
drawRectIntenal(vg, v, p, rounded);
|
||||
}
|
||||
|
||||
|
||||
@@ -62,16 +62,6 @@ PopupList::PopupList(std::string title, Items items, Callback cb, s64 index)
|
||||
, m_callback{cb}
|
||||
, m_index{index} {
|
||||
this->SetActions(
|
||||
std::make_pair(Button::DOWN, Action{[this](){
|
||||
if (m_list->ScrollDown(m_index, 1, m_items.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::UP, Action{[this](){
|
||||
if (m_list->ScrollUp(m_index, 1, m_items.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::A, Action{"Select"_i18n, [this](){
|
||||
if (m_callback) {
|
||||
m_callback(m_index);
|
||||
@@ -103,9 +93,11 @@ PopupList::PopupList(std::string title, Items items, Callback cb, s64 index)
|
||||
|
||||
auto PopupList::Update(Controller* controller, TouchInfo* touch) -> void {
|
||||
Widget::Update(controller, touch);
|
||||
m_list->OnUpdate(controller, touch, m_items.size(), [this](auto i) {
|
||||
m_list->OnUpdate(controller, touch, m_index, m_items.size(), [this](bool touch, auto i) {
|
||||
SetIndex(i);
|
||||
FireAction(Button::A);
|
||||
if (touch) {
|
||||
FireAction(Button::A);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "defines.hpp"
|
||||
#include "log.hpp"
|
||||
#include "i18n.hpp"
|
||||
#include <cstring>
|
||||
|
||||
namespace sphaira::ui {
|
||||
namespace {
|
||||
@@ -17,7 +18,7 @@ void threadFunc(void* arg) {
|
||||
|
||||
} // namespace
|
||||
|
||||
ProgressBox::ProgressBox(const std::string& title, ProgressBoxCallback callback, ProgressBoxDoneCallback done, int cpuid, int prio, int stack_size) {
|
||||
ProgressBox::ProgressBox(int image, const std::string& action, const std::string& title, ProgressBoxCallback callback, ProgressBoxDoneCallback done, int cpuid, int prio, int stack_size) {
|
||||
SetAction(Button::B, Action{"Back"_i18n, [this](){
|
||||
App::Push(std::make_shared<OptionBox>("Are you sure you wish to cancel?"_i18n, "No"_i18n, "Yes"_i18n, 1, [this](auto op_index){
|
||||
if (op_index && *op_index) {
|
||||
@@ -27,11 +28,6 @@ ProgressBox::ProgressBox(const std::string& title, ProgressBoxCallback callback,
|
||||
}));
|
||||
}});
|
||||
|
||||
m_pos.w = 770.f;
|
||||
m_pos.h = 430.f;
|
||||
m_pos.x = 255;
|
||||
m_pos.y = 145;
|
||||
|
||||
m_pos.w = 770.f;
|
||||
m_pos.h = 295.f;
|
||||
m_pos.x = (SCREEN_WIDTH / 2.f) - (m_pos.w / 2.f);
|
||||
@@ -39,6 +35,8 @@ ProgressBox::ProgressBox(const std::string& title, ProgressBoxCallback callback,
|
||||
|
||||
m_done = done;
|
||||
m_title = title;
|
||||
m_action = action;
|
||||
m_image = image;
|
||||
|
||||
m_thread_data.pbox = this;
|
||||
m_thread_data.callback = callback;
|
||||
@@ -60,6 +58,7 @@ ProgressBox::~ProgressBox() {
|
||||
log_write("failed to close thread\n");
|
||||
}
|
||||
|
||||
FreeImage();
|
||||
m_done(m_thread_data.result);
|
||||
}
|
||||
|
||||
@@ -73,12 +72,28 @@ auto ProgressBox::Update(Controller* controller, TouchInfo* touch) -> void {
|
||||
|
||||
auto ProgressBox::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||
mutexLock(&m_mutex);
|
||||
std::vector<u8> image_data{};
|
||||
std::swap(m_image_data, image_data);
|
||||
if (m_timestamp.GetSeconds()) {
|
||||
m_timestamp.Update();
|
||||
m_speed = m_offset - m_last_offset;
|
||||
m_last_offset = m_offset;
|
||||
}
|
||||
|
||||
const auto title = m_title;
|
||||
const auto transfer = m_transfer;
|
||||
const auto size = m_size;
|
||||
const auto offset = m_offset;
|
||||
const auto speed = m_speed;
|
||||
const auto last_offset = m_last_offset;
|
||||
mutexUnlock(&m_mutex);
|
||||
|
||||
if (!image_data.empty()) {
|
||||
FreeImage();
|
||||
m_image = nvgCreateImageMem(vg, 0, image_data.data(), image_data.size());
|
||||
m_own_image = true;
|
||||
}
|
||||
|
||||
gfx::dimBackground(vg);
|
||||
gfx::drawRect(vg, m_pos, theme->GetColour(ThemeEntryID_POPUP));
|
||||
|
||||
@@ -86,20 +101,70 @@ auto ProgressBox::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||
// const Vec4 box = { 255, 145, 770, 430 };
|
||||
const auto center_x = m_pos.x + m_pos.w/2;
|
||||
const auto end_y = m_pos.y + m_pos.h;
|
||||
const Vec4 prog_bar = { 400, end_y - 80, 480, 12 };
|
||||
const auto progress_bar_w = m_pos.w - 230;
|
||||
const Vec4 prog_bar = { center_x - progress_bar_w / 2, end_y - 100, progress_bar_w, 12 };
|
||||
|
||||
nvgSave(vg);
|
||||
nvgIntersectScissor(vg, GetX(), GetY(), GetW(), GetH());
|
||||
|
||||
if (m_image) {
|
||||
gfx::drawImage(vg, GetX() + 30, GetY() + 30, 128, 128, m_image, 10);
|
||||
}
|
||||
|
||||
// shapes.
|
||||
if (offset && size) {
|
||||
gfx::drawRect(vg, prog_bar, theme->GetColour(ThemeEntryID_PROGRESSBAR_BACKGROUND));
|
||||
const auto font_size = 18.F;
|
||||
const auto pad = 15.F;
|
||||
const float rounding = 5;
|
||||
|
||||
gfx::drawRect(vg, prog_bar, theme->GetColour(ThemeEntryID_PROGRESSBAR_BACKGROUND), rounding);
|
||||
const u32 percentage = ((double)offset / (double)size) * 100.0;
|
||||
gfx::drawRect(vg, prog_bar.x, prog_bar.y, ((float)offset / (float)size) * prog_bar.w, prog_bar.h, theme->GetColour(ThemeEntryID_PROGRESSBAR));
|
||||
gfx::drawTextArgs(vg, prog_bar.x + prog_bar.w + 10, prog_bar.y, 20, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%u%%", percentage);
|
||||
gfx::drawRect(vg, prog_bar.x, prog_bar.y, ((float)offset / (float)size) * prog_bar.w, prog_bar.h, theme->GetColour(ThemeEntryID_PROGRESSBAR), rounding);
|
||||
gfx::drawTextArgs(vg, prog_bar.x + prog_bar.w + pad, prog_bar.y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%u%%", percentage);
|
||||
|
||||
const double speed_mb = (double)speed / (1024.0 * 1024.0);
|
||||
const double speed_kb = (double)speed / (1024.0);
|
||||
|
||||
char speed_str[32];
|
||||
if (speed_mb >= 0.01) {
|
||||
std::snprintf(speed_str, sizeof(speed_str), "%.2f MiB/s", speed_mb);
|
||||
} else {
|
||||
std::snprintf(speed_str, sizeof(speed_str), "%.2f KiB/s", speed_kb);
|
||||
}
|
||||
|
||||
const auto left = size - last_offset;
|
||||
const auto left_seconds = left / speed;
|
||||
const auto hours = left_seconds / (60 * 60);
|
||||
const auto minutes = left_seconds % (60 * 60) / 60;
|
||||
const auto seconds = left_seconds % 60;
|
||||
|
||||
char time_str[64];
|
||||
if (hours) {
|
||||
std::snprintf(time_str, sizeof(time_str), "%zu hours %zu minutes remaining", hours, minutes);
|
||||
} else if (minutes) {
|
||||
std::snprintf(time_str, sizeof(time_str), "%zu minutes %zu seconds remaining", minutes, seconds);
|
||||
} else {
|
||||
std::snprintf(time_str, sizeof(time_str), "%zu seconds remaining", seconds);
|
||||
}
|
||||
|
||||
gfx::drawTextArgs(vg, center_x, prog_bar.y + prog_bar.h + 30, 18, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%s (%s)", time_str, speed_str);
|
||||
}
|
||||
|
||||
gfx::drawTextArgs(vg, center_x, m_pos.y + 60, 25, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), title.c_str());
|
||||
gfx::drawTextArgs(vg, center_x, m_pos.y + 40, 24, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), m_action.c_str());
|
||||
gfx::drawTextArgs(vg, center_x, m_pos.y + 100, 22, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), title.c_str());
|
||||
if (!transfer.empty()) {
|
||||
gfx::drawTextArgs(vg, center_x, prog_bar.y - 15 - 20 * 1.5F, 20, NVG_ALIGN_CENTER, theme->GetColour(ThemeEntryID_TEXT), "%s", transfer.c_str());
|
||||
gfx::drawTextArgs(vg, center_x, m_pos.y + 150, 18, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT_INFO), "%s", transfer.c_str());
|
||||
}
|
||||
|
||||
nvgRestore(vg);
|
||||
}
|
||||
|
||||
auto ProgressBox::SetTitle(const std::string& title) -> ProgressBox& {
|
||||
mutexLock(&m_mutex);
|
||||
m_title = title;
|
||||
mutexUnlock(&m_mutex);
|
||||
Yield();
|
||||
return *this;
|
||||
}
|
||||
|
||||
auto ProgressBox::NewTransfer(const std::string& transfer) -> ProgressBox& {
|
||||
@@ -107,6 +172,8 @@ auto ProgressBox::NewTransfer(const std::string& transfer) -> ProgressBox& {
|
||||
m_transfer = transfer;
|
||||
m_size = 0;
|
||||
m_offset = 0;
|
||||
m_last_offset = 0;
|
||||
m_timestamp.Update();
|
||||
mutexUnlock(&m_mutex);
|
||||
Yield();
|
||||
return *this;
|
||||
@@ -121,6 +188,21 @@ auto ProgressBox::UpdateTransfer(s64 offset, s64 size) -> ProgressBox& {
|
||||
return *this;
|
||||
}
|
||||
|
||||
auto ProgressBox::SetImageData(std::vector<u8>& data) -> ProgressBox& {
|
||||
mutexLock(&m_mutex);
|
||||
std::swap(m_image_data, data);
|
||||
mutexUnlock(&m_mutex);
|
||||
return *this;
|
||||
}
|
||||
|
||||
auto ProgressBox::SetImageDataConst(std::span<const u8> data) -> ProgressBox& {
|
||||
mutexLock(&m_mutex);
|
||||
m_image_data.resize(data.size());
|
||||
std::memcpy(m_image_data.data(), data.data(), m_image_data.size());
|
||||
mutexUnlock(&m_mutex);
|
||||
return *this;
|
||||
}
|
||||
|
||||
void ProgressBox::RequestExit() {
|
||||
m_stop_source.request_stop();
|
||||
}
|
||||
@@ -176,4 +258,13 @@ void ProgressBox::Yield() {
|
||||
svcSleepThread(YieldType_WithoutCoreMigration);
|
||||
}
|
||||
|
||||
void ProgressBox::FreeImage() {
|
||||
if (m_image && m_own_image) {
|
||||
nvgDeleteImage(App::GetVg(), m_image);
|
||||
}
|
||||
|
||||
m_image = 0;
|
||||
m_own_image = false;
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui
|
||||
|
||||
@@ -259,9 +259,11 @@ auto Sidebar::Update(Controller* controller, TouchInfo* touch) -> void {
|
||||
if (touch->is_clicked && !touch->in_range(GetPos())) {
|
||||
App::PopToMenu();
|
||||
} else {
|
||||
m_list->OnUpdate(controller, touch, m_items.size(), [this](auto i) {
|
||||
m_list->OnUpdate(controller, touch, m_index, m_items.size(), [this](bool touch, auto i) {
|
||||
SetIndex(i);
|
||||
FireAction(Button::A);
|
||||
if (touch) {
|
||||
FireAction(Button::A);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -334,18 +336,6 @@ void Sidebar::SetupButtons() {
|
||||
|
||||
// add default actions, overriding if needed.
|
||||
this->SetActions(
|
||||
std::make_pair(Button::DOWN, Action{[this](){
|
||||
auto index = m_index;
|
||||
if (m_list->ScrollDown(index, 1, m_items.size())) {
|
||||
SetIndex(index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::UP, Action{[this](){
|
||||
auto index = m_index;
|
||||
if (m_list->ScrollUp(index, 1, m_items.size())) {
|
||||
SetIndex(index);
|
||||
}
|
||||
}}),
|
||||
// each item has it's own Action, but we take over B
|
||||
std::make_pair(Button::B, Action{"Back"_i18n, [this](){
|
||||
SetPop();
|
||||
|
||||
59
sphaira/source/yati/container/nsp.cpp
Normal file
59
sphaira/source/yati/container/nsp.cpp
Normal file
@@ -0,0 +1,59 @@
|
||||
#include "yati/container/nsp.hpp"
|
||||
#include "defines.hpp"
|
||||
#include "log.hpp"
|
||||
#include <memory>
|
||||
|
||||
namespace sphaira::yati::container {
|
||||
namespace {
|
||||
|
||||
#define PFS0_MAGIC 0x30534650
|
||||
|
||||
struct Pfs0Header {
|
||||
u32 magic;
|
||||
u32 total_files;
|
||||
u32 string_table_size;
|
||||
u32 padding;
|
||||
};
|
||||
|
||||
struct Pfs0FileTableEntry {
|
||||
u64 data_offset;
|
||||
u64 data_size;
|
||||
u32 name_offset;
|
||||
u32 padding;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
Result Nsp::GetCollections(Collections& out) {
|
||||
u64 bytes_read;
|
||||
s64 off = 0;
|
||||
|
||||
// get header
|
||||
Pfs0Header header{};
|
||||
R_TRY(m_source->Read(std::addressof(header), off, sizeof(header), std::addressof(bytes_read)));
|
||||
R_UNLESS(header.magic == PFS0_MAGIC, 0x1);
|
||||
off += bytes_read;
|
||||
|
||||
// get file table
|
||||
std::vector<Pfs0FileTableEntry> file_table(header.total_files);
|
||||
R_TRY(m_source->Read(file_table.data(), off, file_table.size() * sizeof(Pfs0FileTableEntry), std::addressof(bytes_read)))
|
||||
off += bytes_read;
|
||||
|
||||
// get string table
|
||||
std::vector<char> string_table(header.string_table_size);
|
||||
R_TRY(m_source->Read(string_table.data(), off, string_table.size(), std::addressof(bytes_read)))
|
||||
off += bytes_read;
|
||||
|
||||
out.reserve(header.total_files);
|
||||
for (u32 i = 0; i < header.total_files; i++) {
|
||||
CollectionEntry entry;
|
||||
entry.name = string_table.data() + file_table[i].name_offset;
|
||||
entry.offset = off + file_table[i].data_offset;
|
||||
entry.size = file_table[i].data_size;
|
||||
out.emplace_back(entry);
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
} // namespace sphaira::yati::container
|
||||
87
sphaira/source/yati/container/xci.cpp
Normal file
87
sphaira/source/yati/container/xci.cpp
Normal file
@@ -0,0 +1,87 @@
|
||||
#include "yati/container/xci.hpp"
|
||||
#include "defines.hpp"
|
||||
#include "log.hpp"
|
||||
|
||||
namespace sphaira::yati::container {
|
||||
namespace {
|
||||
|
||||
#define XCI_MAGIC std::byteswap(0x48454144)
|
||||
#define HFS0_MAGIC 0x30534648
|
||||
#define HFS0_HEADER_OFFSET 0xF000
|
||||
|
||||
struct Hfs0Header {
|
||||
u32 magic;
|
||||
u32 total_files;
|
||||
u32 string_table_size;
|
||||
u32 padding;
|
||||
};
|
||||
|
||||
struct Hfs0FileTableEntry {
|
||||
u64 data_offset;
|
||||
u64 data_size;
|
||||
u32 name_offset;
|
||||
u32 hash_size;
|
||||
u64 padding;
|
||||
u8 hash[0x20];
|
||||
};
|
||||
|
||||
struct Hfs0 {
|
||||
Hfs0Header header{};
|
||||
std::vector<Hfs0FileTableEntry> file_table{};
|
||||
std::vector<std::string> string_table{};
|
||||
s64 data_offset{};
|
||||
};
|
||||
|
||||
Result Hfs0GetPartition(source::Base* source, s64 off, Hfs0& out) {
|
||||
u64 bytes_read;
|
||||
|
||||
// get header
|
||||
R_TRY(source->Read(std::addressof(out.header), off, sizeof(out.header), std::addressof(bytes_read)));
|
||||
R_UNLESS(out.header.magic == HFS0_MAGIC, 0x1);
|
||||
off += bytes_read;
|
||||
|
||||
// get file table
|
||||
out.file_table.resize(out.header.total_files);
|
||||
R_TRY(source->Read(out.file_table.data(), off, out.file_table.size() * sizeof(Hfs0FileTableEntry), std::addressof(bytes_read)))
|
||||
off += bytes_read;
|
||||
|
||||
// get string table
|
||||
std::vector<char> string_table(out.header.string_table_size);
|
||||
R_TRY(source->Read(string_table.data(), off, string_table.size(), std::addressof(bytes_read)))
|
||||
off += bytes_read;
|
||||
|
||||
for (u32 i = 0; i < out.header.total_files; i++) {
|
||||
out.string_table.emplace_back(string_table.data() + out.file_table[i].name_offset);
|
||||
}
|
||||
|
||||
out.data_offset = off;
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Result Xci::GetCollections(Collections& out) {
|
||||
Hfs0 root{};
|
||||
R_TRY(Hfs0GetPartition(m_source.get(), HFS0_HEADER_OFFSET, root));
|
||||
|
||||
for (u32 i = 0; i < root.header.total_files; i++) {
|
||||
if (root.string_table[i] == "secure") {
|
||||
Hfs0 secure{};
|
||||
R_TRY(Hfs0GetPartition(m_source.get(), root.data_offset + root.file_table[i].data_offset, secure));
|
||||
|
||||
for (u32 i = 0; i < secure.header.total_files; i++) {
|
||||
CollectionEntry entry;
|
||||
entry.name = secure.string_table[i];
|
||||
entry.offset = secure.data_offset + secure.file_table[i].data_offset;
|
||||
entry.size = secure.file_table[i].data_size;
|
||||
out.emplace_back(entry);
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
}
|
||||
|
||||
return 0x1;
|
||||
}
|
||||
|
||||
} // namespace sphaira::yati::container
|
||||
122
sphaira/source/yati/nx/es.cpp
Normal file
122
sphaira/source/yati/nx/es.cpp
Normal file
@@ -0,0 +1,122 @@
|
||||
#include "yati/nx/es.hpp"
|
||||
#include "yati/nx/crypto.hpp"
|
||||
#include "yati/nx/nxdumptool_rsa.h"
|
||||
#include "defines.hpp"
|
||||
#include "log.hpp"
|
||||
#include <memory>
|
||||
#include <cstring>
|
||||
|
||||
namespace sphaira::es {
|
||||
namespace {
|
||||
|
||||
} // namespace
|
||||
|
||||
Result ImportTicket(Service* srv, const void* tik_buf, u64 tik_size, const void* cert_buf, u64 cert_size) {
|
||||
return serviceDispatch(srv, 1,
|
||||
.buffer_attrs = { SfBufferAttr_HipcMapAlias | SfBufferAttr_In, SfBufferAttr_HipcMapAlias | SfBufferAttr_In },
|
||||
.buffers = { { tik_buf, tik_size }, { cert_buf, cert_size } });
|
||||
}
|
||||
|
||||
typedef enum {
|
||||
TikPropertyMask_None = 0,
|
||||
TikPropertyMask_PreInstallation = BIT(0), ///< Determines if the title comes pre-installed on the device. Most likely unused -- a remnant from previous ticket formats.
|
||||
TikPropertyMask_SharedTitle = BIT(1), ///< Determines if the title holds shared contents only. Most likely unused -- a remnant from previous ticket formats.
|
||||
TikPropertyMask_AllContents = BIT(2), ///< Determines if the content index mask shall be bypassed. Most likely unused -- a remnant from previous ticket formats.
|
||||
TikPropertyMask_DeviceLinkIndepedent = BIT(3), ///< Determines if the console should *not* connect to the Internet to verify if the title's being used by the primary console.
|
||||
TikPropertyMask_Volatile = BIT(4), ///< Determines if the ticket copy inside ticket.bin is available after reboot. Can be encrypted.
|
||||
TikPropertyMask_ELicenseRequired = BIT(5), ///< Determines if the console should connect to the Internet to perform license verification.
|
||||
TikPropertyMask_Count = 6 ///< Total values supported by this enum.
|
||||
} TikPropertyMask;
|
||||
|
||||
Result GetTicketDataOffset(std::span<const u8> ticket, u64& out) {
|
||||
log_write("inside es\n");
|
||||
u32 signature_type;
|
||||
std::memcpy(std::addressof(signature_type), ticket.data(), sizeof(signature_type));
|
||||
|
||||
u32 signature_size;
|
||||
switch (signature_type) {
|
||||
case es::TicketSigantureType_RSA_4096_SHA1: log_write("RSA-4096 PKCS#1 v1.5 with SHA-1\n"); signature_size = 0x200; break;
|
||||
case es::TicketSigantureType_RSA_2048_SHA1: log_write("RSA-2048 PKCS#1 v1.5 with SHA-1\n"); signature_size = 0x100; break;
|
||||
case es::TicketSigantureType_ECDSA_SHA1: log_write("ECDSA with SHA-1\n"); signature_size = 0x3C; break;
|
||||
case es::TicketSigantureType_RSA_4096_SHA256: log_write("RSA-4096 PKCS#1 v1.5 with SHA-256\n"); signature_size = 0x200; break;
|
||||
case es::TicketSigantureType_RSA_2048_SHA256: log_write("RSA-2048 PKCS#1 v1.5 with SHA-256\n"); signature_size = 0x100; break;
|
||||
case es::TicketSigantureType_ECDSA_SHA256: log_write("ECDSA with SHA-256\n"); signature_size = 0x3C; break;
|
||||
case es::TicketSigantureType_HMAC_SHA1_160: log_write("HMAC-SHA1-160\n"); signature_size = 0x14; break;
|
||||
default: log_write("unknown ticket\n"); return 0x1;
|
||||
}
|
||||
|
||||
// align-up to 0x40.
|
||||
out = ((signature_size + sizeof(signature_type)) + 0x3F) & ~0x3F;
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result GetTicketData(std::span<const u8> ticket, es::TicketData* out) {
|
||||
u64 data_off;
|
||||
R_TRY(GetTicketDataOffset(ticket, data_off));
|
||||
std::memcpy(out, ticket.data() + data_off, sizeof(*out));
|
||||
|
||||
// validate ticket data.
|
||||
R_UNLESS(out->ticket_version1 == 0x2, Result_InvalidTicketVersion); // must be version 2.
|
||||
R_UNLESS(out->title_key_type == es::TicketTitleKeyType_Common || out->title_key_type == es::TicketTitleKeyType_Personalized, Result_InvalidTicketKeyType);
|
||||
R_UNLESS(out->master_key_revision <= 0x20, Result_InvalidTicketKeyRevision);
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result SetTicketData(std::span<u8> ticket, const es::TicketData* in) {
|
||||
u64 data_off;
|
||||
R_TRY(GetTicketDataOffset(ticket, data_off));
|
||||
std::memcpy(ticket.data() + data_off, in, sizeof(*in));
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result GetTitleKey(keys::KeyEntry& out, const TicketData& data, const keys::Keys& keys) {
|
||||
if (data.title_key_type == es::TicketTitleKeyType_Common) {
|
||||
std::memcpy(std::addressof(out), data.title_key_block, sizeof(out));
|
||||
} else if (data.title_key_type == es::TicketTitleKeyType_Personalized) {
|
||||
auto rsa_key = (const es::EticketRsaDeviceKey*)keys.eticket_device_key.key;
|
||||
log_write("personalised ticket\n");
|
||||
log_write("master_key_revision: %u\n", data.master_key_revision);
|
||||
log_write("license_type: %u\n", data.license_type);
|
||||
log_write("properties_bitfield: 0x%X\n", data.properties_bitfield);
|
||||
log_write("device_id: 0x%lX vs 0x%lX\n", data.device_id, std::byteswap(rsa_key->device_id));
|
||||
|
||||
R_UNLESS(data.device_id == std::byteswap(rsa_key->device_id), 0x1);
|
||||
log_write("device id is same\n");
|
||||
|
||||
u8 out_keydata[RSA2048_BYTES]{};
|
||||
size_t out_keydata_size;
|
||||
R_UNLESS(rsa2048OaepDecrypt(out_keydata, sizeof(out_keydata), data.title_key_block, rsa_key->modulus, &rsa_key->public_exponent, sizeof(rsa_key->public_exponent), rsa_key->private_exponent, sizeof(rsa_key->private_exponent), NULL, 0, &out_keydata_size), 0x1);
|
||||
R_UNLESS(out_keydata_size >= sizeof(out), 0x1);
|
||||
std::memcpy(std::addressof(out), out_keydata, sizeof(out));
|
||||
} else {
|
||||
R_THROW(0x1);
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result DecryptTitleKey(keys::KeyEntry& out, u8 key_gen, const keys::Keys& keys) {
|
||||
keys::KeyEntry title_kek;
|
||||
R_TRY(keys.GetTitleKek(std::addressof(title_kek), key_gen));
|
||||
crypto::cryptoAes128(std::addressof(out), std::addressof(out), std::addressof(title_kek), false);
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
// todo: i thought i already wrote the code for this??
|
||||
// todo: patch the ticket.
|
||||
Result PatchTicket(std::span<u8> ticket, const keys::Keys& keys) {
|
||||
TicketData data;
|
||||
R_TRY(GetTicketData(ticket, &data));
|
||||
|
||||
if (data.title_key_type == es::TicketTitleKeyType_Common) {
|
||||
// todo: verify common signature
|
||||
} else if (data.title_key_type == es::TicketTitleKeyType_Personalized) {
|
||||
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
} // namespace sphaira::es
|
||||
130
sphaira/source/yati/nx/keys.cpp
Normal file
130
sphaira/source/yati/nx/keys.cpp
Normal file
@@ -0,0 +1,130 @@
|
||||
#include "yati/nx/keys.hpp"
|
||||
#include "yati/nx/nca.hpp"
|
||||
#include "yati/nx/es.hpp"
|
||||
#include "yati/nx/crypto.hpp"
|
||||
#include "defines.hpp"
|
||||
#include "log.hpp"
|
||||
#include <minIni.h>
|
||||
#include <memory>
|
||||
#include <bit>
|
||||
#include <cstring>
|
||||
|
||||
namespace sphaira::keys {
|
||||
namespace {
|
||||
|
||||
constexpr u8 HEADER_KEK_SRC[0x10] = {
|
||||
0x1F, 0x12, 0x91, 0x3A, 0x4A, 0xCB, 0xF0, 0x0D, 0x4C, 0xDE, 0x3A, 0xF6, 0xD5, 0x23, 0x88, 0x2A
|
||||
};
|
||||
|
||||
constexpr u8 HEADER_KEY_SRC[0x20] = {
|
||||
0x5A, 0x3E, 0xD8, 0x4F, 0xDE, 0xC0, 0xD8, 0x26, 0x31, 0xF7, 0xE2, 0x5D, 0x19, 0x7B, 0xF5, 0xD0,
|
||||
0x1C, 0x9B, 0x7B, 0xFA, 0xF6, 0x28, 0x18, 0x3D, 0x71, 0xF6, 0x4D, 0x73, 0xF1, 0x50, 0xB9, 0xD2
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
void parse_hex_key(void* key, const char* hex) {
|
||||
char low[0x11]{};
|
||||
char upp[0x11]{};
|
||||
std::memcpy(low, hex, 0x10);
|
||||
std::memcpy(upp, hex + 0x10, 0x10);
|
||||
*(u64*)key = std::byteswap(std::strtoul(low, nullptr, 0x10));
|
||||
*(u64*)((u8*)key + 8) = std::byteswap(std::strtoul(upp, nullptr, 0x10));
|
||||
}
|
||||
|
||||
Result parse_keys(Keys& out, bool read_from_file) {
|
||||
static constexpr auto find_key = [](const char* key, const char* value, const char* search_key, KeySection& key_section) -> bool {
|
||||
if (!std::strncmp(key, search_key, std::strlen(search_key))) {
|
||||
// get key index.
|
||||
char* end;
|
||||
const auto key_value_str = key + std::strlen(search_key);
|
||||
const auto index = std::strtoul(key_value_str, &end, 0x10);
|
||||
if (end && end != key_value_str && index < 0x20) {
|
||||
KeyEntry keak;
|
||||
parse_hex_key(std::addressof(keak), value);
|
||||
key_section[index] = keak;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
static constexpr auto find_key_single = [](const char* key, const char* value, const char* search_key, KeyEntry& key_entry) -> bool {
|
||||
if (!std::strcmp(key, search_key)) {
|
||||
parse_hex_key(std::addressof(key_entry), value);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
static constexpr auto cb = [](const mTCHAR *Section, const mTCHAR *Key, const mTCHAR *Value, void *UserData) -> int {
|
||||
auto keys = static_cast<Keys*>(UserData);
|
||||
|
||||
auto key_text_key_area_key_app = "key_area_key_application_";
|
||||
auto key_text_key_area_key_oce = "key_area_key_ocean_";
|
||||
auto key_text_key_area_key_sys = "key_area_key_system_";
|
||||
auto key_text_titlekek = "titlekek_";
|
||||
auto key_text_master_key = "master_key_";
|
||||
auto key_text_eticket_rsa_kek = keys->eticket_device_key.generation ? "eticket_rsa_kek_personalized" : "eticket_rsa_kek";
|
||||
|
||||
if (find_key(Key, Value, key_text_key_area_key_app, keys->key_area_key[nca::KeyAreaEncryptionKeyIndex_Application])) {
|
||||
return 1;
|
||||
} else if (find_key(Key, Value, key_text_key_area_key_oce, keys->key_area_key[nca::KeyAreaEncryptionKeyIndex_Ocean])) {
|
||||
return 1;
|
||||
} else if (find_key(Key, Value, key_text_key_area_key_sys, keys->key_area_key[nca::KeyAreaEncryptionKeyIndex_System])) {
|
||||
return 1;
|
||||
} else if (find_key(Key, Value, key_text_titlekek, keys->titlekek)) {
|
||||
return 1;
|
||||
} else if (find_key(Key, Value, key_text_master_key, keys->master_key)) {
|
||||
return 1;
|
||||
} else if (find_key_single(Key, Value, key_text_eticket_rsa_kek, keys->eticket_rsa_kek)) {
|
||||
log_write("found key single: key: %s value %s\n", Key, Value);
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 1;
|
||||
};
|
||||
|
||||
R_TRY(splCryptoInitialize());
|
||||
ON_SCOPE_EXIT(splCryptoExit());
|
||||
|
||||
u8 header_kek[0x20];
|
||||
R_TRY(splCryptoGenerateAesKek(HEADER_KEK_SRC, 0, 0, header_kek));
|
||||
R_TRY(splCryptoGenerateAesKey(header_kek, HEADER_KEY_SRC, out.header_key));
|
||||
R_TRY(splCryptoGenerateAesKey(header_kek, HEADER_KEY_SRC + 0x10, out.header_key + 0x10));
|
||||
|
||||
if (read_from_file) {
|
||||
// get eticket device key, needed for decrypting personalised tickets.
|
||||
R_TRY(setcalInitialize());
|
||||
ON_SCOPE_EXIT(setcalExit());
|
||||
R_TRY(setcalGetEticketDeviceKey(std::addressof(out.eticket_device_key)));
|
||||
|
||||
R_UNLESS(ini_browse(cb, std::addressof(out), "/switch/prod.keys"), 0x1);
|
||||
|
||||
// decrypt eticket device key.
|
||||
if (out.eticket_rsa_kek.IsValid()) {
|
||||
auto rsa_key = (es::EticketRsaDeviceKey*)out.eticket_device_key.key;
|
||||
|
||||
Aes128CtrContext eticket_aes_ctx{};
|
||||
aes128CtrContextCreate(&eticket_aes_ctx, &out.eticket_rsa_kek, rsa_key->ctr);
|
||||
aes128CtrCrypt(&eticket_aes_ctx, &(rsa_key->private_exponent), &(rsa_key->private_exponent), sizeof(es::EticketRsaDeviceKey) - sizeof(rsa_key->ctr));
|
||||
|
||||
const auto public_exponent = std::byteswap(rsa_key->public_exponent);
|
||||
if (public_exponent != 0x10001) {
|
||||
log_write("etick decryption fail: 0x%X\n", public_exponent);
|
||||
if (public_exponent == 0) {
|
||||
log_write("eticket device id is NULL\n");
|
||||
}
|
||||
R_THROW(0x1);
|
||||
} else {
|
||||
log_write("eticket match\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
} // namespace sphaira::keys
|
||||
180
sphaira/source/yati/nx/nca.cpp
Normal file
180
sphaira/source/yati/nx/nca.cpp
Normal file
@@ -0,0 +1,180 @@
|
||||
#include "yati/nx/nca.hpp"
|
||||
#include "yati/nx/crypto.hpp"
|
||||
#include "yati/nx/nxdumptool_rsa.h"
|
||||
#include "log.hpp"
|
||||
|
||||
namespace sphaira::nca {
|
||||
namespace {
|
||||
|
||||
constexpr u8 g_key_area_key_application_source[0x10] = { 0x7F, 0x59, 0x97, 0x1E, 0x62, 0x9F, 0x36, 0xA1, 0x30, 0x98, 0x06, 0x6F, 0x21, 0x44, 0xC3, 0x0D };
|
||||
constexpr u8 g_key_area_key_ocean_source[0x10] = { 0x32, 0x7D, 0x36, 0x08, 0x5A, 0xD1, 0x75, 0x8D, 0xAB, 0x4E, 0x6F, 0xBA, 0xA5, 0x55, 0xD8, 0x82 };
|
||||
constexpr u8 g_key_area_key_system_source[0x10] = { 0x87, 0x45, 0xF1, 0xBB, 0xA6, 0xBE, 0x79, 0x64, 0x7D, 0x04, 0x8B, 0xA6, 0x7B, 0x5F, 0xDA, 0x4A };
|
||||
|
||||
constexpr const u8* g_key_area_key[] = {
|
||||
g_key_area_key_application_source,
|
||||
g_key_area_key_ocean_source,
|
||||
g_key_area_key_system_source
|
||||
};
|
||||
|
||||
const unsigned char nca_hdr_fixed_key_moduli_retail[0x2][0x100] = { /* Fixed RSA key used to validate NCA signature 0. */
|
||||
{
|
||||
0xBF, 0xBE, 0x40, 0x6C, 0xF4, 0xA7, 0x80, 0xE9, 0xF0, 0x7D, 0x0C, 0x99, 0x61, 0x1D, 0x77, 0x2F,
|
||||
0x96, 0xBC, 0x4B, 0x9E, 0x58, 0x38, 0x1B, 0x03, 0xAB, 0xB1, 0x75, 0x49, 0x9F, 0x2B, 0x4D, 0x58,
|
||||
0x34, 0xB0, 0x05, 0xA3, 0x75, 0x22, 0xBE, 0x1A, 0x3F, 0x03, 0x73, 0xAC, 0x70, 0x68, 0xD1, 0x16,
|
||||
0xB9, 0x04, 0x46, 0x5E, 0xB7, 0x07, 0x91, 0x2F, 0x07, 0x8B, 0x26, 0xDE, 0xF6, 0x00, 0x07, 0xB2,
|
||||
0xB4, 0x51, 0xF8, 0x0D, 0x0A, 0x5E, 0x58, 0xAD, 0xEB, 0xBC, 0x9A, 0xD6, 0x49, 0xB9, 0x64, 0xEF,
|
||||
0xA7, 0x82, 0xB5, 0xCF, 0x6D, 0x70, 0x13, 0xB0, 0x0F, 0x85, 0xF6, 0xA9, 0x08, 0xAA, 0x4D, 0x67,
|
||||
0x66, 0x87, 0xFA, 0x89, 0xFF, 0x75, 0x90, 0x18, 0x1E, 0x6B, 0x3D, 0xE9, 0x8A, 0x68, 0xC9, 0x26,
|
||||
0x04, 0xD9, 0x80, 0xCE, 0x3F, 0x5E, 0x92, 0xCE, 0x01, 0xFF, 0x06, 0x3B, 0xF2, 0xC1, 0xA9, 0x0C,
|
||||
0xCE, 0x02, 0x6F, 0x16, 0xBC, 0x92, 0x42, 0x0A, 0x41, 0x64, 0xCD, 0x52, 0xB6, 0x34, 0x4D, 0xAE,
|
||||
0xC0, 0x2E, 0xDE, 0xA4, 0xDF, 0x27, 0x68, 0x3C, 0xC1, 0xA0, 0x60, 0xAD, 0x43, 0xF3, 0xFC, 0x86,
|
||||
0xC1, 0x3E, 0x6C, 0x46, 0xF7, 0x7C, 0x29, 0x9F, 0xFA, 0xFD, 0xF0, 0xE3, 0xCE, 0x64, 0xE7, 0x35,
|
||||
0xF2, 0xF6, 0x56, 0x56, 0x6F, 0x6D, 0xF1, 0xE2, 0x42, 0xB0, 0x83, 0x40, 0xA5, 0xC3, 0x20, 0x2B,
|
||||
0xCC, 0x9A, 0xAE, 0xCA, 0xED, 0x4D, 0x70, 0x30, 0xA8, 0x70, 0x1C, 0x70, 0xFD, 0x13, 0x63, 0x29,
|
||||
0x02, 0x79, 0xEA, 0xD2, 0xA7, 0xAF, 0x35, 0x28, 0x32, 0x1C, 0x7B, 0xE6, 0x2F, 0x1A, 0xAA, 0x40,
|
||||
0x7E, 0x32, 0x8C, 0x27, 0x42, 0xFE, 0x82, 0x78, 0xEC, 0x0D, 0xEB, 0xE6, 0x83, 0x4B, 0x6D, 0x81,
|
||||
0x04, 0x40, 0x1A, 0x9E, 0x9A, 0x67, 0xF6, 0x72, 0x29, 0xFA, 0x04, 0xF0, 0x9D, 0xE4, 0xF4, 0x03,
|
||||
},
|
||||
{
|
||||
0xAD, 0xE3, 0xE1, 0xFA, 0x04, 0x35, 0xE5, 0xB6, 0xDD, 0x49, 0xEA, 0x89, 0x29, 0xB1, 0xFF, 0xB6,
|
||||
0x43, 0xDF, 0xCA, 0x96, 0xA0, 0x4A, 0x13, 0xDF, 0x43, 0xD9, 0x94, 0x97, 0x96, 0x43, 0x65, 0x48,
|
||||
0x70, 0x58, 0x33, 0xA2, 0x7D, 0x35, 0x7B, 0x96, 0x74, 0x5E, 0x0B, 0x5C, 0x32, 0x18, 0x14, 0x24,
|
||||
0xC2, 0x58, 0xB3, 0x6C, 0x22, 0x7A, 0xA1, 0xB7, 0xCB, 0x90, 0xA7, 0xA3, 0xF9, 0x7D, 0x45, 0x16,
|
||||
0xA5, 0xC8, 0xED, 0x8F, 0xAD, 0x39, 0x5E, 0x9E, 0x4B, 0x51, 0x68, 0x7D, 0xF8, 0x0C, 0x35, 0xC6,
|
||||
0x3F, 0x91, 0xAE, 0x44, 0xA5, 0x92, 0x30, 0x0D, 0x46, 0xF8, 0x40, 0xFF, 0xD0, 0xFF, 0x06, 0xD2,
|
||||
0x1C, 0x7F, 0x96, 0x18, 0xDC, 0xB7, 0x1D, 0x66, 0x3E, 0xD1, 0x73, 0xBC, 0x15, 0x8A, 0x2F, 0x94,
|
||||
0xF3, 0x00, 0xC1, 0x83, 0xF1, 0xCD, 0xD7, 0x81, 0x88, 0xAB, 0xDF, 0x8C, 0xEF, 0x97, 0xDD, 0x1B,
|
||||
0x17, 0x5F, 0x58, 0xF6, 0x9A, 0xE9, 0xE8, 0xC2, 0x2F, 0x38, 0x15, 0xF5, 0x21, 0x07, 0xF8, 0x37,
|
||||
0x90, 0x5D, 0x2E, 0x02, 0x40, 0x24, 0x15, 0x0D, 0x25, 0xB7, 0x26, 0x5D, 0x09, 0xCC, 0x4C, 0xF4,
|
||||
0xF2, 0x1B, 0x94, 0x70, 0x5A, 0x9E, 0xEE, 0xED, 0x77, 0x77, 0xD4, 0x51, 0x99, 0xF5, 0xDC, 0x76,
|
||||
0x1E, 0xE3, 0x6C, 0x8C, 0xD1, 0x12, 0xD4, 0x57, 0xD1, 0xB6, 0x83, 0xE4, 0xE4, 0xFE, 0xDA, 0xE9,
|
||||
0xB4, 0x3B, 0x33, 0xE5, 0x37, 0x8A, 0xDF, 0xB5, 0x7F, 0x89, 0xF1, 0x9B, 0x9E, 0xB0, 0x15, 0xB2,
|
||||
0x3A, 0xFE, 0xEA, 0x61, 0x84, 0x5B, 0x7D, 0x4B, 0x23, 0x12, 0x0B, 0x83, 0x12, 0xF2, 0x22, 0x6B,
|
||||
0xB9, 0x22, 0x96, 0x4B, 0x26, 0x0B, 0x63, 0x5E, 0x96, 0x57, 0x52, 0xA3, 0x67, 0x64, 0x22, 0xCA,
|
||||
0xD0, 0x56, 0x3E, 0x74, 0xB5, 0x98, 0x1F, 0x0D, 0xF8, 0xB3, 0x34, 0xE6, 0x98, 0x68, 0x5A, 0xAD,
|
||||
}
|
||||
};
|
||||
|
||||
const unsigned char acid_fixed_key_moduli_retail[0x2][0x100] = { /* Fixed RSA keys used to validate ACID signatures. */
|
||||
{
|
||||
0xDD, 0xC8, 0xDD, 0xF2, 0x4E, 0x6D, 0xF0, 0xCA, 0x9E, 0xC7, 0x5D, 0xC7, 0x7B, 0xAD, 0xFE, 0x7D,
|
||||
0x23, 0x89, 0x69, 0xB6, 0xF2, 0x06, 0xA2, 0x02, 0x88, 0xE1, 0x55, 0x91, 0xAB, 0xCB, 0x4D, 0x50,
|
||||
0x2E, 0xFC, 0x9D, 0x94, 0x76, 0xD6, 0x4C, 0xD8, 0xFF, 0x10, 0xFA, 0x5E, 0x93, 0x0A, 0xB4, 0x57,
|
||||
0xAC, 0x51, 0xC7, 0x16, 0x66, 0xF4, 0x1A, 0x54, 0xC2, 0xC5, 0x04, 0x3D, 0x1B, 0xFE, 0x30, 0x20,
|
||||
0x8A, 0xAC, 0x6F, 0x6F, 0xF5, 0xC7, 0xB6, 0x68, 0xB8, 0xC9, 0x40, 0x6B, 0x42, 0xAD, 0x11, 0x21,
|
||||
0xE7, 0x8B, 0xE9, 0x75, 0x01, 0x86, 0xE4, 0x48, 0x9B, 0x0A, 0x0A, 0xF8, 0x7F, 0xE8, 0x87, 0xF2,
|
||||
0x82, 0x01, 0xE6, 0xA3, 0x0F, 0xE4, 0x66, 0xAE, 0x83, 0x3F, 0x4E, 0x9F, 0x5E, 0x01, 0x30, 0xA4,
|
||||
0x00, 0xB9, 0x9A, 0xAE, 0x5F, 0x03, 0xCC, 0x18, 0x60, 0xE5, 0xEF, 0x3B, 0x5E, 0x15, 0x16, 0xFE,
|
||||
0x1C, 0x82, 0x78, 0xB5, 0x2F, 0x47, 0x7C, 0x06, 0x66, 0x88, 0x5D, 0x35, 0xA2, 0x67, 0x20, 0x10,
|
||||
0xE7, 0x6C, 0x43, 0x68, 0xD3, 0xE4, 0x5A, 0x68, 0x2A, 0x5A, 0xE2, 0x6D, 0x73, 0xB0, 0x31, 0x53,
|
||||
0x1C, 0x20, 0x09, 0x44, 0xF5, 0x1A, 0x9D, 0x22, 0xBE, 0x12, 0xA1, 0x77, 0x11, 0xE2, 0xA1, 0xCD,
|
||||
0x40, 0x9A, 0xA2, 0x8B, 0x60, 0x9B, 0xEF, 0xA0, 0xD3, 0x48, 0x63, 0xA2, 0xF8, 0xA3, 0x2C, 0x08,
|
||||
0x56, 0x52, 0x2E, 0x60, 0x19, 0x67, 0x5A, 0xA7, 0x9F, 0xDC, 0x3F, 0x3F, 0x69, 0x2B, 0x31, 0x6A,
|
||||
0xB7, 0x88, 0x4A, 0x14, 0x84, 0x80, 0x33, 0x3C, 0x9D, 0x44, 0xB7, 0x3F, 0x4C, 0xE1, 0x75, 0xEA,
|
||||
0x37, 0xEA, 0xE8, 0x1E, 0x7C, 0x77, 0xB7, 0xC6, 0x1A, 0xA2, 0xF0, 0x9F, 0x10, 0x61, 0xCD, 0x7B,
|
||||
0x5B, 0x32, 0x4C, 0x37, 0xEF, 0xB1, 0x71, 0x68, 0x53, 0x0A, 0xED, 0x51, 0x7D, 0x35, 0x22, 0xFD,
|
||||
},
|
||||
{
|
||||
0xE7, 0xAA, 0x25, 0xC8, 0x01, 0xA5, 0x14, 0x6B, 0x01, 0x60, 0x3E, 0xD9, 0x96, 0x5A, 0xBF, 0x90,
|
||||
0xAC, 0xA7, 0xFD, 0x9B, 0x5B, 0xBD, 0x8A, 0x26, 0xB0, 0xCB, 0x20, 0x28, 0x9A, 0x72, 0x12, 0xF5,
|
||||
0x20, 0x65, 0xB3, 0xB9, 0x84, 0x58, 0x1F, 0x27, 0xBC, 0x7C, 0xA2, 0xC9, 0x9E, 0x18, 0x95, 0xCF,
|
||||
0xC2, 0x73, 0x2E, 0x74, 0x8C, 0x66, 0xE5, 0x9E, 0x79, 0x2B, 0xB8, 0x07, 0x0C, 0xB0, 0x4E, 0x8E,
|
||||
0xAB, 0x85, 0x21, 0x42, 0xC4, 0xC5, 0x6D, 0x88, 0x9C, 0xDB, 0x15, 0x95, 0x3F, 0x80, 0xDB, 0x7A,
|
||||
0x9A, 0x7D, 0x41, 0x56, 0x25, 0x17, 0x18, 0x42, 0x4D, 0x8C, 0xAC, 0xA5, 0x7B, 0xDB, 0x42, 0x5D,
|
||||
0x59, 0x35, 0x45, 0x5D, 0x8A, 0x02, 0xB5, 0x70, 0xC0, 0x72, 0x35, 0x46, 0xD0, 0x1D, 0x60, 0x01,
|
||||
0x4A, 0xCC, 0x1C, 0x46, 0xD3, 0xD6, 0x35, 0x52, 0xD6, 0xE1, 0xF8, 0x3B, 0x5D, 0xEA, 0xDD, 0xB8,
|
||||
0xFE, 0x7D, 0x50, 0xCB, 0x35, 0x23, 0x67, 0x8B, 0xB6, 0xE4, 0x74, 0xD2, 0x60, 0xFC, 0xFD, 0x43,
|
||||
0xBF, 0x91, 0x08, 0x81, 0xC5, 0x4F, 0x5D, 0x16, 0x9A, 0xC4, 0x9A, 0xC6, 0xF6, 0xF3, 0xE1, 0xF6,
|
||||
0x5C, 0x07, 0xAA, 0x71, 0x6C, 0x13, 0xA4, 0xB1, 0xB3, 0x66, 0xBF, 0x90, 0x4C, 0x3D, 0xA2, 0xC4,
|
||||
0x0B, 0xB8, 0x3D, 0x7A, 0x8C, 0x19, 0xFA, 0xFF, 0x6B, 0xB9, 0x1F, 0x02, 0xCC, 0xB6, 0xD3, 0x0C,
|
||||
0x7D, 0x19, 0x1F, 0x47, 0xF9, 0xC7, 0x40, 0x01, 0xFA, 0x46, 0xEA, 0x0B, 0xD4, 0x02, 0xE0, 0x3D,
|
||||
0x30, 0x9A, 0x1A, 0x0F, 0xEA, 0xA7, 0x66, 0x55, 0xF7, 0xCB, 0x28, 0xE2, 0xBB, 0x99, 0xE4, 0x83,
|
||||
0xC3, 0x43, 0x03, 0xEE, 0xDC, 0x1F, 0x02, 0x23, 0xDD, 0xD1, 0x2D, 0x39, 0xA4, 0x65, 0x75, 0x03,
|
||||
0xEF, 0x37, 0x9C, 0x06, 0xD6, 0xFA, 0xA1, 0x15, 0xF0, 0xDB, 0x17, 0x47, 0x26, 0x4F, 0x49, 0x03
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
Result DecryptKeak(const keys::Keys& keys, Header& header) {
|
||||
const auto key_generation = header.GetKeyGeneration();
|
||||
|
||||
// try with spl.
|
||||
keys::KeyEntry keak;
|
||||
if (R_SUCCEEDED(splCryptoGenerateAesKek(g_key_area_key[header.kaek_index], key_generation, 0, &keak))) {
|
||||
for (auto& key_area : header.key_area) {
|
||||
R_TRY(splCryptoGenerateAesKey(&keak, std::addressof(key_area), std::addressof(key_area)));
|
||||
}
|
||||
} else {
|
||||
// failed with spl, try using keys.
|
||||
R_TRY(keys.GetNcaKeyArea(&keak, key_generation, header.kaek_index));
|
||||
for (auto& key_area : header.key_area) {
|
||||
crypto::cryptoAes128(std::addressof(key_area), std::addressof(key_area), std::addressof(keak), false);
|
||||
}
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result EncryptKeak(const keys::Keys& keys, Header& header, u8 key_generation) {
|
||||
header.SetKeyGeneration(key_generation);
|
||||
|
||||
keys::KeyEntry keak;
|
||||
R_TRY(keys.GetNcaKeyArea(&keak, key_generation, header.kaek_index));
|
||||
log_write("re-encrypting with: 0x%X\n", key_generation);
|
||||
|
||||
for (auto& key_area : header.key_area) {
|
||||
crypto::cryptoAes128(std::addressof(key_area), std::addressof(key_area), std::addressof(keak), true);
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result VerifyFixedKey(const Header& header) {
|
||||
R_UNLESS(header.sig_key_gen < std::size(nca_hdr_fixed_key_moduli_retail), 0x1);
|
||||
auto mod = nca_hdr_fixed_key_moduli_retail[header.sig_key_gen];
|
||||
|
||||
const u8 E[3] = { 1, 0, 1 };
|
||||
if (!rsa2048VerifySha256BasedPssSignature(&header.magic, 0x200, header.rsa_fixed_key, mod, E, sizeof(E))) {
|
||||
auto new_header = header;
|
||||
// if failed, detect if this is a eshop/xci convert.
|
||||
new_header.distribution_type ^= 1;
|
||||
if (!rsa2048VerifySha256BasedPssSignature(&new_header.magic, 0x200, new_header.rsa_fixed_key, mod, E, sizeof(E))) {
|
||||
log_write("FAILED nca header hash\n");
|
||||
R_THROW(0x1);
|
||||
} else {
|
||||
log_write("WARNING! nca is converted! distribution_type: %u\n", new_header.distribution_type);
|
||||
R_SUCCEED();
|
||||
}
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
auto GetKeyGenStr(u8 key_gen) -> const char* {
|
||||
switch (key_gen) {
|
||||
case KeyGenerationOld_100: return "1.0.0";
|
||||
case KeyGenerationOld_300: return "3.0.0";
|
||||
case KeyGeneration_301: return "3.0.1";
|
||||
case KeyGeneration_400: return "4.0.0";
|
||||
case KeyGeneration_500: return "5.0.0";
|
||||
case KeyGeneration_600: return "6.0.0";
|
||||
case KeyGeneration_620: return "6.2.0";
|
||||
case KeyGeneration_700: return "7.0.0";
|
||||
case KeyGeneration_810: return "8.1.0";
|
||||
case KeyGeneration_900: return "9.0.0";
|
||||
case KeyGeneration_910: return "9.1.0";
|
||||
case KeyGeneration_1210: return "12.1.0";
|
||||
case KeyGeneration_1300: return "13.0.0";
|
||||
case KeyGeneration_1400: return "14.0.0";
|
||||
case KeyGeneration_1500: return "15.0.0";
|
||||
case KeyGeneration_1600: return "16.0.0";
|
||||
case KeyGeneration_1700: return "17.0.0";
|
||||
case KeyGeneration_1800: return "18.0.0";
|
||||
case KeyGeneration_1900: return "19.0.0";
|
||||
}
|
||||
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
} // namespace sphaira::nca
|
||||
42
sphaira/source/yati/nx/ncm.cpp
Normal file
42
sphaira/source/yati/nx/ncm.cpp
Normal file
@@ -0,0 +1,42 @@
|
||||
#include "yati/nx/ncm.hpp"
|
||||
#include "defines.hpp"
|
||||
#include <memory>
|
||||
|
||||
namespace sphaira::ncm {
|
||||
namespace {
|
||||
|
||||
} // namespace
|
||||
|
||||
auto GetAppId(u8 meta_type, u64 id) -> u64 {
|
||||
if (meta_type == NcmContentMetaType_Patch) {
|
||||
return id ^ 0x800;
|
||||
} else if (meta_type == NcmContentMetaType_AddOnContent) {
|
||||
return (id ^ 0x1000) & ~0xFFF;
|
||||
} else {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
auto GetAppId(const NcmContentMetaKey& key) -> u64 {
|
||||
return GetAppId(key.type, key.id);
|
||||
}
|
||||
|
||||
auto GetAppId(const PackagedContentMeta& meta) -> u64 {
|
||||
return GetAppId(meta.meta_type, meta.title_id);
|
||||
}
|
||||
|
||||
Result Delete(NcmContentStorage* cs, const NcmContentId *content_id) {
|
||||
bool has;
|
||||
R_TRY(ncmContentStorageHas(cs, std::addressof(has), content_id));
|
||||
if (has) {
|
||||
R_TRY(ncmContentStorageDelete(cs, content_id));
|
||||
}
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Register(NcmContentStorage* cs, const NcmContentId *content_id, const NcmPlaceHolderId *placeholder_id) {
|
||||
R_TRY(Delete(cs, content_id));
|
||||
return ncmContentStorageRegister(cs, content_id, placeholder_id);
|
||||
}
|
||||
|
||||
} // namespace sphaira::ncm
|
||||
39
sphaira/source/yati/nx/ns.cpp
Normal file
39
sphaira/source/yati/nx/ns.cpp
Normal file
@@ -0,0 +1,39 @@
|
||||
#include "yati/nx/ns.hpp"
|
||||
|
||||
namespace sphaira::ns {
|
||||
namespace {
|
||||
|
||||
} // namespace
|
||||
|
||||
Result PushApplicationRecord(Service* srv, u64 tid, const ncm::ContentStorageRecord* records, u32 count) {
|
||||
const struct {
|
||||
u8 last_modified_event;
|
||||
u8 padding[0x7];
|
||||
u64 tid;
|
||||
} in = { ApplicationRecordType_Installed, {0}, tid };
|
||||
|
||||
return serviceDispatchIn(srv, 16, in,
|
||||
.buffer_attrs = { SfBufferAttr_HipcMapAlias | SfBufferAttr_In },
|
||||
.buffers = { { records, sizeof(*records) * count } });
|
||||
}
|
||||
|
||||
Result ListApplicationRecordContentMeta(Service* srv, u64 offset, u64 tid, ncm::ContentStorageRecord* out_records, u32 count, s32* entries_read) {
|
||||
struct {
|
||||
u64 offset;
|
||||
u64 tid;
|
||||
} in = { offset, tid };
|
||||
|
||||
return serviceDispatchInOut(srv, 17, in, *entries_read,
|
||||
.buffer_attrs = { SfBufferAttr_HipcMapAlias | SfBufferAttr_Out },
|
||||
.buffers = { { out_records, sizeof(*out_records) * count } });
|
||||
}
|
||||
|
||||
Result DeleteApplicationRecord(Service* srv, u64 tid) {
|
||||
return serviceDispatchIn(srv, 27, tid);
|
||||
}
|
||||
|
||||
Result InvalidateApplicationControlCache(Service* srv, u64 tid) {
|
||||
return serviceDispatchIn(srv, 404, tid);
|
||||
}
|
||||
|
||||
} // namespace sphaira::ns
|
||||
158
sphaira/source/yati/nx/nxdumptool_rsa.c
Normal file
158
sphaira/source/yati/nx/nxdumptool_rsa.c
Normal file
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* rsa.c
|
||||
*
|
||||
* Copyright (c) 2018-2019, SciresM.
|
||||
* Copyright (c) 2020-2024, DarkMatterCore <pabloacurielz@gmail.com>.
|
||||
*
|
||||
* This file is part of nxdumptool (https://github.com/DarkMatterCore/nxdumptool).
|
||||
*
|
||||
* nxdumptool is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* nxdumptool is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "yati/nx/nxdumptool_rsa.h"
|
||||
#include "log.hpp"
|
||||
#include <switch.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <mbedtls/rsa.h>
|
||||
#include <mbedtls/entropy.h>
|
||||
#include <mbedtls/ctr_drbg.h>
|
||||
#include <mbedtls/pk.h>
|
||||
|
||||
#define LOG_MSG_ERROR(...) log_write(__VA_ARGS__)
|
||||
|
||||
/* Function prototypes. */
|
||||
|
||||
static bool rsa2048VerifySha256BasedSignature(const void *data, size_t data_size, const void *signature, const void *modulus, const void *public_exponent, size_t public_exponent_size, \
|
||||
bool use_pss);
|
||||
|
||||
bool rsa2048VerifySha256BasedPssSignature(const void *data, size_t data_size, const void *signature, const void *modulus, const void *public_exponent, size_t public_exponent_size)
|
||||
{
|
||||
return rsa2048VerifySha256BasedSignature(data, data_size, signature, modulus, public_exponent, public_exponent_size, true);
|
||||
}
|
||||
|
||||
bool rsa2048VerifySha256BasedPkcs1v15Signature(const void *data, size_t data_size, const void *signature, const void *modulus, const void *public_exponent, size_t public_exponent_size)
|
||||
{
|
||||
return rsa2048VerifySha256BasedSignature(data, data_size, signature, modulus, public_exponent, public_exponent_size, false);
|
||||
}
|
||||
|
||||
bool rsa2048OaepDecrypt(void *dst, size_t dst_size, const void *signature, const void *modulus, const void *public_exponent, size_t public_exponent_size, const void *private_exponent, \
|
||||
size_t private_exponent_size, const void *label, size_t label_size, size_t *out_size)
|
||||
{
|
||||
if (!dst || !dst_size || !signature || !modulus || !public_exponent || !public_exponent_size || !private_exponent || !private_exponent_size || (!label && label_size) || (label && !label_size) || \
|
||||
!out_size)
|
||||
{
|
||||
LOG_MSG_ERROR("Invalid parameters!");
|
||||
return false;
|
||||
}
|
||||
|
||||
mbedtls_entropy_context entropy = {0};
|
||||
mbedtls_ctr_drbg_context ctr_drbg = {0};
|
||||
mbedtls_rsa_context rsa = {0};
|
||||
|
||||
const char *pers = __func__;
|
||||
int mbedtls_ret = 0;
|
||||
bool ret = false;
|
||||
|
||||
/* Initialize contexts. */
|
||||
mbedtls_entropy_init(&entropy);
|
||||
mbedtls_ctr_drbg_init(&ctr_drbg);
|
||||
mbedtls_rsa_init(&rsa, MBEDTLS_RSA_PKCS_V21, MBEDTLS_MD_SHA256);
|
||||
|
||||
/* Seed the random number generator. */
|
||||
mbedtls_ret = mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func, &entropy, (const u8*)pers, strlen(pers));
|
||||
if (mbedtls_ret != 0)
|
||||
{
|
||||
LOG_MSG_ERROR("mbedtls_ctr_drbg_seed failed! (%d).", mbedtls_ret);
|
||||
goto end;
|
||||
}
|
||||
|
||||
/* Import RSA parameters. */
|
||||
mbedtls_ret = mbedtls_rsa_import_raw(&rsa, (const u8*)modulus, RSA2048_BYTES, NULL, 0, NULL, 0, (const u8*)private_exponent, private_exponent_size, (const u8*)public_exponent, public_exponent_size);
|
||||
if (mbedtls_ret != 0)
|
||||
{
|
||||
LOG_MSG_ERROR("mbedtls_rsa_import_raw failed! (%d).", mbedtls_ret);
|
||||
goto end;
|
||||
}
|
||||
|
||||
/* Derive RSA prime factors. */
|
||||
mbedtls_ret = mbedtls_rsa_complete(&rsa);
|
||||
if (mbedtls_ret != 0)
|
||||
{
|
||||
LOG_MSG_ERROR("mbedtls_rsa_complete failed! (%d).", mbedtls_ret);
|
||||
goto end;
|
||||
}
|
||||
|
||||
/* Perform RSA-OAEP decryption. */
|
||||
mbedtls_ret = mbedtls_rsa_rsaes_oaep_decrypt(&rsa, mbedtls_ctr_drbg_random, &ctr_drbg, MBEDTLS_RSA_PRIVATE, (const u8*)label, label_size, out_size, (const u8*)signature, (u8*)dst, dst_size);
|
||||
if (mbedtls_ret != 0)
|
||||
{
|
||||
LOG_MSG_ERROR("mbedtls_rsa_rsaes_oaep_decrypt failed! (%d).", mbedtls_ret);
|
||||
goto end;
|
||||
}
|
||||
|
||||
ret = true;
|
||||
|
||||
end:
|
||||
mbedtls_rsa_free(&rsa);
|
||||
mbedtls_ctr_drbg_free(&ctr_drbg);
|
||||
mbedtls_entropy_free(&entropy);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static bool rsa2048VerifySha256BasedSignature(const void *data, size_t data_size, const void *signature, const void *modulus, const void *public_exponent, size_t public_exponent_size, \
|
||||
bool use_pss)
|
||||
{
|
||||
if (!data || !data_size || !signature || !modulus || !public_exponent || !public_exponent_size)
|
||||
{
|
||||
LOG_MSG_ERROR("Invalid parameters!");
|
||||
return false;
|
||||
}
|
||||
|
||||
int mbedtls_ret = 0;
|
||||
mbedtls_rsa_context rsa = {0};
|
||||
u8 hash[SHA256_HASH_SIZE] = {0};
|
||||
bool ret = false;
|
||||
|
||||
/* Initialize RSA context. */
|
||||
mbedtls_rsa_init(&rsa, use_pss ? MBEDTLS_RSA_PKCS_V21 : MBEDTLS_RSA_PKCS_V15, MBEDTLS_MD_SHA256);
|
||||
|
||||
/* Import RSA parameters. */
|
||||
mbedtls_ret = mbedtls_rsa_import_raw(&rsa, (const u8*)modulus, RSA2048_BYTES, NULL, 0, NULL, 0, NULL, 0, (const u8*)public_exponent, public_exponent_size);
|
||||
if (mbedtls_ret != 0)
|
||||
{
|
||||
LOG_MSG_ERROR("mbedtls_rsa_import_raw failed! (%d).", mbedtls_ret);
|
||||
goto end;
|
||||
}
|
||||
|
||||
/* Calculate SHA-256 checksum for the input data. */
|
||||
sha256CalculateHash(hash, data, data_size);
|
||||
|
||||
/* Verify signature. */
|
||||
mbedtls_ret = (use_pss ? mbedtls_rsa_rsassa_pss_verify(&rsa, NULL, NULL, MBEDTLS_RSA_PUBLIC, MBEDTLS_MD_SHA256, SHA256_HASH_SIZE, hash, (const u8*)signature) : \
|
||||
mbedtls_rsa_rsassa_pkcs1_v15_verify(&rsa, NULL, NULL, MBEDTLS_RSA_PUBLIC, MBEDTLS_MD_SHA256, SHA256_HASH_SIZE, hash, (const u8*)signature));
|
||||
if (mbedtls_ret != 0)
|
||||
{
|
||||
LOG_MSG_ERROR("mbedtls_rsa_rsassa_%s_verify failed! (%d).", use_pss ? "pss" : "pkcs1_v15", mbedtls_ret);
|
||||
goto end;
|
||||
}
|
||||
|
||||
ret = true;
|
||||
|
||||
end:
|
||||
mbedtls_rsa_free(&rsa);
|
||||
|
||||
return ret;
|
||||
}
|
||||
20
sphaira/source/yati/source/file.cpp
Normal file
20
sphaira/source/yati/source/file.cpp
Normal file
@@ -0,0 +1,20 @@
|
||||
#include "yati/source/file.hpp"
|
||||
|
||||
namespace sphaira::yati::source {
|
||||
|
||||
File::File(FsFileSystem* fs, const fs::FsPath& path) {
|
||||
m_open_result = fsFsOpenFile(fs, path, FsOpenMode_Read, std::addressof(m_file));
|
||||
}
|
||||
|
||||
File::~File() {
|
||||
if (R_SUCCEEDED(GetOpenResult())) {
|
||||
fsFileClose(std::addressof(m_file));
|
||||
}
|
||||
}
|
||||
|
||||
Result File::Read(void* buf, s64 off, s64 size, u64* bytes_read) {
|
||||
R_TRY(GetOpenResult());
|
||||
return fsFileRead(std::addressof(m_file), off, buf, size, 0, bytes_read);
|
||||
}
|
||||
|
||||
} // namespace sphaira::yati::source
|
||||
28
sphaira/source/yati/source/stdio.cpp
Normal file
28
sphaira/source/yati/source/stdio.cpp
Normal file
@@ -0,0 +1,28 @@
|
||||
#include "yati/source/stdio.hpp"
|
||||
|
||||
namespace sphaira::yati::source {
|
||||
|
||||
Stdio::Stdio(const fs::FsPath& path) {
|
||||
m_file = std::fopen(path, "rb");
|
||||
if (!m_file) {
|
||||
m_open_result = fsdevGetLastResult();
|
||||
}
|
||||
}
|
||||
|
||||
Stdio::~Stdio() {
|
||||
if (R_SUCCEEDED(GetOpenResult())) {
|
||||
std::fclose(m_file);
|
||||
}
|
||||
}
|
||||
|
||||
Result Stdio::Read(void* buf, s64 off, s64 size, u64* bytes_read) {
|
||||
R_TRY(GetOpenResult());
|
||||
|
||||
std::fseek(m_file, off, SEEK_SET);
|
||||
R_TRY(fsdevGetLastResult());
|
||||
|
||||
*bytes_read = std::fread(buf, 1, size, m_file);
|
||||
return fsdevGetLastResult();
|
||||
}
|
||||
|
||||
} // namespace sphaira::yati::source
|
||||
41
sphaira/source/yati/source/stream.cpp
Normal file
41
sphaira/source/yati/source/stream.cpp
Normal file
@@ -0,0 +1,41 @@
|
||||
#include "yati/source/stream.hpp"
|
||||
#include "defines.hpp"
|
||||
#include "log.hpp"
|
||||
|
||||
namespace sphaira::yati::source {
|
||||
|
||||
Result Stream::Read(void* _buf, s64 off, s64 size, u64* bytes_read_out) {
|
||||
// streams don't allow for random access (seeking backwards).
|
||||
R_UNLESS(off >= m_offset, 0x1);
|
||||
|
||||
auto buf = static_cast<u8*>(_buf);
|
||||
*bytes_read_out = 0;
|
||||
|
||||
// check if we already have some data in the buffer.
|
||||
while (size) {
|
||||
// while it is invalid to seek backwards, it is valid to seek forwards.
|
||||
// this can be done to skip padding, skip undeeded files etc.
|
||||
// to handle this, simply read the data into a buffer and discard it.
|
||||
if (off > m_offset) {
|
||||
const auto skip_size = off - m_offset;
|
||||
std::vector<u8> temp_buf(skip_size);
|
||||
u64 bytes_read;
|
||||
R_TRY(ReadChunk(temp_buf.data(), temp_buf.size(), &bytes_read));
|
||||
|
||||
m_offset += bytes_read;
|
||||
} else {
|
||||
u64 bytes_read;
|
||||
R_TRY(ReadChunk(buf, size, &bytes_read));
|
||||
|
||||
*bytes_read_out += bytes_read;
|
||||
buf += bytes_read;
|
||||
off += bytes_read;
|
||||
m_offset += bytes_read;
|
||||
size -= bytes_read;
|
||||
}
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
} // namespace sphaira::yati::source
|
||||
23
sphaira/source/yati/source/stream_file.cpp
Normal file
23
sphaira/source/yati/source/stream_file.cpp
Normal file
@@ -0,0 +1,23 @@
|
||||
#include "yati/source/stream_file.hpp"
|
||||
#include "log.hpp"
|
||||
|
||||
namespace sphaira::yati::source {
|
||||
|
||||
StreamFile::StreamFile(FsFileSystem* fs, const fs::FsPath& path) {
|
||||
m_open_result = fsFsOpenFile(fs, path, FsOpenMode_Read, std::addressof(m_file));
|
||||
}
|
||||
|
||||
StreamFile::~StreamFile() {
|
||||
if (R_SUCCEEDED(GetOpenResult())) {
|
||||
fsFileClose(std::addressof(m_file));
|
||||
}
|
||||
}
|
||||
|
||||
Result StreamFile::ReadChunk(void* buf, s64 size, u64* bytes_read) {
|
||||
R_TRY(GetOpenResult());
|
||||
const auto rc = fsFileRead(std::addressof(m_file), m_offset, buf, size, 0, bytes_read);
|
||||
m_offset += *bytes_read;
|
||||
return rc;
|
||||
}
|
||||
|
||||
} // namespace sphaira::yati::source
|
||||
383
sphaira/source/yati/source/usb.cpp
Normal file
383
sphaira/source/yati/source/usb.cpp
Normal file
@@ -0,0 +1,383 @@
|
||||
/*
|
||||
* Copyright (c) Atmosphère-NX
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms and conditions of the GNU General Public License,
|
||||
* version 2, as published by the Free Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// Most of the usb transfer code was taken from Haze.
|
||||
#include "yati/source/usb.hpp"
|
||||
#include "log.hpp"
|
||||
|
||||
namespace sphaira::yati::source {
|
||||
namespace {
|
||||
|
||||
constexpr u32 MAGIC = 0x53504841;
|
||||
constexpr u32 VERSION = 2;
|
||||
|
||||
struct SendHeader {
|
||||
u32 magic;
|
||||
u32 version;
|
||||
};
|
||||
|
||||
struct RecvHeader {
|
||||
u32 magic;
|
||||
u32 version;
|
||||
u32 bcdUSB;
|
||||
u32 count;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
Usb::Usb(u64 transfer_timeout) {
|
||||
m_open_result = usbDsInitialize();
|
||||
m_transfer_timeout = transfer_timeout;
|
||||
}
|
||||
|
||||
Usb::~Usb() {
|
||||
if (R_SUCCEEDED(GetOpenResult())) {
|
||||
usbDsExit();
|
||||
}
|
||||
}
|
||||
|
||||
Result Usb::Init() {
|
||||
log_write("doing USB init\n");
|
||||
R_TRY(m_open_result);
|
||||
|
||||
u8 iManufacturer, iProduct, iSerialNumber;
|
||||
static const u16 supported_langs[1] = {0x0409};
|
||||
// Send language descriptor
|
||||
R_TRY(usbDsAddUsbLanguageStringDescriptor(NULL, supported_langs, sizeof(supported_langs)/sizeof(u16)));
|
||||
// Send manufacturer
|
||||
R_TRY(usbDsAddUsbStringDescriptor(&iManufacturer, "Nintendo"));
|
||||
// Send product
|
||||
R_TRY(usbDsAddUsbStringDescriptor(&iProduct, "Nintendo Switch"));
|
||||
// Send serial number
|
||||
R_TRY(usbDsAddUsbStringDescriptor(&iSerialNumber, "SerialNumber"));
|
||||
|
||||
// Send device descriptors
|
||||
struct usb_device_descriptor device_descriptor = {
|
||||
.bLength = USB_DT_DEVICE_SIZE,
|
||||
.bDescriptorType = USB_DT_DEVICE,
|
||||
.bcdUSB = 0x0110,
|
||||
.bDeviceClass = 0x00,
|
||||
.bDeviceSubClass = 0x00,
|
||||
.bDeviceProtocol = 0x00,
|
||||
.bMaxPacketSize0 = 0x40,
|
||||
.idVendor = 0x057e,
|
||||
.idProduct = 0x3000,
|
||||
.bcdDevice = 0x0100,
|
||||
.iManufacturer = iManufacturer,
|
||||
.iProduct = iProduct,
|
||||
.iSerialNumber = iSerialNumber,
|
||||
.bNumConfigurations = 0x01
|
||||
};
|
||||
|
||||
// Full Speed is USB 1.1
|
||||
R_TRY(usbDsSetUsbDeviceDescriptor(UsbDeviceSpeed_Full, &device_descriptor));
|
||||
|
||||
// High Speed is USB 2.0
|
||||
device_descriptor.bcdUSB = 0x0200;
|
||||
R_TRY(usbDsSetUsbDeviceDescriptor(UsbDeviceSpeed_High, &device_descriptor));
|
||||
|
||||
// Super Speed is USB 3.0
|
||||
device_descriptor.bcdUSB = 0x0300;
|
||||
// Upgrade packet size to 512
|
||||
device_descriptor.bMaxPacketSize0 = 0x09;
|
||||
R_TRY(usbDsSetUsbDeviceDescriptor(UsbDeviceSpeed_Super, &device_descriptor));
|
||||
|
||||
// Define Binary Object Store
|
||||
const u8 bos[0x16] = {
|
||||
0x05, // .bLength
|
||||
USB_DT_BOS, // .bDescriptorType
|
||||
0x16, 0x00, // .wTotalLength
|
||||
0x02, // .bNumDeviceCaps
|
||||
|
||||
// USB 2.0
|
||||
0x07, // .bLength
|
||||
USB_DT_DEVICE_CAPABILITY, // .bDescriptorType
|
||||
0x02, // .bDevCapabilityType
|
||||
0x02, 0x00, 0x00, 0x00, // dev_capability_data
|
||||
|
||||
// USB 3.0
|
||||
0x0A, // .bLength
|
||||
USB_DT_DEVICE_CAPABILITY, // .bDescriptorType
|
||||
0x03, /* .bDevCapabilityType */
|
||||
0x00, /* .bmAttributes */
|
||||
0x0E, 0x00, /* .wSpeedSupported */
|
||||
0x03, /* .bFunctionalitySupport */
|
||||
0x00, /* .bU1DevExitLat */
|
||||
0x00, 0x00 /* .bU2DevExitLat */
|
||||
};
|
||||
|
||||
R_TRY(usbDsSetBinaryObjectStore(bos, sizeof(bos)));
|
||||
|
||||
struct usb_interface_descriptor interface_descriptor = {
|
||||
.bLength = USB_DT_INTERFACE_SIZE,
|
||||
.bDescriptorType = USB_DT_INTERFACE,
|
||||
.bInterfaceNumber = USBDS_DEFAULT_InterfaceNumber, // set below
|
||||
.bNumEndpoints = static_cast<u8>(std::size(m_endpoints)),
|
||||
.bInterfaceClass = USB_CLASS_VENDOR_SPEC,
|
||||
.bInterfaceSubClass = USB_CLASS_VENDOR_SPEC,
|
||||
.bInterfaceProtocol = USB_CLASS_VENDOR_SPEC,
|
||||
};
|
||||
|
||||
|
||||
struct usb_endpoint_descriptor endpoint_descriptor_in = {
|
||||
.bLength = USB_DT_ENDPOINT_SIZE,
|
||||
.bDescriptorType = USB_DT_ENDPOINT,
|
||||
.bEndpointAddress = USB_ENDPOINT_IN,
|
||||
.bmAttributes = USB_TRANSFER_TYPE_BULK,
|
||||
.wMaxPacketSize = 0x40,
|
||||
};
|
||||
|
||||
struct usb_endpoint_descriptor endpoint_descriptor_out = {
|
||||
.bLength = USB_DT_ENDPOINT_SIZE,
|
||||
.bDescriptorType = USB_DT_ENDPOINT,
|
||||
.bEndpointAddress = USB_ENDPOINT_OUT,
|
||||
.bmAttributes = USB_TRANSFER_TYPE_BULK,
|
||||
.wMaxPacketSize = 0x40,
|
||||
};
|
||||
|
||||
const struct usb_ss_endpoint_companion_descriptor endpoint_companion = {
|
||||
.bLength = sizeof(struct usb_ss_endpoint_companion_descriptor),
|
||||
.bDescriptorType = USB_DT_SS_ENDPOINT_COMPANION,
|
||||
.bMaxBurst = 0x0F,
|
||||
.bmAttributes = 0x00,
|
||||
.wBytesPerInterval = 0x00,
|
||||
};
|
||||
|
||||
R_TRY(usbDsRegisterInterface(&m_interface));
|
||||
|
||||
interface_descriptor.bInterfaceNumber = m_interface->interface_index;
|
||||
endpoint_descriptor_in.bEndpointAddress += interface_descriptor.bInterfaceNumber + 1;
|
||||
endpoint_descriptor_out.bEndpointAddress += interface_descriptor.bInterfaceNumber + 1;
|
||||
|
||||
// Full Speed Config
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Full, &interface_descriptor, USB_DT_INTERFACE_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Full, &endpoint_descriptor_in, USB_DT_ENDPOINT_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Full, &endpoint_descriptor_out, USB_DT_ENDPOINT_SIZE));
|
||||
|
||||
// High Speed Config
|
||||
endpoint_descriptor_in.wMaxPacketSize = 0x200;
|
||||
endpoint_descriptor_out.wMaxPacketSize = 0x200;
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_High, &interface_descriptor, USB_DT_INTERFACE_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_High, &endpoint_descriptor_in, USB_DT_ENDPOINT_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_High, &endpoint_descriptor_out, USB_DT_ENDPOINT_SIZE));
|
||||
|
||||
// Super Speed Config
|
||||
endpoint_descriptor_in.wMaxPacketSize = 0x400;
|
||||
endpoint_descriptor_out.wMaxPacketSize = 0x400;
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &interface_descriptor, USB_DT_INTERFACE_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &endpoint_descriptor_in, USB_DT_ENDPOINT_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &endpoint_companion, USB_DT_SS_ENDPOINT_COMPANION_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &endpoint_descriptor_out, USB_DT_ENDPOINT_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &endpoint_companion, USB_DT_SS_ENDPOINT_COMPANION_SIZE));
|
||||
|
||||
//Setup endpoints.
|
||||
R_TRY(usbDsInterface_RegisterEndpoint(m_interface, &m_endpoints[UsbSessionEndpoint_In], endpoint_descriptor_in.bEndpointAddress));
|
||||
R_TRY(usbDsInterface_RegisterEndpoint(m_interface, &m_endpoints[UsbSessionEndpoint_Out], endpoint_descriptor_out.bEndpointAddress));
|
||||
|
||||
R_TRY(usbDsInterface_EnableInterface(m_interface));
|
||||
R_TRY(usbDsEnable());
|
||||
|
||||
log_write("success USB init\n");
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Usb::WaitForConnection(u64 timeout, u32& speed, u32& count) {
|
||||
const SendHeader send_header{
|
||||
.magic = MAGIC,
|
||||
.version = VERSION,
|
||||
};
|
||||
|
||||
alignas(0x1000) u8 aligned[0x1000]{};
|
||||
std::memcpy(aligned, std::addressof(send_header), sizeof(send_header));
|
||||
|
||||
// send header.
|
||||
u32 transferredSize;
|
||||
R_TRY(TransferPacketImpl(false, aligned, sizeof(send_header), &transferredSize, timeout));
|
||||
|
||||
// receive header.
|
||||
struct RecvHeader recv_header{};
|
||||
R_TRY(TransferPacketImpl(true, aligned, sizeof(recv_header), &transferredSize, timeout));
|
||||
|
||||
// copy data into header struct.
|
||||
std::memcpy(&recv_header, aligned, sizeof(recv_header));
|
||||
|
||||
// validate received header.
|
||||
R_UNLESS(recv_header.magic == MAGIC, Result_BadMagic);
|
||||
R_UNLESS(recv_header.version == VERSION, Result_BadVersion);
|
||||
R_UNLESS(recv_header.count > 0, Result_BadCount);
|
||||
|
||||
count = recv_header.count;
|
||||
speed = recv_header.bcdUSB;
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Usb::GetFileInfo(std::string& name_out, u64& size_out) {
|
||||
struct {
|
||||
u64 size;
|
||||
u64 name_length;
|
||||
} file_info_meta;
|
||||
|
||||
alignas(0x1000) u8 aligned[0x1000]{};
|
||||
|
||||
// receive meta.
|
||||
u32 transferredSize;
|
||||
R_TRY(TransferPacketImpl(true, aligned, sizeof(file_info_meta), &transferredSize, m_transfer_timeout));
|
||||
std::memcpy(&file_info_meta, aligned, sizeof(file_info_meta));
|
||||
R_UNLESS(file_info_meta.name_length < sizeof(aligned), 0x1);
|
||||
|
||||
R_TRY(TransferPacketImpl(true, aligned, file_info_meta.name_length, &transferredSize, m_transfer_timeout));
|
||||
name_out.resize(file_info_meta.name_length);
|
||||
std::memcpy(name_out.data(), aligned, name_out.size());
|
||||
|
||||
size_out = file_info_meta.size;
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
bool Usb::GetConfigured() const {
|
||||
UsbState usb_state;
|
||||
usbDsGetState(std::addressof(usb_state));
|
||||
return usb_state == UsbState_Configured;
|
||||
}
|
||||
|
||||
Event *Usb::GetCompletionEvent(UsbSessionEndpoint ep) const {
|
||||
return std::addressof(m_endpoints[ep]->CompletionEvent);
|
||||
}
|
||||
|
||||
Result Usb::WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) const {
|
||||
auto event = GetCompletionEvent(ep);
|
||||
const auto rc = eventWait(event, timeout);
|
||||
|
||||
if (R_FAILED(rc)) {
|
||||
R_TRY(usbDsEndpoint_Cancel(m_endpoints[ep]));
|
||||
eventClear(event);
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
Result Usb::TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 size, u32 *out_urb_id) const {
|
||||
return usbDsEndpoint_PostBufferAsync(m_endpoints[ep], buffer, size, out_urb_id);
|
||||
}
|
||||
|
||||
Result Usb::GetTransferResult(UsbSessionEndpoint ep, u32 urb_id, u32 *out_requested_size, u32 *out_transferred_size) const {
|
||||
UsbDsReportData report_data;
|
||||
|
||||
R_TRY(eventClear(std::addressof(m_endpoints[ep]->CompletionEvent)));
|
||||
R_TRY(usbDsEndpoint_GetReportData(m_endpoints[ep], std::addressof(report_data)));
|
||||
R_TRY(usbDsParseReportData(std::addressof(report_data), urb_id, out_requested_size, out_transferred_size));
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Usb::TransferPacketImpl(bool read, void *page, u32 size, u32 *out_size_transferred, u64 timeout) const {
|
||||
u32 urb_id;
|
||||
|
||||
/* If we're not configured yet, wait to become configured first. */
|
||||
// R_TRY(usbDsWaitReady(timeout));
|
||||
if (!GetConfigured()) {
|
||||
R_TRY(eventWait(usbDsGetStateChangeEvent(), timeout));
|
||||
R_TRY(eventClear(usbDsGetStateChangeEvent()));
|
||||
R_THROW(0xEA01);
|
||||
}
|
||||
|
||||
/* Select the appropriate endpoint and begin a transfer. */
|
||||
const auto ep = read ? UsbSessionEndpoint_Out : UsbSessionEndpoint_In;
|
||||
R_TRY(TransferAsync(ep, page, size, std::addressof(urb_id)));
|
||||
|
||||
/* Try to wait for the event. */
|
||||
R_TRY(WaitTransferCompletion(ep, timeout));
|
||||
|
||||
/* Return what we transferred. */
|
||||
return GetTransferResult(ep, urb_id, nullptr, out_size_transferred);
|
||||
}
|
||||
|
||||
Result Usb::SendCommand(s64 off, s64 size) const {
|
||||
struct {
|
||||
u32 hash;
|
||||
u32 magic;
|
||||
s64 off;
|
||||
s64 size;
|
||||
} meta{0, 0, off, size};
|
||||
|
||||
alignas(0x1000) static u8 aligned[0x1000]{};
|
||||
std::memcpy(aligned, std::addressof(meta), sizeof(meta));
|
||||
|
||||
u32 transferredSize;
|
||||
return TransferPacketImpl(false, aligned, sizeof(meta), &transferredSize, m_transfer_timeout);
|
||||
}
|
||||
|
||||
Result Usb::Finished() const {
|
||||
return SendCommand(0, 0);
|
||||
}
|
||||
|
||||
Result Usb::InternalRead(void* _buf, s64 off, s64 size) const {
|
||||
u8* buf = (u8*)_buf;
|
||||
alignas(0x1000) u8 aligned[0x1000]{};
|
||||
const auto stored_size = size;
|
||||
s64 total = 0;
|
||||
|
||||
while (size) {
|
||||
auto read_size = size;
|
||||
auto read_buf = buf;
|
||||
|
||||
if (u64(buf) & 0xFFF) {
|
||||
read_size = std::min<u64>(size, sizeof(aligned) - (u64(buf) & 0xFFF));
|
||||
read_buf = aligned;
|
||||
log_write("unaligned read %zd %zd read_size: %zd align: %zd\n", off, size, read_size, u64(buf) & 0xFFF);
|
||||
} else if (read_size & 0xFFF) {
|
||||
if (read_size <= 0xFFF) {
|
||||
log_write("unaligned small read %zd %zd read_size: %zd align: %zd\n", off, size, read_size, u64(buf) & 0xFFF);
|
||||
read_buf = aligned;
|
||||
} else {
|
||||
log_write("unaligned big read %zd %zd read_size: %zd align: %zd\n", off, size, read_size, u64(buf) & 0xFFF);
|
||||
// read as much as possible into buffer, the rest will
|
||||
// be handled in a second read which will be aligned size aligned.
|
||||
read_size = read_size & ~0xFFF;
|
||||
}
|
||||
}
|
||||
|
||||
R_TRY(SendCommand(off, read_size));
|
||||
|
||||
u32 transferredSize{};
|
||||
R_TRY(TransferPacketImpl(true, read_buf, read_size, &transferredSize, m_transfer_timeout));
|
||||
R_UNLESS(transferredSize <= read_size, Result_BadTransferSize);
|
||||
|
||||
if (read_buf == aligned) {
|
||||
std::memcpy(buf, aligned, transferredSize);
|
||||
}
|
||||
|
||||
if (transferredSize < read_size) {
|
||||
log_write("reading less than expected! %u vs %zd stored: %zd\n", transferredSize, read_size, stored_size);
|
||||
}
|
||||
|
||||
off += transferredSize;
|
||||
buf += transferredSize;
|
||||
size -= transferredSize;
|
||||
total += transferredSize;
|
||||
}
|
||||
|
||||
R_UNLESS(total == stored_size, Result_BadTotalSize);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Usb::Read(void* buf, s64 off, s64 size, u64* bytes_read) {
|
||||
R_TRY(GetOpenResult());
|
||||
R_TRY(InternalRead(buf, off, size));
|
||||
*bytes_read = size;
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
} // namespace sphaira::yati::source
|
||||
1482
sphaira/source/yati/yati.cpp
Normal file
1482
sphaira/source/yati/yati.cpp
Normal file
File diff suppressed because it is too large
Load Diff
1
tools/requirements.txt
Normal file
1
tools/requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
pyusb
|
||||
141
tools/usb_total.py
Normal file
141
tools/usb_total.py
Normal file
@@ -0,0 +1,141 @@
|
||||
# based on usb.py from Tinfoil, by Adubbz.
|
||||
import struct
|
||||
import sys
|
||||
import os
|
||||
import usb.core
|
||||
import usb.util
|
||||
import time
|
||||
import glob
|
||||
from pathlib import Path
|
||||
|
||||
# magic number (SPHA) for the script and switch.
|
||||
MAGIC = 0x53504841
|
||||
# version of the usb script.
|
||||
VERSION = 2
|
||||
# list of supported extensions.
|
||||
EXTS = (".nsp", ".xci", ".nsz", ".xcz")
|
||||
|
||||
def verify_switch(bcdUSB, count, in_ep, out_ep):
|
||||
header = in_ep.read(8, timeout=0)
|
||||
switch_magic = struct.unpack('<I', header[0:4])[0]
|
||||
switch_version = struct.unpack('<I', header[4:8])[0]
|
||||
|
||||
if switch_magic != MAGIC:
|
||||
raise Exception("Unexpected magic {}".format(switch_magic))
|
||||
|
||||
if switch_version != VERSION:
|
||||
raise Exception("Unexpected version {}".format(switch_version))
|
||||
|
||||
send_data = struct.pack('<IIII', MAGIC, VERSION, bcdUSB, count)
|
||||
out_ep.write(data=send_data, timeout=0)
|
||||
|
||||
def send_file_info(path, in_ep, out_ep):
|
||||
file_name = Path(path).name
|
||||
file_size = Path(path).stat().st_size
|
||||
file_name_len = len(file_name)
|
||||
|
||||
send_data = struct.pack('<QQ', file_size, file_name_len)
|
||||
out_ep.write(data=send_data, timeout=0)
|
||||
out_ep.write(data=file_name, timeout=0)
|
||||
|
||||
def wait_for_input(path, in_ep, out_ep):
|
||||
buf = None
|
||||
predicted_off = 0
|
||||
print("now waiting for intput\n")
|
||||
|
||||
with open(path, "rb") as file:
|
||||
while True:
|
||||
header = in_ep.read(24, timeout=0)
|
||||
|
||||
range_offset = struct.unpack('<Q', header[8:16])[0]
|
||||
range_size = struct.unpack('<Q', header[16:24])[0]
|
||||
|
||||
if (range_offset == 0 and range_size == 0):
|
||||
break
|
||||
|
||||
if (buf != None and range_offset == predicted_off and range_size == len(buf)):
|
||||
# print("predicted the read off {} size {}".format(predicted_off, len(buf)))
|
||||
pass
|
||||
else:
|
||||
file.seek(range_offset)
|
||||
buf = file.read(range_size)
|
||||
|
||||
if (len(buf) != range_size):
|
||||
# print("off: {} size: {}".format(range_offset, range_size))
|
||||
raise ValueError('bad buf size!!!!!')
|
||||
|
||||
result = out_ep.write(data=buf, timeout=0)
|
||||
if (len(buf) != result):
|
||||
print("off: {} size: {}".format(range_offset, range_size))
|
||||
raise ValueError('bad result!!!!!')
|
||||
|
||||
predicted_off = range_offset + range_size
|
||||
buf = file.read(range_size)
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("hello world")
|
||||
|
||||
# check which mode the user has selected.
|
||||
args = len(sys.argv)
|
||||
if (args != 2):
|
||||
print("either run python usb_total.py game.nsp OR drag and drop the game onto the python file (if python is in your path)")
|
||||
sys.exit(1)
|
||||
|
||||
path = sys.argv[1]
|
||||
files = []
|
||||
|
||||
if os.path.isfile(path) and path.endswith(EXTS):
|
||||
files.append(path)
|
||||
elif os.path.isdir(path):
|
||||
for f in glob.glob(path + "/**/*.*", recursive=True):
|
||||
if os.path.isfile(f) and f.endswith(EXTS):
|
||||
files.append(f)
|
||||
else:
|
||||
raise ValueError('must be a file!')
|
||||
|
||||
# for file in files:
|
||||
# print("found file: {}".format(file))
|
||||
|
||||
# Find the switch
|
||||
print("waiting for switch...\n")
|
||||
dev = None
|
||||
|
||||
while (dev is None):
|
||||
dev = usb.core.find(idVendor=0x057E, idProduct=0x3000)
|
||||
time.sleep(0.5)
|
||||
|
||||
print("found the switch!\n")
|
||||
|
||||
cfg = None
|
||||
|
||||
try:
|
||||
cfg = dev.get_active_configuration()
|
||||
print("found active config")
|
||||
except usb.core.USBError:
|
||||
print("no currently active config")
|
||||
cfg = None
|
||||
|
||||
if cfg is None:
|
||||
dev.set_configuration()
|
||||
cfg = dev.get_active_configuration()
|
||||
|
||||
is_out_ep = lambda ep: usb.util.endpoint_direction(ep.bEndpointAddress) == usb.util.ENDPOINT_OUT
|
||||
is_in_ep = lambda ep: usb.util.endpoint_direction(ep.bEndpointAddress) == usb.util.ENDPOINT_IN
|
||||
out_ep = usb.util.find_descriptor(cfg[(0,0)], custom_match=is_out_ep)
|
||||
in_ep = usb.util.find_descriptor(cfg[(0,0)], custom_match=is_in_ep)
|
||||
assert out_ep is not None
|
||||
assert in_ep is not None
|
||||
|
||||
print("iManufacturer: {} iProduct: {} iSerialNumber: {}".format(dev.manufacturer, dev.product, dev.serial_number))
|
||||
print("bcdUSB: {} bMaxPacketSize0: {}".format(hex(dev.bcdUSB), dev.bMaxPacketSize0))
|
||||
|
||||
try:
|
||||
verify_switch(dev.bcdUSB, len(files), in_ep, out_ep)
|
||||
|
||||
for file in files:
|
||||
print("installing file: {}".format(file))
|
||||
send_file_info(file, in_ep, out_ep)
|
||||
wait_for_input(file, in_ep, out_ep)
|
||||
dev.reset()
|
||||
except Exception as inst:
|
||||
print("An exception occurred " + str(inst))
|
||||
Reference in New Issue
Block a user