Compare commits
9 Commits
0.7.0
...
stream_ins
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90f8a62823 | ||
|
|
e2a1c8b5e3 | ||
|
|
21f6f4b74d | ||
|
|
75d3b3ee0d | ||
|
|
0dde379932 | ||
|
|
9800bbecdf | ||
|
|
60e915c255 | ||
|
|
786f8a42fa | ||
|
|
5a4a0f75f2 |
@@ -2,3 +2,4 @@
|
||||
path=/retroarch/cores/fbneo_libretro_libnx.nro
|
||||
supported_extensions=zip|7z|cue|ccd
|
||||
database=FBNeo - Arcade Games
|
||||
use_base_name=true
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
path=/retroarch/cores/mame2000_libretro_libnx.nro
|
||||
supported_extensions=zip|7z
|
||||
database=MAME 2000
|
||||
use_base_name=true
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
path=/retroarch/cores/mame2003_libretro_libnx.nro
|
||||
supported_extensions=zip
|
||||
database=MAME 2003
|
||||
use_base_name=true
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
path=/retroarch/cores/mame2003_plus_libretro_libnx.nro
|
||||
supported_extensions=zip
|
||||
database=MAME 2003-Plus
|
||||
use_base_name=true
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
path=/retroarch/cores/xrick_libretro_libnx.nro
|
||||
supported_extensions=zip
|
||||
database=Rick Dangerous
|
||||
use_base_name=true
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -454,4 +454,10 @@ struct FsNativeContentStorage final : FsNative {
|
||||
}
|
||||
};
|
||||
|
||||
struct FsNativeGameCard final : FsNative {
|
||||
FsNativeGameCard(const FsGameCardHandle* handle, FsGameCardPartition partition, bool ignore_read_only = true) : FsNative{ignore_read_only} {
|
||||
m_open_result = fsOpenGameCardFileSystem(&m_fs, handle, partition);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace fs
|
||||
|
||||
@@ -1,8 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
|
||||
namespace sphaira::ftpsrv {
|
||||
|
||||
bool Init();
|
||||
void Exit();
|
||||
|
||||
using OnInstallStart = std::function<bool(void* user, const char* path)>;
|
||||
using OnInstallWrite = std::function<bool(void* user, const void* buf, size_t size)>;
|
||||
using OnInstallClose = std::function<void(void* user)>;
|
||||
|
||||
void InitInstallMode(void* user, OnInstallStart on_start, OnInstallWrite on_write, OnInstallClose on_close);
|
||||
void DisableInstallMode();
|
||||
|
||||
unsigned GetPort();
|
||||
bool IsAnon();
|
||||
const char* GetUser();
|
||||
const char* GetPass();
|
||||
|
||||
} // namespace sphaira::ftpsrv
|
||||
|
||||
@@ -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 {
|
||||
|
||||
59
sphaira/include/ui/menus/ftp_menu.hpp
Normal file
59
sphaira/include/ui/menus/ftp_menu.hpp
Normal file
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
#include "ui/menus/menu_base.hpp"
|
||||
#include "yati/source/stream.hpp"
|
||||
|
||||
namespace sphaira::ui::menu::ftp {
|
||||
|
||||
enum class State {
|
||||
// not connected.
|
||||
None,
|
||||
// just connected, starts the transfer.
|
||||
Connected,
|
||||
// set whilst transfer is in progress.
|
||||
Progress,
|
||||
// set when the transfer is finished.
|
||||
Done,
|
||||
// failed to connect.
|
||||
Failed,
|
||||
};
|
||||
|
||||
struct StreamFtp final : yati::source::Stream {
|
||||
StreamFtp(const fs::FsPath& path, std::stop_token token);
|
||||
|
||||
Result ReadChunk(void* buf, s64 size, u64* bytes_read) override;
|
||||
bool Push(const void* buf, s64 size);
|
||||
void Disable();
|
||||
|
||||
// private:
|
||||
fs::FsPath m_path{};
|
||||
std::stop_token m_token{};
|
||||
std::vector<u8> m_buffer{};
|
||||
Mutex m_mutex{};
|
||||
bool m_active{};
|
||||
// bool m_push_exit{};
|
||||
};
|
||||
|
||||
struct Menu final : MenuBase {
|
||||
Menu();
|
||||
~Menu();
|
||||
|
||||
void Update(Controller* controller, TouchInfo* touch) override;
|
||||
void Draw(NVGcontext* vg, Theme* theme) override;
|
||||
void OnFocusGained() override;
|
||||
|
||||
// this should be private
|
||||
// private:
|
||||
std::shared_ptr<StreamFtp> m_source;
|
||||
Thread m_thread{};
|
||||
Mutex m_mutex{};
|
||||
// the below are shared across threads, lock with the above mutex!
|
||||
State m_state{State::None};
|
||||
|
||||
const char* m_user{};
|
||||
const char* m_pass{};
|
||||
unsigned m_port{};
|
||||
bool m_anon{};
|
||||
};
|
||||
|
||||
} // namespace sphaira::ui::menu::ftp
|
||||
38
sphaira/include/ui/menus/gc_menu.hpp
Normal file
38
sphaira/include/ui/menus/gc_menu.hpp
Normal 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
|
||||
@@ -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{};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
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
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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" {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
296
sphaira/source/ui/menus/ftp_menu.cpp
Normal file
296
sphaira/source/ui/menus/ftp_menu.cpp
Normal 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
|
||||
187
sphaira/source/ui/menus/gc_menu.cpp
Normal file
187
sphaira/source/ui/menus/gc_menu.cpp
Normal 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
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
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
|
||||
@@ -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));
|
||||
|
||||
@@ -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
1
tools/requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
pyusb
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user