[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.
This commit is contained in:
@@ -47,6 +47,7 @@ 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
|
||||
@@ -83,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
|
||||
@@ -143,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
|
||||
)
|
||||
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
#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();
|
||||
|
||||
} // namespace sphaira::ftpsrv
|
||||
|
||||
54
sphaira/include/ui/menus/ftp_menu.hpp
Normal file
54
sphaira/include/ui/menus/ftp_menu.hpp
Normal file
@@ -0,0 +1,54 @@
|
||||
#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};
|
||||
};
|
||||
|
||||
} // namespace sphaira::ui::menu::ftp
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
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
|
||||
@@ -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,20 @@ 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;
|
||||
}
|
||||
|
||||
} // namespace sphaira::ftpsrv
|
||||
|
||||
extern "C" {
|
||||
|
||||
242
sphaira/source/ui/menus/ftp_menu.cpp
Normal file
242
sphaira/source/ui/menus/ftp_menu.cpp
Normal file
@@ -0,0 +1,242 @@
|
||||
#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);
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
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
|
||||
@@ -3,6 +3,7 @@
|
||||
#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"
|
||||
@@ -321,6 +322,12 @@ 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>());
|
||||
}));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -60,14 +60,6 @@ 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.get(), HFS0_HEADER_OFFSET, root));
|
||||
|
||||
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
|
||||
@@ -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{};
|
||||
@@ -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);
|
||||
@@ -891,7 +905,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");
|
||||
@@ -1027,11 +1040,7 @@ Result Yati::InstallControlNca(std::span<TikCollection> tickets, const CnmtColle
|
||||
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{};
|
||||
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{};
|
||||
@@ -1046,34 +1055,23 @@ Result InstallInternal(ui::ProgressBox* pbox, std::shared_ptr<source::Base> sour
|
||||
entry.ticket.resize(collection.size);
|
||||
entry.cert.resize(cert->size);
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
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")) {
|
||||
auto& cnmt = cnmts.emplace_back(NcaCollection{collection});
|
||||
cnmt.type = NcmContentType_Meta;
|
||||
}
|
||||
}
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
for (auto& cnmt : cnmts) {
|
||||
ON_SCOPE_EXIT(
|
||||
ncmContentStorageDeletePlaceHolder(std::addressof(yati->cs), std::addressof(cnmt.placeholder_id));
|
||||
for (auto& nca : cnmt.ncas) {
|
||||
ncmContentStorageDeletePlaceHolder(std::addressof(yati->cs), std::addressof(nca.placeholder_id));
|
||||
}
|
||||
);
|
||||
|
||||
R_TRY(yati->InstallCnmtNca(tickets, cnmt, collections));
|
||||
|
||||
bool skip = false;
|
||||
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));
|
||||
|
||||
@@ -1088,21 +1086,21 @@ Result InstallInternal(ui::ProgressBox* pbox, std::shared_ptr<source::Base> sour
|
||||
|
||||
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_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 && yati->config.skip_if_already_installed) {
|
||||
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 && !yati->config.allow_downgrade) {
|
||||
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;
|
||||
}
|
||||
@@ -1112,59 +1110,53 @@ Result InstallInternal(ui::ProgressBox* pbox, std::shared_ptr<source::Base> sour
|
||||
}
|
||||
}
|
||||
|
||||
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 (yati->config.skip_base && cnmt.key.type == NcmContentMetaType_Application) {
|
||||
} else if (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) {
|
||||
} else if (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) {
|
||||
} else if (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) {
|
||||
} else if (config.skip_data_patch && cnmt.key.type == NcmContentMetaType_DataPatch) {
|
||||
log_write("\tskipping: [NcmContentMetaType_DataPatch]\n");
|
||||
skip = true;
|
||||
}
|
||||
|
||||
if (skip) {
|
||||
log_write("skipping install!\n");
|
||||
continue;
|
||||
}
|
||||
|
||||
log_write("installing nca's\n");
|
||||
for (auto& nca : cnmt.ncas) {
|
||||
if (nca.type == NcmContentType_Control) {
|
||||
log_write("installing control nca\n");
|
||||
R_TRY(yati->InstallControlNca(tickets, cnmt, nca));
|
||||
} else {
|
||||
R_TRY(yati->InstallNca(tickets, nca));
|
||||
}
|
||||
}
|
||||
|
||||
// log_write("exiting early :)\n");
|
||||
// return 0;
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Yati::ImportTickets(std::span<TikCollection> tickets) {
|
||||
for (auto& ticket : tickets) {
|
||||
if (ticket.required) {
|
||||
if (yati->config.skip_ticket) {
|
||||
if (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));
|
||||
if (config.patch_ticket) {
|
||||
R_TRY(es::PatchTicket(ticket.ticket, 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()));
|
||||
R_TRY(es::ImportTicket(std::addressof(es), ticket.ticket.data(), ticket.ticket.size(), ticket.cert.data(), ticket.cert.size()));
|
||||
ticket.required = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log_write("listing keys\n");
|
||||
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;
|
||||
@@ -1179,9 +1171,10 @@ Result InstallInternal(ui::ProgressBox* pbox, std::shared_ptr<source::Base> sour
|
||||
id_max = UINT64_MAX;
|
||||
}
|
||||
|
||||
log_write("listing keys\n");
|
||||
for (size_t i = 0; i < std::size(NCM_STORAGE_IDS); i++) {
|
||||
auto& cs = yati->ncm_cs[i];
|
||||
auto& db = yati->ncm_db[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));
|
||||
@@ -1220,21 +1213,28 @@ Result InstallInternal(ui::ProgressBox* pbox, std::shared_ptr<source::Base> sour
|
||||
}
|
||||
|
||||
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(yati->cs), std::addressof(cnmt.content_id), std::addressof(cnmt.placeholder_id)));
|
||||
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(yati->cs), std::addressof(nca.content_id), std::addressof(nca.placeholder_id)));
|
||||
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());
|
||||
@@ -1245,17 +1245,16 @@ Result InstallInternal(ui::ProgressBox* pbox, std::shared_ptr<source::Base> sour
|
||||
}
|
||||
|
||||
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)));
|
||||
}
|
||||
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 = yati->storage_id;
|
||||
content_storage_record.storage_id = 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));
|
||||
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());
|
||||
@@ -1263,9 +1262,156 @@ Result InstallInternal(ui::ProgressBox* pbox, std::shared_ptr<source::Base> sour
|
||||
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") || collection.name.ends_with(".cnmt.ncz")) {
|
||||
auto& cnmt = cnmts.emplace_back(NcaCollection{collection});
|
||||
cnmt.type = NcmContentType_Meta;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& cnmt : cnmts) {
|
||||
ON_SCOPE_EXIT(
|
||||
ncmContentStorageDeletePlaceHolder(std::addressof(yati->cs), std::addressof(cnmt.placeholder_id));
|
||||
for (auto& nca : cnmt.ncas) {
|
||||
ncmContentStorageDeletePlaceHolder(std::addressof(yati->cs), std::addressof(nca.placeholder_id));
|
||||
}
|
||||
);
|
||||
|
||||
R_TRY(yati->InstallCnmtNca(tickets, cnmt, collections));
|
||||
|
||||
u32 latest_version_num;
|
||||
bool skip = false;
|
||||
R_TRY(yati->GetLatestVersion(cnmt, latest_version_num, skip));
|
||||
R_TRY(yati->ShouldSkip(cnmt, skip));
|
||||
|
||||
if (skip) {
|
||||
log_write("skipping install!\n");
|
||||
continue;
|
||||
}
|
||||
|
||||
log_write("installing nca's\n");
|
||||
for (auto& nca : cnmt.ncas) {
|
||||
if (nca.type == NcmContentType_Control) {
|
||||
log_write("installing control nca\n");
|
||||
R_TRY(yati->InstallControlNca(tickets, cnmt, nca));
|
||||
} else {
|
||||
R_TRY(yati->InstallNca(tickets, nca));
|
||||
}
|
||||
}
|
||||
|
||||
R_TRY(yati->ImportTickets(tickets));
|
||||
R_TRY(yati->RemoveInstalledNcas(cnmt));
|
||||
R_TRY(yati->RegisterNcasAndPushRecord(cnmt, latest_version_num));
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
R_UNLESS(it != ncas.cend(), Result_NczSectionNotFound);
|
||||
const auto type = cnmt_nca.type;
|
||||
cnmt_nca = *it;
|
||||
cnmt_nca.type = type;
|
||||
}
|
||||
|
||||
u32 latest_version_num;
|
||||
bool skip = false;
|
||||
R_TRY(yati->GetLatestVersion(cnmt, latest_version_num, skip));
|
||||
R_TRY(yati->ShouldSkip(cnmt, skip));
|
||||
|
||||
if (skip) {
|
||||
log_write("skipping install!\n");
|
||||
continue;
|
||||
}
|
||||
|
||||
R_TRY(yati->ImportTickets(tickets));
|
||||
R_TRY(yati->RemoveInstalledNcas(cnmt));
|
||||
R_TRY(yati->RegisterNcasAndPushRecord(cnmt, latest_version_num));
|
||||
}
|
||||
|
||||
log_write("success!\n");
|
||||
R_SUCCEED();
|
||||
}
|
||||
@@ -1274,6 +1420,7 @@ Result InstallInternal(ui::ProgressBox* pbox, std::shared_ptr<source::Base> sour
|
||||
|
||||
Result InstallFromFile(ui::ProgressBox* pbox, FsFileSystem* fs, const fs::FsPath& 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 fs::FsPath& path) {
|
||||
@@ -1281,16 +1428,16 @@ Result InstallFromStdioFile(ui::ProgressBox* pbox, const fs::FsPath& path) {
|
||||
}
|
||||
|
||||
Result InstallFromSource(ui::ProgressBox* pbox, std::shared_ptr<source::Base> source, const fs::FsPath& path) {
|
||||
if (R_SUCCEEDED(container::Nsp::Validate(source.get()))) {
|
||||
log_write("found nsp\n");
|
||||
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 (R_SUCCEEDED(container::Xci::Validate(source.get()))) {
|
||||
log_write("found xci\n");
|
||||
} else if (!strcasecmp(ext, ".xci") || !strcasecmp(ext, ".xcz")) {
|
||||
return InstallFromContainer(pbox, std::make_unique<container::Xci>(source));
|
||||
} else {
|
||||
log_write("found unknown container\n");
|
||||
R_THROW(Result_ContainerNotFound);
|
||||
}
|
||||
|
||||
R_THROW(Result_ContainerNotFound);
|
||||
}
|
||||
|
||||
Result InstallFromContainer(ui::ProgressBox* pbox, std::shared_ptr<container::Base> container) {
|
||||
@@ -1300,7 +1447,11 @@ Result InstallFromContainer(ui::ProgressBox* pbox, std::shared_ptr<container::Ba
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user