9 Commits

Author SHA1 Message Date
ITotalJustice
90f8a62823 display useful info in ftp menu (ip, port, user, pass, ssid, passphrase) 2025-04-23 01:00:36 +01:00
ITotalJustice
e2a1c8b5e3 fix yati not setting correct version, add support for using zip name when creating forwarder, remove some dead code.
fixes #126
fixes #127
2025-04-22 23:15:16 +01:00
ITotalJustice
21f6f4b74d [skip ci] fix file assoc always using internal name, fix menu showing wrong time
fixes #126
2025-04-22 00:08:26 +01:00
ITotalJustice
75d3b3ee0d [skip-ci] initial support for stream installs, add ftp installs.
do NOT build or release binaries of this version, it is not complete and there will be dragons.
2025-04-21 23:23:59 +01:00
ITotalJustice
0dde379932 don't return from usb menu on error, wait until the user presses B 2025-04-21 13:33:36 +01:00
ITotalJustice
9800bbecdf add basic support for gamecard installing 2025-04-21 13:30:46 +01:00
ITotalJustice
60e915c255 enable screenshot permissions in applet mode. 2025-04-21 12:40:37 +01:00
ITotalJustice
786f8a42fa send file name and size via usb, add requirements.txt for usb.py 2025-04-21 01:41:20 +01:00
ITotalJustice
5a4a0f75f2 add support for mame and neogeo, as well as alias for rom folder names 2025-04-20 22:03:53 +01:00
42 changed files with 1627 additions and 375 deletions

View File

@@ -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

View File

@@ -2,3 +2,4 @@
path=/retroarch/cores/mame2000_libretro_libnx.nro
supported_extensions=zip|7z
database=MAME 2000
use_base_name=true

View File

@@ -2,3 +2,4 @@
path=/retroarch/cores/mame2003_libretro_libnx.nro
supported_extensions=zip
database=MAME 2003
use_base_name=true

View File

@@ -2,3 +2,4 @@
path=/retroarch/cores/mame2003_plus_libretro_libnx.nro
supported_extensions=zip
database=MAME 2003-Plus
use_base_name=true

View File

@@ -2,3 +2,4 @@
path=/retroarch/cores/xrick_libretro_libnx.nro
supported_extensions=zip
database=Rick Dangerous
use_base_name=true

View File

@@ -47,6 +47,8 @@ add_executable(sphaira
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
@@ -82,6 +84,8 @@ add_executable(sphaira
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
@@ -142,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
)

View File

@@ -64,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;
@@ -185,7 +185,6 @@ public:
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_patch_ticket{INI_SECTION, "patch_ticket", true};
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};

View File

@@ -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

View File

@@ -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

View File

@@ -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 {

View 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

View File

@@ -0,0 +1,38 @@
#pragma once
#include "ui/menus/menu_base.hpp"
#include "yati/container/base.hpp"
#include "yati/source/base.hpp"
namespace sphaira::ui::menu::gc {
enum class State {
// no gamecard inserted.
None,
// set whilst transfer is in progress.
Progress,
// set when the transfer is finished.
Done,
// set when no gamecard is inserted.
NotFound,
// failed to parse gamecard.
Failed,
};
struct Menu final : MenuBase {
Menu();
~Menu();
void Update(Controller* controller, TouchInfo* touch) override;
void Draw(NVGcontext* vg, Theme* theme) override;
Result ScanGamecard();
private:
std::unique_ptr<fs::FsNativeGameCard> m_fs{};
FsDeviceOperator m_dev_op{};
yati::container::Collections m_collections{};
State m_state{State::None};
};
} // namespace sphaira::ui::menu::gc

View File

@@ -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{};

View File

@@ -3,6 +3,7 @@
#include "yati/source/base.hpp"
#include <vector>
#include <string>
#include <memory>
#include <switch.h>
namespace sphaira::yati::container {
@@ -28,12 +29,15 @@ using Collections = std::vector<CollectionEntry>;
struct Base {
using Source = source::Base;
Base(Source* source) : m_source{source} { }
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:
Source* m_source;
std::shared_ptr<Source> m_source;
};
} // namespace sphaira::yati::container

View File

@@ -8,7 +8,6 @@ namespace sphaira::yati::container {
struct Nsp final : Base {
using Base::Base;
Result GetCollections(Collections& out) override;
static Result Validate(source::Base* source);
};
} // namespace sphaira::yati::container

View File

@@ -10,7 +10,6 @@ namespace sphaira::yati::container {
struct Xci final : Base {
using Base::Base;
Result GetCollections(Collections& out) override;
static Result Validate(source::Base* source);
};
} // namespace sphaira::yati::container

View File

@@ -78,6 +78,6 @@ 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, bool convert_personalised);
Result PatchTicket(std::span<u8> ticket, const keys::Keys& keys);
} // namespace sphaira::es

View File

@@ -10,6 +10,10 @@ struct Base {
// 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;
}

View File

@@ -8,7 +8,7 @@
namespace sphaira::yati::source {
struct Stdio final : Base {
Stdio(const char* path);
Stdio(const fs::FsPath& path);
~Stdio();
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override;

View 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

View 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

View File

@@ -25,6 +25,7 @@ struct Usb final : Base {
Result Init();
Result WaitForConnection(u64 timeout, u32& speed, u32& count);
Result GetFileInfo(std::string& name_out, u64& size_out);
private:
enum UsbSessionEndpoint {

View File

@@ -9,6 +9,7 @@
#include "fs.hpp"
#include "source/base.hpp"
#include "container/base.hpp"
#include "ui/progress_box.hpp"
#include <memory>
@@ -78,11 +79,6 @@ struct Config {
// installs tickets only.
bool ticket_only{};
// converts personalised tickets to common tickets, allows for offline play.
// this breaks ticket signature so es needs to be patched.
// modified common tickets are patched regardless of this setting.
bool patch_ticket{};
// flags to enable / disable install of specific types.
bool skip_base{};
bool skip_patch{};
@@ -116,12 +112,10 @@ struct Config {
bool lower_system_version{};
};
Result InstallFromFile(FsFileSystem* fs, const fs::FsPath& path);
Result InstallFromStdioFile(const char* path);
Result InstallFromSource(std::shared_ptr<source::Base> source);
Result InstallFromFile(ui::ProgressBox* pbox, FsFileSystem* fs, const fs::FsPath& path);
Result InstallFromStdioFile(ui::ProgressBox* pbox, const char* path);
Result InstallFromSource(ui::ProgressBox* pbox, std::shared_ptr<source::Base> source);
Result InstallFromStdioFile(ui::ProgressBox* pbox, const fs::FsPath& path);
Result InstallFromSource(ui::ProgressBox* pbox, std::shared_ptr<source::Base> source, const fs::FsPath& path);
Result InstallFromContainer(ui::ProgressBox* pbox, std::shared_ptr<container::Base> container);
Result InstallFromCollections(ui::ProgressBox* pbox, std::shared_ptr<source::Base> source, const container::Collections& collections);
} // namespace sphaira::yati

View File

@@ -544,7 +544,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;
}
@@ -1392,8 +1392,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");

View File

@@ -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" {

View File

@@ -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();
}

View File

@@ -60,11 +60,29 @@ constexpr std::string_view INSTALL_EXTENSIONS[] = {
struct RomDatabaseEntry {
std::string_view folder;
std::string_view database;
// 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"},
@@ -100,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"};
@@ -120,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);
}
@@ -184,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;
}
}
}
@@ -784,8 +812,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");
@@ -793,7 +820,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();
@@ -801,7 +828,7 @@ 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);
return R_SUCCEEDED(App::Install(pbox, config));
}));
@@ -912,32 +939,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
@@ -948,11 +975,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);
}
}
}
@@ -1009,6 +1034,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);
@@ -1044,7 +1073,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());

View File

@@ -0,0 +1,296 @@
#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();
}});
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>("Installing App"_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

View File

@@ -0,0 +1,187 @@
#include "ui/menus/gc_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::gc {
namespace {
auto InRange(u64 off, u64 offset, u64 size) -> bool {
return off < offset + size && off >= offset;
}
struct GcSource final : yati::source::Base {
GcSource(const yati::container::Collections& collections, fs::FsNativeGameCard* fs);
~GcSource();
Result Read(void* buf, s64 off, s64 size, u64* bytes_read);
const yati::container::Collections& m_collections;
fs::FsNativeGameCard* m_fs{};
FsFile m_file{};
s64 m_offset{};
s64 m_size{};
};
GcSource::GcSource(const yati::container::Collections& collections, fs::FsNativeGameCard* fs)
: m_collections{collections}
, m_fs{fs} {
m_offset = -1;
}
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
Menu::Menu() : MenuBase{"GameCard"_i18n} {
SetAction(Button::B, Action{"Back"_i18n, [this](){
SetPop();
}});
SetAction(Button::X, Action{"Refresh"_i18n, [this](){
m_state = State::None;
}});
fsOpenDeviceOperator(std::addressof(m_dev_op));
}
Menu::~Menu() {
// manually close this as it needs(?) to be closed before dev_op.
m_fs.reset();
fsDeviceOperatorClose(std::addressof(m_dev_op));
}
void Menu::Update(Controller* controller, TouchInfo* touch) {
MenuBase::Update(controller, touch);
switch (m_state) {
case State::None: {
bool gc_inserted;
if (R_FAILED(fsDeviceOperatorIsGameCardInserted(std::addressof(m_dev_op), std::addressof(gc_inserted)))) {
m_state = State::Failed;
} else {
if (!gc_inserted) {
m_state = State::NotFound;
} else {
if (R_FAILED(ScanGamecard())) {
m_state = State::Failed;
}
}
}
} break;
case State::Progress:
case State::Done:
case State::NotFound:
case State::Failed:
break;
}
}
void Menu::Draw(NVGcontext* vg, Theme* theme) {
MenuBase::Draw(vg, theme);
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::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::NotFound:
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "No GameCard inserted, press X to refresh"_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), "Installed GameCard, 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 scan GameCard..."_i18n.c_str());
break;
}
}
Result Menu::ScanGamecard() {
m_state = State::None;
m_fs.reset();
m_collections.clear();
FsGameCardHandle gc_handle;
R_TRY(fsDeviceOperatorGetGameCardHandle(std::addressof(m_dev_op), std::addressof(gc_handle)));
m_fs = std::make_unique<fs::FsNativeGameCard>(std::addressof(gc_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()));
m_collections.reserve(total_entries);
s64 offset{};
for (s64 i = 0; i < total_entries; i++) {
yati::container::CollectionEntry entry{};
entry.name = buf[i].name;
entry.offset = offset;
entry.size = buf[i].file_size;
m_collections.emplace_back(entry);
offset += buf[i].file_size;
}
m_state = State::Progress;
App::Push(std::make_shared<ui::ProgressBox>("Installing App"_i18n, [this](auto pbox) mutable -> bool {
auto source = std::make_shared<GcSource>(m_collections, m_fs.get());
return R_SUCCEEDED(yati::InstallFromCollections(pbox, source, m_collections));
}, [this](bool result){
if (result) {
App::Notify("Gc install success!"_i18n);
m_state = State::Done;
} else {
App::Notify("Gc install failed!"_i18n);
m_state = State::Failed;
}
}));
R_SUCCEED();
}
} // namespace sphaira::ui::menu::gc

View File

@@ -3,6 +3,8 @@
#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 "ui/sidebar.hpp"
#include "ui/popup_list.hpp"
@@ -320,9 +322,19 @@ MainMenu::MainMenu() {
}
if (App::GetApp()->m_install.Get()) {
if (App::GetFtpEnable()) {
options->Add(std::make_shared<SidebarEntryCallback>("Ftp Install"_i18n, [](){
App::Push(std::make_shared<menu::ftp::Menu>());
}));
}
options->Add(std::make_shared<SidebarEntryCallback>("Usb Install"_i18n, [](){
App::Push(std::make_shared<menu::usb::Menu>());
}));
options->Add(std::make_shared<SidebarEntryCallback>("GameCard Install"_i18n, [](){
App::Push(std::make_shared<menu::gc::Menu>());
}));
}
}));
@@ -379,10 +391,6 @@ MainMenu::MainMenu() {
App::GetApp()->m_ticket_only.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("Patch ticket"_i18n, App::GetApp()->m_patch_ticket.Get(), [this](bool& enable){
App::GetApp()->m_patch_ticket.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("Skip base"_i18n, App::GetApp()->m_skip_base.Get(), [this](bool& enable){
App::GetApp()->m_skip_base.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));

View File

@@ -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;

View File

@@ -102,7 +102,15 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
App::Push(std::make_shared<ui::ProgressBox>("Installing App"_i18n, [this](auto pbox) mutable -> bool {
log_write("inside progress box\n");
for (u32 i = 0; i < m_usb_count; i++) {
const auto rc = yati::InstallFromSource(pbox, m_usb_source);
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;
}
@@ -115,11 +123,11 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
}, [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;
}
m_state = State::Done;
this->SetPop();
}));
break;
@@ -153,12 +161,11 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
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());
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..."_i18n.c_str());
this->SetPop();
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;
}
}

View File

@@ -24,14 +24,6 @@ struct Pfs0FileTableEntry {
} // namespace
Result Nsp::Validate(source::Base* source) {
u32 magic;
u64 bytes_read;
R_TRY(source->Read(std::addressof(magic), 0, sizeof(magic), std::addressof(bytes_read)));
R_UNLESS(magic == PFS0_MAGIC, 0x1);
R_SUCCEED();
}
Result Nsp::GetCollections(Collections& out) {
u64 bytes_read;
s64 off = 0;

View File

@@ -60,22 +60,14 @@ Result Hfs0GetPartition(source::Base* source, s64 off, Hfs0& out) {
} // namespace
Result Xci::Validate(source::Base* source) {
u32 magic;
u64 bytes_read;
R_TRY(source->Read(std::addressof(magic), 0x100, sizeof(magic), std::addressof(bytes_read)));
R_UNLESS(magic == XCI_MAGIC, 0x1);
R_SUCCEED();
}
Result Xci::GetCollections(Collections& out) {
Hfs0 root{};
R_TRY(Hfs0GetPartition(m_source, HFS0_HEADER_OFFSET, 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, root.data_offset + root.file_table[i].data_offset, 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;

View File

@@ -106,13 +106,13 @@ Result DecryptTitleKey(keys::KeyEntry& out, u8 key_gen, const keys::Keys& keys)
// todo: i thought i already wrote the code for this??
// todo: patch the ticket.
Result PatchTicket(std::span<u8> ticket, const keys::Keys& keys, bool convert_personalised) {
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 && convert_personalised) {
} else if (data.title_key_type == es::TicketTitleKeyType_Personalized) {
}

View File

@@ -2,7 +2,7 @@
namespace sphaira::yati::source {
Stdio::Stdio(const char* path) {
Stdio::Stdio(const fs::FsPath& path) {
m_file = std::fopen(path, "rb");
if (!m_file) {
m_open_result = fsdevGetLastResult();

View 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

View 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

View File

@@ -22,7 +22,7 @@ namespace sphaira::yati::source {
namespace {
constexpr u32 MAGIC = 0x53504841;
constexpr u32 VERSION = 1;
constexpr u32 VERSION = 2;
struct SendHeader {
u32 magic;
@@ -224,6 +224,28 @@ Result Usb::WaitForConnection(u64 timeout, u32& speed, u32& count) {
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));

View File

@@ -1,5 +1,6 @@
#include "yati/yati.hpp"
#include "yati/source/file.hpp"
#include "yati/source/stream_file.hpp"
#include "yati/source/stdio.hpp"
#include "yati/container/nsp.hpp"
#include "yati/container/xci.hpp"
@@ -36,13 +37,13 @@ struct CustomVectorAllocator {
public:
// https://en.cppreference.com/w/cpp/memory/new/operator_new
auto allocate(std::size_t n) -> T* {
log_write("allocating ptr size: %zu\n", n);
// log_write("allocating ptr size: %zu\n", n);
return new(align) T[n];
}
// https://en.cppreference.com/w/cpp/memory/new/operator_delete
auto deallocate(T* p, std::size_t n) noexcept -> void {
log_write("deleting ptr size: %zu\n", n);
// log_write("deleting ptr size: %zu\n", n);
::operator delete[] (p, n, align);
}
@@ -62,17 +63,6 @@ using PageAlignedVector = std::vector<u8, PageAllocator<u8>>;
constexpr u32 KEYGEN_LIMIT = 0x20;
#if 0
struct FwVersion {
u32 value;
auto relstep() const -> u8 { return (value >> 0) & 0xFFFF; }
auto micro() const -> u8 { return (value >> 16) & 0x000F; }
auto minor() const -> u8 { return (value >> 20) & 0x003F; }
auto major() const -> u8 { return (value >> 26) & 0x003F; }
auto hos() const -> u32 { return MAKEHOSVERSION(major(), minor(), micro()); }
};
#endif
struct NcaCollection : container::CollectionEntry {
// NcmContentType
u8 type{};
@@ -284,7 +274,7 @@ struct Yati {
Yati(ui::ProgressBox*, std::shared_ptr<source::Base>);
~Yati();
Result Setup(container::Collections& out);
Result Setup();
Result InstallNca(std::span<TikCollection> tickets, NcaCollection& nca);
Result InstallCnmtNca(std::span<TikCollection> tickets, CnmtCollection& cnmt, const container::Collections& collections);
Result InstallControlNca(std::span<TikCollection> tickets, const CnmtCollection& cnmt, NcaCollection& nca);
@@ -293,6 +283,14 @@ struct Yati {
Result decompressFuncInternal(ThreadData* t);
Result writeFuncInternal(ThreadData* t);
Result ParseTicketsIntoCollection(std::vector<TikCollection>& tickets, const container::Collections& collections, bool read_data);
Result GetLatestVersion(const CnmtCollection& cnmt, u32& version_out, bool& skip);
Result ShouldSkip(const CnmtCollection& cnmt, bool& skip);
Result ImportTickets(std::span<TikCollection> tickets);
Result RemoveInstalledNcas(const CnmtCollection& cnmt);
Result RegisterNcasAndPushRecord(const CnmtCollection& cnmt, u32 latest_version_num);
// private:
ui::ProgressBox* pbox{};
std::shared_ptr<source::Base> source{};
@@ -362,8 +360,14 @@ HashStr hexIdToStr(auto id) {
// read thread reads all data from the source, it also handles
// parsing ncz headers, sections and reading ncz blocks
Result Yati::readFuncInternal(ThreadData* t) {
// the main buffer which data is read into.
PageAlignedVector buf;
// workaround ncz block reading ahead. if block isn't found, we usually
// would seek back to the offset, however this is not possible in stream
// mode, so we instead store the data to the temp buffer and pre-pend it.
PageAlignedVector temp_buf;
buf.reserve(t->max_buffer_size);
temp_buf.reserve(t->max_buffer_size);
while (t->read_offset < t->nca->size && R_SUCCEEDED(t->GetResults())) {
const auto buffer_offset = t->read_offset;
@@ -374,10 +378,18 @@ Result Yati::readFuncInternal(ThreadData* t) {
read_size = NCZ_SECTION_OFFSET;
}
s64 buf_offset = 0;
if (!temp_buf.empty()) {
buf = temp_buf;
read_size -= temp_buf.size();
buf_offset = temp_buf.size();
temp_buf.clear();
}
u64 bytes_read{};
buf.resize(read_size);
R_TRY(t->Read(buf.data(), read_size, std::addressof(bytes_read)));
auto buf_size = bytes_read;
buf.resize(buf_offset + read_size);
R_TRY(t->Read(buf.data() + buf_offset, read_size, std::addressof(bytes_read)));
auto buf_size = buf_offset + bytes_read;
// read enough bytes for ncz, check magic
if (t->read_offset == NCZ_SECTION_OFFSET) {
@@ -394,10 +406,12 @@ Result Yati::readFuncInternal(ThreadData* t) {
R_TRY(t->Read(t->ncz_sections.data(), t->ncz_sections.size() * sizeof(ncz::Section), std::addressof(bytes_read)));
// check for ncz block header.
const auto read_off = t->read_offset;
R_TRY(t->Read(std::addressof(t->ncz_block_header), sizeof(t->ncz_block_header), std::addressof(bytes_read)));
if (t->ncz_block_header.magic != NCZ_BLOCK_MAGIC) {
t->read_offset = read_off;
// didn't find block, keep the data we just read in the temp buffer.
temp_buf.resize(sizeof(t->ncz_block_header));
std::memcpy(temp_buf.data(), std::addressof(t->ncz_block_header), temp_buf.size());
log_write("storing temp data of size: %zu\n", temp_buf.size());
} else {
// validate block header.
R_UNLESS(t->ncz_block_header.version == 0x2, Result_InvalidNczBlockVersion);
@@ -778,12 +792,11 @@ Yati::~Yati() {
appletSetMediaPlaybackState(false);
}
Result Yati::Setup(container::Collections& out) {
Result Yati::Setup() {
config.sd_card_install = App::GetApp()->m_install_sd.Get();
config.allow_downgrade = App::GetApp()->m_allow_downgrade.Get();
config.skip_if_already_installed = App::GetApp()->m_skip_if_already_installed.Get();
config.ticket_only = App::GetApp()->m_ticket_only.Get();
config.patch_ticket = App::GetApp()->m_patch_ticket.Get();
config.skip_base = App::GetApp()->m_skip_base.Get();
config.skip_patch = App::GetApp()->m_skip_patch.Get();
config.skip_addon = App::GetApp()->m_skip_addon.Get();
@@ -812,19 +825,6 @@ Result Yati::Setup(container::Collections& out) {
cs = ncm_cs[config.sd_card_install];
db = ncm_db[config.sd_card_install];
if (R_SUCCEEDED(container::Nsp::Validate(source.get()))) {
log_write("found nsp\n");
container = std::make_unique<container::Nsp>(source.get());
} else if (R_SUCCEEDED(container::Xci::Validate(source.get()))) {
log_write("found xci\n");
container = std::make_unique<container::Xci>(source.get());
} else {
log_write("found unknown container\n");
}
R_UNLESS(container, Result_ContainerNotFound);
R_TRY(container->GetCollections(out));
R_TRY(parse_keys(keys, true));
R_SUCCEED();
}
@@ -904,7 +904,6 @@ Result Yati::InstallNca(std::span<TikCollection> tickets, NcaCollection& nca) {
std::memcpy(std::addressof(content_id), nca.hash, sizeof(content_id));
log_write("old id: %s new id: %s\n", hexIdToStr(nca.content_id).str, hexIdToStr(content_id).str);
log_write("doing register: %s\n", nca.name.c_str());
if (!config.skip_nca_hash_verify && !nca.modified) {
if (std::memcmp(&nca.content_id, nca.hash, sizeof(nca.content_id))) {
log_write("nca hash is invalid!!!!\n");
@@ -1040,13 +1039,7 @@ Result Yati::InstallControlNca(std::span<TikCollection> tickets, const CnmtColle
R_SUCCEED();
}
Result InstallInternal(ui::ProgressBox* pbox, std::shared_ptr<source::Base> source) {
auto yati = std::make_unique<Yati>(pbox, source);
container::Collections collections{};
R_TRY(yati->Setup(collections));
std::vector<TikCollection> tickets{};
Result Yati::ParseTicketsIntoCollection(std::vector<TikCollection>& tickets, const container::Collections& collections, bool read_data) {
for (const auto& collection : collections) {
if (collection.name.ends_with(".tik")) {
TikCollection entry{};
@@ -1061,17 +1054,226 @@ Result InstallInternal(ui::ProgressBox* pbox, std::shared_ptr<source::Base> sour
entry.ticket.resize(collection.size);
entry.cert.resize(cert->size);
u64 bytes_read;
R_TRY(source->Read(entry.ticket.data(), collection.offset, entry.ticket.size(), &bytes_read));
R_TRY(source->Read(entry.cert.data(), cert->offset, entry.cert.size(), &bytes_read));
// only supported on non-stream installs.
if (read_data) {
u64 bytes_read;
R_TRY(source->Read(entry.ticket.data(), collection.offset, entry.ticket.size(), &bytes_read));
R_TRY(source->Read(entry.cert.data(), cert->offset, entry.cert.size(), &bytes_read));
}
tickets.emplace_back(entry);
}
}
R_SUCCEED();
}
Result Yati::GetLatestVersion(const CnmtCollection& cnmt, u32& version_out, bool& skip) {
const auto app_id = ncm::GetAppId(cnmt.key);
bool has_records;
R_TRY(nsIsAnyApplicationEntityInstalled(app_id, &has_records));
// TODO: fix this when gamecard is inserted as it will only return records
// for the gamecard...
// may have to use ncm directly to get the keys, then parse that.
version_out = cnmt.key.version;
if (has_records) {
s32 meta_count{};
R_TRY(nsCountApplicationContentMeta(app_id, &meta_count));
R_UNLESS(meta_count > 0, 0x1);
std::vector<ncm::ContentStorageRecord> records(meta_count);
s32 count;
R_TRY(ns::ListApplicationRecordContentMeta(std::addressof(ns_app), 0, app_id, records.data(), records.size(), &count));
R_UNLESS(count == records.size(), 0x1);
for (auto& record : records) {
log_write("found record: 0x%016lX type: %u version: %u\n", record.key.id, record.key.type, record.key.version);
log_write("cnmt record: 0x%016lX type: %u version: %u\n", cnmt.key.id, cnmt.key.type, cnmt.key.version);
if (record.key.id == cnmt.key.id && cnmt.key.version == record.key.version && config.skip_if_already_installed) {
log_write("skipping as already installed\n");
skip = true;
}
// check if we are downgrading
if (cnmt.key.type == NcmContentMetaType_Patch) {
if (cnmt.key.type == record.key.type && cnmt.key.version < record.key.version && !config.allow_downgrade) {
log_write("skipping due to it being lower\n");
skip = true;
}
} else {
version_out = std::max(version_out, record.key.version);
}
}
}
R_SUCCEED();
}
Result Yati::ShouldSkip(const CnmtCollection& cnmt, bool& skip) {
// skip invalid types
if (!(cnmt.key.type & 0x80)) {
log_write("\tskipping: invalid: %u\n", cnmt.key.type);
skip = true;
} else if (config.skip_base && cnmt.key.type == NcmContentMetaType_Application) {
log_write("\tskipping: [NcmContentMetaType_Application]\n");
skip = true;
} else if (config.skip_patch && cnmt.key.type == NcmContentMetaType_Patch) {
log_write("\tskipping: [NcmContentMetaType_Application]\n");
skip = true;
} else if (config.skip_addon && cnmt.key.type == NcmContentMetaType_AddOnContent) {
log_write("\tskipping: [NcmContentMetaType_AddOnContent]\n");
skip = true;
} else if (config.skip_data_patch && cnmt.key.type == NcmContentMetaType_DataPatch) {
log_write("\tskipping: [NcmContentMetaType_DataPatch]\n");
skip = true;
}
R_SUCCEED();
}
Result Yati::ImportTickets(std::span<TikCollection> tickets) {
for (auto& ticket : tickets) {
if (ticket.required) {
if (config.skip_ticket) {
log_write("WARNING: skipping ticket install, but it's required!\n");
} else {
log_write("patching ticket\n");
R_TRY(es::PatchTicket(ticket.ticket, keys));
log_write("installing ticket\n");
R_TRY(es::ImportTicket(std::addressof(es), ticket.ticket.data(), ticket.ticket.size(), ticket.cert.data(), ticket.cert.size()));
ticket.required = false;
}
}
}
R_SUCCEED();
}
Result Yati::RemoveInstalledNcas(const CnmtCollection& cnmt) {
const auto app_id = ncm::GetAppId(cnmt.key);
// remove current entries (if any).
s32 db_list_total;
s32 db_list_count;
u64 id_min = cnmt.key.id;
u64 id_max = cnmt.key.id;
std::vector<NcmContentMetaKey> keys(1);
// if installing a patch, remove all previously installed patches.
if (cnmt.key.type == NcmContentMetaType_Patch) {
id_min = 0;
id_max = UINT64_MAX;
}
log_write("listing keys\n");
for (size_t i = 0; i < std::size(NCM_STORAGE_IDS); i++) {
auto& cs = ncm_cs[i];
auto& db = ncm_db[i];
std::vector<NcmContentMetaKey> keys(1);
R_TRY(ncmContentMetaDatabaseList(std::addressof(db), std::addressof(db_list_total), std::addressof(db_list_count), keys.data(), keys.size(), static_cast<NcmContentMetaType>(cnmt.key.type), app_id, id_min, id_max, NcmContentInstallType_Full));
if (db_list_total != keys.size()) {
keys.resize(db_list_total);
if (keys.size()) {
R_TRY(ncmContentMetaDatabaseList(std::addressof(db), std::addressof(db_list_total), std::addressof(db_list_count), keys.data(), keys.size(), static_cast<NcmContentMetaType>(cnmt.key.type), app_id, id_min, id_max, NcmContentInstallType_Full));
}
}
for (auto& key : keys) {
log_write("found key: 0x%016lX type: %u version: %u\n", key.id, key.type, key.version);
NcmContentMetaHeader header;
u64 out_size;
log_write("trying to get from db\n");
R_TRY(ncmContentMetaDatabaseGet(std::addressof(db), std::addressof(key), std::addressof(out_size), std::addressof(header), sizeof(header)));
R_UNLESS(out_size == sizeof(header), Result_NcmDbCorruptHeader);
log_write("trying to list infos\n");
std::vector<NcmContentInfo> infos(header.content_count);
s32 content_info_out;
R_TRY(ncmContentMetaDatabaseListContentInfo(std::addressof(db), std::addressof(content_info_out), infos.data(), infos.size(), std::addressof(key), 0));
R_UNLESS(content_info_out == infos.size(), Result_NcmDbCorruptInfos);
log_write("size matches\n");
for (auto& info : infos) {
R_TRY(ncm::Delete(std::addressof(cs), std::addressof(info.content_id)));
}
log_write("trying to remove it\n");
R_TRY(ncmContentMetaDatabaseRemove(std::addressof(db), std::addressof(key)));
R_TRY(ncmContentMetaDatabaseCommit(std::addressof(db)));
log_write("all done with this key\n\n");
}
}
log_write("done with keys\n");
R_SUCCEED();
}
Result Yati::RegisterNcasAndPushRecord(const CnmtCollection& cnmt, u32 latest_version_num) {
const auto app_id = ncm::GetAppId(cnmt.key);
// register all nca's
log_write("registering cnmt nca\n");
R_TRY(ncm::Register(std::addressof(cs), std::addressof(cnmt.content_id), std::addressof(cnmt.placeholder_id)));
log_write("registered cnmt nca\n");
for (auto& nca : cnmt.ncas) {
if (nca.type != NcmContentType_DeltaFragment) {
log_write("registering nca: %s\n", nca.name.c_str());
R_TRY(ncm::Register(std::addressof(cs), std::addressof(nca.content_id), std::addressof(nca.placeholder_id)));
log_write("registered nca: %s\n", nca.name.c_str());
}
}
log_write("register'd all ncas\n");
// build ncm meta and push to the database.
BufHelper buf{};
buf.write(std::addressof(cnmt.header), sizeof(cnmt.header));
buf.write(cnmt.extended_header.data(), cnmt.extended_header.size());
buf.write(std::addressof(cnmt.content_info), sizeof(cnmt.content_info));
for (auto& info : cnmt.infos) {
buf.write(std::addressof(info.info), sizeof(info.info));
}
pbox->NewTransfer("Updating ncm databse"_i18n);
R_TRY(ncmContentMetaDatabaseSet(std::addressof(db), std::addressof(cnmt.key), buf.buf.data(), buf.tell()));
R_TRY(ncmContentMetaDatabaseCommit(std::addressof(db)));
// push record.
ncm::ContentStorageRecord content_storage_record{};
content_storage_record.key = cnmt.key;
content_storage_record.storage_id = storage_id;
pbox->NewTransfer("Pushing application record"_i18n);
R_TRY(ns::PushApplicationRecord(std::addressof(ns_app), app_id, std::addressof(content_storage_record), 1));
if (hosversionAtLeast(6,0,0)) {
R_TRY(avmInitialize());
ON_SCOPE_EXIT(avmExit());
R_TRY(avmPushLaunchVersion(app_id, latest_version_num));
}
log_write("pushed\n");
R_SUCCEED();
}
Result InstallInternal(ui::ProgressBox* pbox, std::shared_ptr<source::Base> source, const container::Collections& collections) {
auto yati = std::make_unique<Yati>(pbox, source);
R_TRY(yati->Setup());
std::vector<TikCollection> tickets{};
R_TRY(yati->ParseTicketsIntoCollection(tickets, collections, true));
std::vector<CnmtCollection> cnmts{};
for (const auto& collection : collections) {
log_write("found collection: %s\n", collection.name.c_str());
if (collection.name.ends_with(".cnmt.nca")) {
if (collection.name.ends_with(".cnmt.nca") || collection.name.ends_with(".cnmt.ncz")) {
auto& cnmt = cnmts.emplace_back(NcaCollection{collection});
cnmt.type = NcmContentType_Meta;
}
@@ -1087,63 +1289,10 @@ Result InstallInternal(ui::ProgressBox* pbox, std::shared_ptr<source::Base> sour
R_TRY(yati->InstallCnmtNca(tickets, cnmt, collections));
u32 latest_version_num;
bool skip = false;
const auto app_id = ncm::GetAppId(cnmt.key);
bool has_records;
R_TRY(nsIsAnyApplicationEntityInstalled(app_id, &has_records));
// TODO: fix this when gamecard is inserted as it will only return records
// for the gamecard...
// may have to use ncm directly to get the keys, then parse that.
u32 latest_version_num = cnmt.key.version;
if (has_records) {
s32 meta_count{};
R_TRY(nsCountApplicationContentMeta(app_id, &meta_count));
R_UNLESS(meta_count > 0, 0x1);
std::vector<ncm::ContentStorageRecord> records(meta_count);
s32 count;
R_TRY(ns::ListApplicationRecordContentMeta(std::addressof(yati->ns_app), 0, app_id, records.data(), records.size(), &count));
R_UNLESS(count == records.size(), 0x1);
for (auto& record : records) {
log_write("found record: 0x%016lX type: %u version: %u\n", record.key.id, record.key.type, record.key.version);
log_write("cnmt record: 0x%016lX type: %u version: %u\n", cnmt.key.id, cnmt.key.type, cnmt.key.version);
if (record.key.id == cnmt.key.id && cnmt.key.version == record.key.version && yati->config.skip_if_already_installed) {
log_write("skipping as already installed\n");
skip = true;
}
// check if we are downgrading
if (cnmt.key.type == NcmContentMetaType_Patch) {
if (cnmt.key.type == record.key.type && cnmt.key.version < record.key.version && !yati->config.allow_downgrade) {
log_write("skipping due to it being lower\n");
skip = true;
}
} else {
latest_version_num = std::max(latest_version_num, record.key.version);
}
}
}
// skip invalid types
if (!(cnmt.key.type & 0x80)) {
log_write("\tskipping: invalid: %u\n", cnmt.key.type);
skip = true;
} else if (yati->config.skip_base && cnmt.key.type == NcmContentMetaType_Application) {
log_write("\tskipping: [NcmContentMetaType_Application]\n");
skip = true;
} else if (yati->config.skip_patch && cnmt.key.type == NcmContentMetaType_Patch) {
log_write("\tskipping: [NcmContentMetaType_Application]\n");
skip = true;
} else if (yati->config.skip_addon && cnmt.key.type == NcmContentMetaType_AddOnContent) {
log_write("\tskipping: [NcmContentMetaType_AddOnContent]\n");
skip = true;
} else if (yati->config.skip_data_patch && cnmt.key.type == NcmContentMetaType_DataPatch) {
log_write("\tskipping: [NcmContentMetaType_DataPatch]\n");
skip = true;
}
R_TRY(yati->GetLatestVersion(cnmt, latest_version_num, skip));
R_TRY(yati->ShouldSkip(cnmt, skip));
if (skip) {
log_write("skipping install!\n");
@@ -1160,125 +1309,104 @@ Result InstallInternal(ui::ProgressBox* pbox, std::shared_ptr<source::Base> sour
}
}
// log_write("exiting early :)\n");
// return 0;
R_TRY(yati->ImportTickets(tickets));
R_TRY(yati->RemoveInstalledNcas(cnmt));
R_TRY(yati->RegisterNcasAndPushRecord(cnmt, latest_version_num));
}
for (auto& ticket : tickets) {
if (ticket.required) {
if (yati->config.skip_ticket) {
log_write("WARNING: skipping ticket install, but it's required!\n");
} else {
log_write("patching ticket\n");
if (yati->config.patch_ticket) {
R_TRY(es::PatchTicket(ticket.ticket, yati->keys, false));
}
log_write("installing ticket\n");
R_TRY(es::ImportTicket(std::addressof(yati->es), ticket.ticket.data(), ticket.ticket.size(), ticket.cert.data(), ticket.cert.size()));
ticket.required = false;
}
log_write("success!\n");
R_SUCCEED();
}
Result InstallInternalStream(ui::ProgressBox* pbox, std::shared_ptr<source::Base> source, container::Collections collections) {
auto yati = std::make_unique<Yati>(pbox, source);
R_TRY(yati->Setup());
// not supported with stream installs (yet).
yati->config.convert_to_standard_crypto = false;
yati->config.lower_master_key = false;
std::vector<NcaCollection> ncas{};
std::vector<CnmtCollection> cnmts{};
std::vector<TikCollection> tickets{};
ON_SCOPE_EXIT(
for (const auto& cnmt : cnmts) {
ncmContentStorageDeletePlaceHolder(std::addressof(yati->cs), std::addressof(cnmt.placeholder_id));
}
for (const auto& nca : ncas) {
ncmContentStorageDeletePlaceHolder(std::addressof(yati->cs), std::addressof(nca.placeholder_id));
}
);
// fill ticket entries, the data will be filled later on.
R_TRY(yati->ParseTicketsIntoCollection(tickets, collections, false));
// sort based on lowest offset.
const auto sorter = [](const container::CollectionEntry& lhs, const container::CollectionEntry& rhs) -> bool {
return lhs.offset < rhs.offset;
};
std::sort(collections.begin(), collections.end(), sorter);
for (const auto& collection : collections) {
if (collection.name.ends_with(".nca") || collection.name.ends_with(".ncz")) {
auto& nca = ncas.emplace_back(NcaCollection{collection});
if (collection.name.ends_with(".cnmt.nca") || collection.name.ends_with(".cnmt.ncz")) {
auto& cnmt = cnmts.emplace_back(nca);
cnmt.type = NcmContentType_Meta;
R_TRY(yati->InstallCnmtNca(tickets, cnmt, collections));
} else {
R_TRY(yati->InstallNca(tickets, nca));
}
} else if (collection.name.ends_with(".tik") || collection.name.ends_with(".cert")) {
FsRightsId rights_id{};
keys::parse_hex_key(rights_id.c, collection.name.c_str());
const auto str = collection.name.substr(0, collection.name.length() - 4) + ".cert";
auto entry = std::find_if(tickets.begin(), tickets.end(), [rights_id](auto& e){
return !std::memcmp(&rights_id, &e.rights_id, sizeof(rights_id));
});
// this will never fail...but just in case.
R_UNLESS(entry != tickets.end(), Result_CertNotFound);
u64 bytes_read;
if (collection.name.ends_with(".tik")) {
R_TRY(source->Read(entry->ticket.data(), collection.offset, entry->ticket.size(), &bytes_read));
} else {
R_TRY(source->Read(entry->cert.data(), collection.offset, entry->cert.size(), &bytes_read));
}
}
}
log_write("listing keys\n");
for (auto& cnmt : cnmts) {
// copy nca structs into cnmt.
for (auto& cnmt_nca : cnmt.ncas) {
auto it = std::find_if(ncas.cbegin(), ncas.cend(), [cnmt_nca](auto& e){
return e.name == cnmt_nca.name;
});
// remove current entries (if any).
s32 db_list_total;
s32 db_list_count;
u64 id_min = cnmt.key.id;
u64 id_max = cnmt.key.id;
std::vector<NcmContentMetaKey> keys(1);
// if installing a patch, remove all previously installed patches.
if (cnmt.key.type == NcmContentMetaType_Patch) {
id_min = 0;
id_max = UINT64_MAX;
R_UNLESS(it != ncas.cend(), Result_NczSectionNotFound);
const auto type = cnmt_nca.type;
cnmt_nca = *it;
cnmt_nca.type = type;
}
for (size_t i = 0; i < std::size(NCM_STORAGE_IDS); i++) {
auto& cs = yati->ncm_cs[i];
auto& db = yati->ncm_db[i];
u32 latest_version_num;
bool skip = false;
R_TRY(yati->GetLatestVersion(cnmt, latest_version_num, skip));
R_TRY(yati->ShouldSkip(cnmt, skip));
std::vector<NcmContentMetaKey> keys(1);
R_TRY(ncmContentMetaDatabaseList(std::addressof(db), std::addressof(db_list_total), std::addressof(db_list_count), keys.data(), keys.size(), static_cast<NcmContentMetaType>(cnmt.key.type), app_id, id_min, id_max, NcmContentInstallType_Full));
if (db_list_total != keys.size()) {
keys.resize(db_list_total);
if (keys.size()) {
R_TRY(ncmContentMetaDatabaseList(std::addressof(db), std::addressof(db_list_total), std::addressof(db_list_count), keys.data(), keys.size(), static_cast<NcmContentMetaType>(cnmt.key.type), app_id, id_min, id_max, NcmContentInstallType_Full));
}
}
for (auto& key : keys) {
log_write("found key: 0x%016lX type: %u version: %u\n", key.id, key.type, key.version);
NcmContentMetaHeader header;
u64 out_size;
log_write("trying to get from db\n");
R_TRY(ncmContentMetaDatabaseGet(std::addressof(db), std::addressof(key), std::addressof(out_size), std::addressof(header), sizeof(header)));
R_UNLESS(out_size == sizeof(header), Result_NcmDbCorruptHeader);
log_write("trying to list infos\n");
std::vector<NcmContentInfo> infos(header.content_count);
s32 content_info_out;
R_TRY(ncmContentMetaDatabaseListContentInfo(std::addressof(db), std::addressof(content_info_out), infos.data(), infos.size(), std::addressof(key), 0));
R_UNLESS(content_info_out == infos.size(), Result_NcmDbCorruptInfos);
log_write("size matches\n");
for (auto& info : infos) {
R_TRY(ncm::Delete(std::addressof(cs), std::addressof(info.content_id)));
}
log_write("trying to remove it\n");
R_TRY(ncmContentMetaDatabaseRemove(std::addressof(db), std::addressof(key)));
R_TRY(ncmContentMetaDatabaseCommit(std::addressof(db)));
log_write("all done with this key\n\n");
}
if (skip) {
log_write("skipping install!\n");
continue;
}
log_write("done with keys\n");
// register all nca's
log_write("registering cnmt nca\n");
R_TRY(ncm::Register(std::addressof(yati->cs), std::addressof(cnmt.content_id), std::addressof(cnmt.placeholder_id)));
log_write("registered cnmt nca\n");
for (auto& nca : cnmt.ncas) {
log_write("registering nca: %s\n", nca.name.c_str());
R_TRY(ncm::Register(std::addressof(yati->cs), std::addressof(nca.content_id), std::addressof(nca.placeholder_id)));
log_write("registered nca: %s\n", nca.name.c_str());
}
log_write("register'd all ncas\n");
{
BufHelper buf{};
buf.write(std::addressof(cnmt.header), sizeof(cnmt.header));
buf.write(cnmt.extended_header.data(), cnmt.extended_header.size());
buf.write(std::addressof(cnmt.content_info), sizeof(cnmt.content_info));
for (auto& info : cnmt.infos) {
buf.write(std::addressof(info.info), sizeof(info.info));
}
pbox->NewTransfer("Updating ncm databse"_i18n);
R_TRY(ncmContentMetaDatabaseSet(std::addressof(yati->db), std::addressof(cnmt.key), buf.buf.data(), buf.tell()));
R_TRY(ncmContentMetaDatabaseCommit(std::addressof(yati->db)));
}
{
ncm::ContentStorageRecord content_storage_record{};
content_storage_record.key = cnmt.key;
content_storage_record.storage_id = yati->storage_id;
pbox->NewTransfer("Pushing application record"_i18n);
R_TRY(ns::PushApplicationRecord(std::addressof(yati->ns_app), app_id, std::addressof(content_storage_record), 1));
if (hosversionAtLeast(6,0,0)) {
R_TRY(avmInitialize());
ON_SCOPE_EXIT(avmExit());
R_TRY(avmPushLaunchVersion(app_id, latest_version_num));
}
log_write("pushed\n");
}
R_TRY(yati->ImportTickets(tickets));
R_TRY(yati->RemoveInstalledNcas(cnmt));
R_TRY(yati->RegisterNcasAndPushRecord(cnmt, latest_version_num));
}
log_write("success!\n");
@@ -1287,31 +1415,40 @@ Result InstallInternal(ui::ProgressBox* pbox, std::shared_ptr<source::Base> sour
} // namespace
Result InstallFromFile(FsFileSystem* fs, const fs::FsPath& path) {
return InstallFromSource(std::make_shared<source::File>(fs, path));
}
Result InstallFromStdioFile(const char* path) {
return InstallFromSource(std::make_shared<source::Stdio>(path));
}
Result InstallFromSource(std::shared_ptr<source::Base> source) {
App::Push(std::make_shared<ui::ProgressBox>("Installing App"_i18n, [source](auto pbox) mutable -> bool {
return R_SUCCEEDED(InstallFromSource(pbox, source));
}));
R_SUCCEED();
}
Result InstallFromFile(ui::ProgressBox* pbox, FsFileSystem* fs, const fs::FsPath& path) {
return InstallFromSource(pbox, std::make_shared<source::File>(fs, path));
return InstallFromSource(pbox, std::make_shared<source::File>(fs, path), path);
// return InstallFromSource(pbox, std::make_shared<source::StreamFile>(fs, path), path);
}
Result InstallFromStdioFile(ui::ProgressBox* pbox, const char* path) {
return InstallFromSource(pbox, std::make_shared<source::Stdio>(path));
Result InstallFromStdioFile(ui::ProgressBox* pbox, const fs::FsPath& path) {
return InstallFromSource(pbox, std::make_shared<source::Stdio>(path), path);
}
Result InstallFromSource(ui::ProgressBox* pbox, std::shared_ptr<source::Base> source) {
return InstallInternal(pbox, source);
Result InstallFromSource(ui::ProgressBox* pbox, std::shared_ptr<source::Base> source, const fs::FsPath& path) {
const auto ext = std::strrchr(path.s, '.');
R_UNLESS(ext, Result_ContainerNotFound);
if (!strcasecmp(ext, ".nsp") || !strcasecmp(ext, ".nsz")) {
return InstallFromContainer(pbox, std::make_unique<container::Nsp>(source));
} else if (!strcasecmp(ext, ".xci") || !strcasecmp(ext, ".xcz")) {
return InstallFromContainer(pbox, std::make_unique<container::Xci>(source));
}
R_THROW(Result_ContainerNotFound);
}
Result InstallFromContainer(ui::ProgressBox* pbox, std::shared_ptr<container::Base> container) {
container::Collections collections;
R_TRY(container->GetCollections(collections));
return InstallFromCollections(pbox, container->GetSource(), collections);
}
Result InstallFromCollections(ui::ProgressBox* pbox, std::shared_ptr<source::Base> source, const container::Collections& collections) {
if (source->IsStream()) {
return InstallInternalStream(pbox, source, collections);
} else {
return InstallInternal(pbox, source, collections);
}
}
} // namespace sphaira::yati

1
tools/requirements.txt Normal file
View File

@@ -0,0 +1 @@
pyusb

View File

@@ -6,11 +6,12 @@ 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 = 1
VERSION = 2
# list of supported extensions.
EXTS = (".nsp", ".xci", ".nsz", ".xcz")
@@ -28,6 +29,15 @@ def verify_switch(bcdUSB, count, in_ep, out_ep):
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
@@ -124,6 +134,7 @@ if __name__ == '__main__':
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: