12 Commits
0.7.0 ... 0.8.0

Author SHA1 Message Date
ITotalJustice
282c6e5493 bump version for release 0.7.0 -> 0.8.0 2025-04-27 21:01:45 +01:00
ITotalJustice
2c2f602d14 add gc_menu, add progress, icon, time remaining to progress bar (see full commit message).
- fix ignore distribution bit doing nothing.
- fix yati failing to parse control nca causing the transfer to abort.
- yati now uses ncm rather than ns to get the latest app version.
- improve ui::list input handling (it handles directional buttons now).
- progress bar displays speed and time remaining.
- added gc menu (taken from my gc installer nx and gci).
2025-04-27 20:01:13 +01:00
ITotalJustice
f7f1254699 Merge pull request #128 from ITotalJustice/stream_installs
Stream installs (FTP), and many fixes
2025-04-23 01:02:26 +01:00
ITotalJustice
90f8a62823 display useful info in ftp menu (ip, port, user, pass, ssid, passphrase) 2025-04-23 01:00:36 +01:00
ITotalJustice
e2a1c8b5e3 fix yati not setting correct version, add support for using zip name when creating forwarder, remove some dead code.
fixes #126
fixes #127
2025-04-22 23:15:16 +01:00
ITotalJustice
21f6f4b74d [skip ci] fix file assoc always using internal name, fix menu showing wrong time
fixes #126
2025-04-22 00:08:26 +01:00
ITotalJustice
75d3b3ee0d [skip-ci] initial support for stream installs, add ftp installs.
do NOT build or release binaries of this version, it is not complete and there will be dragons.
2025-04-21 23:23:59 +01:00
ITotalJustice
0dde379932 don't return from usb menu on error, wait until the user presses B 2025-04-21 13:33:36 +01:00
ITotalJustice
9800bbecdf add basic support for gamecard installing 2025-04-21 13:30:46 +01:00
ITotalJustice
60e915c255 enable screenshot permissions in applet mode. 2025-04-21 12:40:37 +01:00
ITotalJustice
786f8a42fa send file name and size via usb, add requirements.txt for usb.py 2025-04-21 01:41:20 +01:00
ITotalJustice
5a4a0f75f2 add support for mame and neogeo, as well as alias for rom folder names 2025-04-20 22:03:53 +01:00
60 changed files with 2521 additions and 1140 deletions

View File

@@ -2,3 +2,4 @@
path=/retroarch/cores/fbneo_libretro_libnx.nro
supported_extensions=zip|7z|cue|ccd
database=FBNeo - Arcade Games
use_base_name=true

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.13)
set(sphaira_VERSION 0.7.0)
set(sphaira_VERSION 0.8.0)
project(sphaira
VERSION ${sphaira_VERSION}
@@ -47,6 +47,8 @@ add_executable(sphaira
source/ui/menus/themezer.cpp
source/ui/menus/ghdl.cpp
source/ui/menus/usb_menu.cpp
source/ui/menus/ftp_menu.cpp
source/ui/menus/gc_menu.cpp
source/ui/error_box.cpp
source/ui/notification.cpp
@@ -82,6 +84,8 @@ add_executable(sphaira
source/yati/source/file.cpp
source/yati/source/stdio.cpp
source/yati/source/usb.cpp
source/yati/source/stream.cpp
source/yati/source/stream_file.cpp
source/yati/nx/es.cpp
source/yati/nx/keys.cpp
@@ -142,7 +146,8 @@ set(FETCHCONTENT_QUIET FALSE)
FetchContent_Declare(ftpsrv
GIT_REPOSITORY https://github.com/ITotalJustice/ftpsrv.git
GIT_TAG 1.2.2
# GIT_TAG 1.2.2
GIT_TAG f8a30fd
SOURCE_SUBDIR NONE
)

View File

@@ -64,7 +64,7 @@ public:
static void SetTheme(s64 theme_index);
static auto GetThemeIndex() -> s64;
static auto GetDefaultImage(int* w = nullptr, int* h = nullptr) -> int;
static auto GetDefaultImage() -> int;
// returns argv[0]
static auto GetExePath() -> fs::FsPath;
@@ -102,6 +102,13 @@ public:
static void PlaySoundEffect(SoundEffect effect);
static void DisplayThemeOptions(bool left_side = true);
// todo:
static void DisplayNetworkOptions(bool left_side = true);
static void DisplayMiscOptions(bool left_side = true);
static void DisplayAdvancedOptions(bool left_side = true);
static void DisplayInstallOptions(bool left_side = true);
void Draw();
void Update();
void Poll();
@@ -185,7 +192,6 @@ public:
option::OptionBool m_allow_downgrade{INI_SECTION, "allow_downgrade", false};
option::OptionBool m_skip_if_already_installed{INI_SECTION, "skip_if_already_installed", true};
option::OptionBool m_ticket_only{INI_SECTION, "ticket_only", false};
option::OptionBool m_patch_ticket{INI_SECTION, "patch_ticket", true};
option::OptionBool m_skip_base{INI_SECTION, "skip_base", false};
option::OptionBool m_skip_patch{INI_SECTION, "skip_patch", false};
option::OptionBool m_skip_addon{INI_SECTION, "skip_addon", false};

View File

@@ -454,4 +454,10 @@ struct FsNativeContentStorage final : FsNative {
}
};
struct FsNativeGameCard final : FsNative {
FsNativeGameCard(const FsGameCardHandle* handle, FsGameCardPartition partition, bool ignore_read_only = true) : FsNative{ignore_read_only} {
m_open_result = fsOpenGameCardFileSystem(&m_fs, handle, partition);
}
};
} // namespace fs

View File

@@ -1,8 +1,22 @@
#pragma once
#include <functional>
namespace sphaira::ftpsrv {
bool Init();
void Exit();
using OnInstallStart = std::function<bool(void* user, const char* path)>;
using OnInstallWrite = std::function<bool(void* user, const void* buf, size_t size)>;
using OnInstallClose = std::function<void(void* user)>;
void InitInstallMode(void* user, OnInstallStart on_start, OnInstallWrite on_write, OnInstallClose on_close);
void DisableInstallMode();
unsigned GetPort();
bool IsAnon();
const char* GetUser();
const char* GetPass();
} // namespace sphaira::ftpsrv

View File

@@ -6,11 +6,11 @@ namespace sphaira::ui {
struct List final : Object {
using Callback = std::function<void(NVGcontext* vg, Theme* theme, Vec4 v, s64 index)>;
using TouchCallback = std::function<void(s64 index)>;
using TouchCallback = std::function<void(bool touch, s64 index)>;
List(s64 row, s64 page, const Vec4& pos, const Vec4& v, const Vec2& pad = {});
void OnUpdate(Controller* controller, TouchInfo* touch, s64 count, TouchCallback callback);
void OnUpdate(Controller* controller, TouchInfo* touch, s64 index, s64 count, TouchCallback callback);
void Draw(NVGcontext* vg, Theme* theme, s64 count, Callback callback) const;

View File

@@ -89,6 +89,19 @@ struct FileAssocEntry {
std::string name{}; // ini name
std::vector<std::string> ext{}; // list of ext
std::vector<std::string> database{}; // list of systems
bool use_base_name{}; // if set, uses base name (rom.zip) otherwise uses internal name (rom.gba)
auto IsExtension(std::string_view extension, std::string_view internal_extension) const -> bool {
for (const auto& assoc_ext : ext) {
if (extension.length() == assoc_ext.length() && !strncasecmp(assoc_ext.data(), extension.data(), assoc_ext.length())) {
return true;
}
if (internal_extension.length() == assoc_ext.length() && !strncasecmp(assoc_ext.data(), internal_extension.data(), assoc_ext.length())) {
return true;
}
}
return false;
}
};
struct LastFile {

View File

@@ -0,0 +1,59 @@
#pragma once
#include "ui/menus/menu_base.hpp"
#include "yati/source/stream.hpp"
namespace sphaira::ui::menu::ftp {
enum class State {
// not connected.
None,
// just connected, starts the transfer.
Connected,
// set whilst transfer is in progress.
Progress,
// set when the transfer is finished.
Done,
// failed to connect.
Failed,
};
struct StreamFtp final : yati::source::Stream {
StreamFtp(const fs::FsPath& path, std::stop_token token);
Result ReadChunk(void* buf, s64 size, u64* bytes_read) override;
bool Push(const void* buf, s64 size);
void Disable();
// private:
fs::FsPath m_path{};
std::stop_token m_token{};
std::vector<u8> m_buffer{};
Mutex m_mutex{};
bool m_active{};
// bool m_push_exit{};
};
struct Menu final : MenuBase {
Menu();
~Menu();
void Update(Controller* controller, TouchInfo* touch) override;
void Draw(NVGcontext* vg, Theme* theme) override;
void OnFocusGained() override;
// this should be private
// private:
std::shared_ptr<StreamFtp> m_source;
Thread m_thread{};
Mutex m_mutex{};
// the below are shared across threads, lock with the above mutex!
State m_state{State::None};
const char* m_user{};
const char* m_pass{};
unsigned m_port{};
bool m_anon{};
};
} // namespace sphaira::ui::menu::ftp

View File

@@ -0,0 +1,75 @@
#pragma once
#include "ui/menus/menu_base.hpp"
#include "yati/container/base.hpp"
#include "yati/source/base.hpp"
#include "ui/list.hpp"
#include <span>
#include <memory>
namespace sphaira::ui::menu::gc {
struct GcCollection : yati::container::CollectionEntry {
GcCollection(const char* _name, s64 _size, u8 _type) {
name = _name;
size = _size;
type = _type;
}
// NcmContentType
u8 type{};
};
using GcCollections = std::vector<GcCollection>;
struct ApplicationEntry {
u64 app_id{};
u32 version{};
u8 key_gen{};
std::vector<GcCollections> application{};
std::vector<GcCollections> patch{};
std::vector<GcCollections> add_on{};
std::vector<GcCollections> data_patch{};
yati::container::Collections tickets{};
auto GetSize() const -> s64;
auto GetSize(const std::vector<GcCollections>& entries) const -> s64;
};
struct Menu final : MenuBase {
Menu();
~Menu();
void Update(Controller* controller, TouchInfo* touch) override;
void Draw(NVGcontext* vg, Theme* theme) override;
private:
Result GcMount();
void GcUnmount();
Result GcPoll(bool* inserted);
Result UpdateStorageSize();
void FreeImage();
void OnChangeIndex(s64 new_index);
private:
FsDeviceOperator m_dev_op{};
FsGameCardHandle m_handle{};
std::unique_ptr<fs::FsNativeGameCard> m_fs{};
std::vector<ApplicationEntry> m_entries{};
std::unique_ptr<List> m_list{};
s64 m_entry_index{};
s64 m_option_index{};
s64 m_size_free_sd{};
s64 m_size_total_sd{};
s64 m_size_free_nand{};
s64 m_size_total_nand{};
NacpLanguageEntry m_lang_entry{};
int m_icon{};
bool m_mounted{};
};
} // namespace sphaira::ui::menu::gc

View File

@@ -29,6 +29,7 @@ private:
std::string m_title_sub_heading{};
std::string m_sub_heading{};
protected:
struct tm m_tm{};
TimeStamp m_poll_timestamp{};
u32 m_battery_percetange{};

View File

@@ -5,17 +5,15 @@
namespace sphaira::ui::gfx {
void drawImage(NVGcontext*, float x, float y, float w, float h, int texture);
void drawImage(NVGcontext*, const Vec4& v, int texture);
void drawImageRounded(NVGcontext*, float x, float y, float w, float h, int texture);
void drawImageRounded(NVGcontext*, const Vec4& v, int texture);
void drawImage(NVGcontext*, float x, float y, float w, float h, int texture, float rounded = 0.F);
void drawImage(NVGcontext*, const Vec4& v, int texture, float rounded = 0.F);
void dimBackground(NVGcontext*);
void drawRect(NVGcontext*, float x, float y, float w, float h, const NVGcolor& c, bool rounded = false);
void drawRect(NVGcontext*, const Vec4& v, const NVGcolor& c, bool rounded = false);
void drawRect(NVGcontext*, float x, float y, float w, float h, const NVGpaint& p, bool rounded = false);
void drawRect(NVGcontext*, const Vec4& v, const NVGpaint& p, bool rounded = false);
void drawRect(NVGcontext*, float x, float y, float w, float h, const NVGcolor& c, float rounding = 0.F);
void drawRect(NVGcontext*, const Vec4& v, const NVGcolor& c, float rounding = 0.F);
void drawRect(NVGcontext*, float x, float y, float w, float h, const NVGpaint& p, float rounding = 0.F);
void drawRect(NVGcontext*, const Vec4& v, const NVGpaint& p, float rounding = 0.F);
void drawRectOutline(NVGcontext*, const Theme*, float size, float x, float y, float w, float h);
void drawRectOutline(NVGcontext*, const Theme*, float size, const Vec4& v);

View File

@@ -3,6 +3,7 @@
#include "widget.hpp"
#include "fs.hpp"
#include <functional>
#include <span>
namespace sphaira::ui {
@@ -12,6 +13,8 @@ using ProgressBoxDoneCallback = std::function<void(bool success)>;
struct ProgressBox final : Widget {
ProgressBox(
int image,
const std::string& action,
const std::string& title,
ProgressBoxCallback callback, ProgressBoxDoneCallback done = [](bool success){},
int cpuid = 1, int prio = 0x2C, int stack_size = 1024*1024
@@ -24,6 +27,9 @@ struct ProgressBox final : Widget {
auto SetTitle(const std::string& title) -> ProgressBox&;
auto NewTransfer(const std::string& transfer) -> ProgressBox&;
auto UpdateTransfer(s64 offset, s64 size) -> ProgressBox&;
// not const in order to avoid copy by using std::swap
auto SetImageData(std::vector<u8>& data) -> ProgressBox&;
auto SetImageDataConst(std::span<const u8> data) -> ProgressBox&;
void RequestExit();
auto ShouldExit() -> bool;
@@ -41,6 +47,9 @@ struct ProgressBox final : Widget {
};
}
private:
void FreeImage();
public:
struct ThreadData {
ProgressBox* pbox{};
@@ -52,12 +61,22 @@ private:
Mutex m_mutex{};
Thread m_thread{};
ThreadData m_thread_data{};
ProgressBoxDoneCallback m_done{};
// shared data start.
std::string m_action{};
std::string m_title{};
std::string m_transfer{};
s64 m_size{};
s64 m_offset{};
s64 m_last_offset{};
s64 m_speed{};
TimeStamp m_timestamp{};
std::vector<u8> m_image_data{};
// shared data end.
int m_image{};
bool m_own_image{};
};
// this is a helper function that does many things.

View File

@@ -3,6 +3,7 @@
#include "yati/source/base.hpp"
#include <vector>
#include <string>
#include <memory>
#include <switch.h>
namespace sphaira::yati::container {
@@ -28,12 +29,15 @@ using Collections = std::vector<CollectionEntry>;
struct Base {
using Source = source::Base;
Base(Source* source) : m_source{source} { }
Base(std::shared_ptr<Source> source) : m_source{source} { }
virtual ~Base() = default;
virtual Result GetCollections(Collections& out) = 0;
auto GetSource() const {
return m_source;
}
protected:
Source* m_source;
std::shared_ptr<Source> m_source;
};
} // namespace sphaira::yati::container

View File

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

View File

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

View File

@@ -78,6 +78,6 @@ Result SetTicketData(std::span<u8> ticket, const es::TicketData* in);
Result GetTitleKey(keys::KeyEntry& out, const TicketData& data, const keys::Keys& keys);
Result DecryptTitleKey(keys::KeyEntry& out, u8 key_gen, const keys::Keys& keys);
Result PatchTicket(std::span<u8> ticket, const keys::Keys& keys, bool convert_personalised);
Result PatchTicket(std::span<u8> ticket, const keys::Keys& keys);
} // namespace sphaira::es

View File

@@ -215,4 +215,6 @@ Result DecryptKeak(const keys::Keys& keys, Header& header);
Result EncryptKeak(const keys::Keys& keys, Header& header, u8 key_generation);
Result VerifyFixedKey(const Header& header);
auto GetKeyGenStr(u8 key_gen) -> const char*;
} // namespace sphaira::nca

View File

@@ -19,7 +19,7 @@ static_assert(sizeof(PackagedContentMeta) == 0x20);
struct ContentStorageRecord {
NcmContentMetaKey key;
u8 storage_id;
u8 storage_id; // NcmStorageId
u8 padding[0x7];
};
@@ -31,7 +31,9 @@ union ExtendedHeader {
NcmDataPatchMetaExtendedHeader data_patch;
};
auto GetAppId(u8 meta_type, u64 id) -> u64;
auto GetAppId(const NcmContentMetaKey& key) -> u64;
auto GetAppId(const PackagedContentMeta& meta) -> u64;
Result Delete(NcmContentStorage* cs, const NcmContentId *content_id);
Result Register(NcmContentStorage* cs, const NcmContentId *content_id, const NcmPlaceHolderId *placeholder_id);

View File

@@ -1,265 +0,0 @@
/*
* tik.h
*
* Copyright (c) 2020-2024, DarkMatterCore <pabloacurielz@gmail.com>.
*
* This file is part of nxdumptool (https://github.com/DarkMatterCore/nxdumptool).
*
* nxdumptool is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* nxdumptool is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#ifndef __TIK_H__
#define __TIK_H__
#include "signature.h"
#ifdef __cplusplus
extern "C" {
#endif
#define SIGNED_TIK_MIN_SIZE sizeof(TikSigHmac160) /* Assuming no ESV1/ESV2 records are available. */
#define SIGNED_TIK_MAX_SIZE 0x400 /* Max ticket entry size in the ES ticket system savedata file. */
#define TIK_FORMAT_VERSION 2
#define GENERATE_TIK_STRUCT(sigtype, tiksize) \
typedef struct { \
SignatureBlock##sigtype sig_block; \
TikCommonBlock tik_common_block; \
u8 es_section_record_data[]; \
} TikSig##sigtype; \
NXDT_ASSERT(TikSig##sigtype, tiksize);
typedef enum {
TikTitleKeyType_Common = 0,
TikTitleKeyType_Personalized = 1,
TikTitleKeyType_Count = 2 ///< Total values supported by this enum.
} TikTitleKeyType;
typedef enum {
TikLicenseType_Permanent = 0,
TikLicenseType_Demo = 1,
TikLicenseType_Trial = 2,
TikLicenseType_Rental = 3,
TikLicenseType_Subscription = 4,
TikLicenseType_Service = 5,
TikLicenseType_Count = 6 ///< Total values supported by this enum.
} TikLicenseType;
typedef enum {
TikPropertyMask_None = 0,
TikPropertyMask_PreInstallation = BIT(0), ///< Determines if the title comes pre-installed on the device. Most likely unused -- a remnant from previous ticket formats.
TikPropertyMask_SharedTitle = BIT(1), ///< Determines if the title holds shared contents only. Most likely unused -- a remnant from previous ticket formats.
TikPropertyMask_AllContents = BIT(2), ///< Determines if the content index mask shall be bypassed. Most likely unused -- a remnant from previous ticket formats.
TikPropertyMask_DeviceLinkIndepedent = BIT(3), ///< Determines if the console should *not* connect to the Internet to verify if the title's being used by the primary console.
TikPropertyMask_Volatile = BIT(4), ///< Determines if the ticket copy inside ticket.bin is available after reboot. Can be encrypted.
TikPropertyMask_ELicenseRequired = BIT(5), ///< Determines if the console should connect to the Internet to perform license verification.
TikPropertyMask_Count = 6 ///< Total values supported by this enum.
} TikPropertyMask;
/// Placed after the ticket signature block.
typedef struct {
char issuer[0x40];
u8 titlekey_block[0x100];
u8 format_version; ///< Always matches TIK_FORMAT_VERSION.
u8 titlekey_type; ///< TikTitleKeyType.
u16 ticket_version;
u8 license_type; ///< TikLicenseType.
u8 key_generation; ///< NcaKeyGeneration.
u16 property_mask; ///< TikPropertyMask.
u8 reserved[0x8];
u64 ticket_id;
u64 device_id;
FsRightsId rights_id;
u32 account_id;
u32 sect_total_size;
u32 sect_hdr_offset;
u16 sect_hdr_count;
u16 sect_hdr_entry_size;
} TikCommonBlock;
NXDT_ASSERT(TikCommonBlock, 0x180);
/// ESV1/ESV2 section records are placed right after the ticket data. These aren't available in TikTitleKeyType_Common tickets.
/// These are only used if the sect_* fields from the common block are non-zero (other than 'sect_hdr_offset').
/// Each ESV2 section record is followed by a 'record_count' number of ESV1 records, each one of 'record_size' size.
typedef enum {
TikSectionType_None = 0,
TikSectionType_Permanent = 1,
TikSectionType_Subscription = 2,
TikSectionType_Content = 3,
TikSectionType_ContentConsumption = 4,
TikSectionType_AccessTitle = 5,
TikSectionType_LimitedResource = 6,
TikSectionType_Count = 7 ///< Total values supported by this enum.
} TikSectionType;
typedef struct {
u32 sect_offset;
u32 record_size;
u32 section_size;
u16 record_count;
u16 section_type; ///< TikSectionType.
} TikESV2SectionRecord;
/// Used with TikSectionType_Permanent.
typedef struct {
u8 ref_id[0x10];
u32 ref_id_attr;
} TikESV1PermanentRecord;
/// Used with TikSectionType_Subscription.
typedef struct {
u32 limit;
u8 ref_id[0x10];
u32 ref_id_attr;
} TikESV1SubscriptionRecord;
/// Used with TikSectionType_Content.
typedef struct {
u32 offset;
u8 access_mask[0x80];
} TikESV1ContentRecord;
/// Used with TikSectionType_ContentConsumption.
typedef struct {
u16 index;
u16 code;
u32 limit;
} TikESV1ContentConsumptionRecord;
/// Used with TikSectionType_AccessTitle.
typedef struct {
u64 access_title_id;
u64 access_title_mask;
} TikESV1AccessTitleRecord;
/// Used with TikSectionType_LimitedResource.
typedef struct {
u32 limit;
u8 ref_id[0x10];
u32 ref_id_attr;
} TikESV1LimitedResourceRecord;
/// All tickets generated below use a little endian sig_type field.
GENERATE_TIK_STRUCT(Rsa4096, 0x3C0); /// RSA-4096 signature.
GENERATE_TIK_STRUCT(Rsa2048, 0x2C0); /// RSA-2048 signature.
GENERATE_TIK_STRUCT(Ecc480, 0x200); /// ECC signature.
GENERATE_TIK_STRUCT(Hmac160, 0x1C0); /// HMAC signature.
/// Ticket type.
typedef enum {
TikType_None = 0,
TikType_SigRsa4096 = 1,
TikType_SigRsa2048 = 2,
TikType_SigEcc480 = 3,
TikType_SigHmac160 = 4,
TikType_Count = 5 ///< Total values supported by this enum.
} TikType;
/// Used to store ticket type, size and raw data, as well as titlekey data.
typedef struct {
u8 type; ///< TikType.
u64 size; ///< Raw ticket size.
u8 data[SIGNED_TIK_MAX_SIZE]; ///< Raw ticket data.
u8 key_generation; ///< NcaKeyGeneration.
u8 enc_titlekey[0x10]; ///< Titlekey with titlekek crypto (RSA-OAEP unwrapped if dealing with a TikTitleKeyType_Personalized ticket).
char enc_titlekey_str[0x21]; ///< Character string representation of enc_titlekey.
u8 dec_titlekey[0x10]; ///< Titlekey without titlekek crypto. Ready to use for NCA FS section decryption.
char dec_titlekey_str[0x21]; ///< Character string representation of dec_titlekey.
char rights_id_str[0x21]; ///< Character string representation of the rights ID from the ticket.
} Ticket;
/// Retrieves a ticket from either the ES ticket system savedata file (eMMC BIS System partition) or the secure Hash FS partition from an inserted gamecard.
/// Both the input rights ID and key generation values must have been retrieved from a NCA that depends on the desired ticket.
/// Titlekey is also RSA-OAEP unwrapped (if needed) and titlekek-decrypted right away.
bool tikRetrieveTicketByRightsId(Ticket *dst, const FsRightsId *id, u8 key_generation, bool use_gamecard);
/// Converts a TikTitleKeyType_Personalized ticket into a TikTitleKeyType_Common ticket and optionally generates a raw certificate chain for the new signature issuer.
/// Bear in mind the 'size' member from the Ticket parameter will be updated by this function to remove any possible references to ESV1/ESV2 records.
/// If both 'out_raw_cert_chain' and 'out_raw_cert_chain_size' pointers are provided, raw certificate chain data will be saved to them.
/// certGenerateRawCertificateChainBySignatureIssuer() is used internally, so the output buffer must be freed by the user.
bool tikConvertPersonalizedTicketToCommonTicket(Ticket *tik, u8 **out_raw_cert_chain, u64 *out_raw_cert_chain_size);
/// Helper inline functions for signed ticket blobs.
NX_INLINE TikCommonBlock *tikGetCommonBlockFromSignedTicketBlob(void *buf)
{
return (TikCommonBlock*)signatureGetPayloadFromSignedBlob(buf, false);
}
NX_INLINE u64 tikGetSectionRecordsSizeFromSignedTicketBlob(void *buf)
{
TikCommonBlock *tik_common_block = tikGetCommonBlockFromSignedTicketBlob(buf);
if (!tik_common_block) return 0;
u64 offset = sizeof(TikCommonBlock), out_size = 0;
for(u32 i = 0; i < tik_common_block->sect_hdr_count; i++)
{
TikESV2SectionRecord *rec = (TikESV2SectionRecord*)((u8*)tik_common_block + offset);
offset += (sizeof(TikESV2SectionRecord) + ((u64)rec->record_count * (u64)rec->record_size));
out_size += offset;
}
return out_size;
}
NX_INLINE bool tikIsValidSignedTicketBlob(void *buf)
{
u64 ticket_size = (signatureGetBlockSizeFromSignedBlob(buf, false) + sizeof(TikCommonBlock));
return (ticket_size > sizeof(TikCommonBlock) && (ticket_size + tikGetSectionRecordsSizeFromSignedTicketBlob(buf)) <= SIGNED_TIK_MAX_SIZE);
}
NX_INLINE u64 tikGetSignedTicketBlobSize(void *buf)
{
return (tikIsValidSignedTicketBlob(buf) ? (signatureGetBlockSizeFromSignedBlob(buf, false) + sizeof(TikCommonBlock) + tikGetSectionRecordsSizeFromSignedTicketBlob(buf)) : 0);
}
NX_INLINE u64 tikGetSignedTicketBlobHashAreaSize(void *buf)
{
return (tikIsValidSignedTicketBlob(buf) ? (sizeof(TikCommonBlock) + tikGetSectionRecordsSizeFromSignedTicketBlob(buf)) : 0);
}
/// Helper inline functions for Ticket elements.
NX_INLINE bool tikIsValidTicket(Ticket *tik)
{
return (tik && tik->type > TikType_None && tik->type < TikType_Count && tik->size >= SIGNED_TIK_MIN_SIZE && tik->size <= SIGNED_TIK_MAX_SIZE && tikIsValidSignedTicketBlob(tik->data));
}
NX_INLINE TikCommonBlock *tikGetCommonBlockFromTicket(Ticket *tik)
{
return (tikIsValidTicket(tik) ? tikGetCommonBlockFromSignedTicketBlob(tik->data) : NULL);
}
NX_INLINE bool tikIsPersonalizedTicket(Ticket *tik)
{
TikCommonBlock *tik_common_block = tikGetCommonBlockFromTicket(tik);
return (tik_common_block ? (tik_common_block->titlekey_type == TikTitleKeyType_Personalized) : false);
}
NX_INLINE u64 tikGetHashAreaSizeFromTicket(Ticket *tik)
{
return (tikIsValidTicket(tik) ? tikGetSignedTicketBlobHashAreaSize(tik->data) : 0);
}
#ifdef __cplusplus
}
#endif
#endif /* __TIK_H__ */

View File

@@ -10,6 +10,10 @@ struct Base {
// virtual Result Read(void* buf, s64 off, s64 size, u64* bytes_read) = 0;
virtual Result Read(void* buf, s64 off, s64 size, u64* bytes_read) = 0;
virtual bool IsStream() const {
return false;
}
Result GetOpenResult() const {
return m_open_result;
}

View File

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

View File

@@ -0,0 +1,28 @@
#pragma once
#include "base.hpp"
#include <vector>
#include <switch.h>
namespace sphaira::yati::source {
// streams are for data that do not allow for random access,
// such as FTP or MTP.
struct Stream : Base {
virtual ~Stream() = default;
virtual Result ReadChunk(void* buf, s64 size, u64* bytes_read) = 0;
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override;
bool IsStream() const override {
return true;
}
protected:
Result m_open_result{};
private:
s64 m_offset{};
};
} // namespace sphaira::yati::source

View File

@@ -0,0 +1,22 @@
// this is used for testing that streams work, this code isn't used in normal
// release builds as it is slower / less feature complete than normal.
#pragma once
#include "stream.hpp"
#include "fs.hpp"
#include <switch.h>
namespace sphaira::yati::source {
struct StreamFile final : Stream {
StreamFile(FsFileSystem* fs, const fs::FsPath& path);
~StreamFile();
Result ReadChunk(void* buf, s64 size, u64* bytes_read) override;
private:
FsFile m_file{};
s64 m_offset{};
};
} // namespace sphaira::yati::source

View File

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

View File

@@ -9,8 +9,11 @@
#include "fs.hpp"
#include "source/base.hpp"
#include "container/base.hpp"
#include "nx/ncm.hpp"
#include "ui/progress_box.hpp"
#include <memory>
#include <optional>
namespace sphaira::yati {
@@ -78,11 +81,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 +114,25 @@ 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);
// overridable options, set to avoid
struct ConfigOverride {
std::optional<bool> sd_card_install{};
std::optional<bool> skip_nca_hash_verify{};
std::optional<bool> skip_rsa_header_fixed_key_verify{};
std::optional<bool> skip_rsa_npdm_fixed_key_verify{};
std::optional<bool> ignore_distribution_bit{};
std::optional<bool> convert_to_standard_crypto{};
std::optional<bool> lower_master_key{};
std::optional<bool> lower_system_version{};
};
Result InstallFromFile(ui::ProgressBox* pbox, FsFileSystem* fs, const fs::FsPath& path);
Result InstallFromStdioFile(ui::ProgressBox* pbox, const char* path);
Result InstallFromSource(ui::ProgressBox* pbox, std::shared_ptr<source::Base> source);
Result InstallFromFile(ui::ProgressBox* pbox, FsFileSystem* fs, const fs::FsPath& path, const ConfigOverride& override = {});
Result InstallFromStdioFile(ui::ProgressBox* pbox, const fs::FsPath& path, const ConfigOverride& override = {});
Result InstallFromSource(ui::ProgressBox* pbox, std::shared_ptr<source::Base> source, const fs::FsPath& path, const ConfigOverride& override = {});
Result InstallFromContainer(ui::ProgressBox* pbox, std::shared_ptr<container::Base> container, const ConfigOverride& override = {});
Result InstallFromCollections(ui::ProgressBox* pbox, std::shared_ptr<source::Base> source, const container::Collections& collections, const ConfigOverride& override = {});
Result ParseCnmtNca(const fs::FsPath& path, ncm::PackagedContentMeta& header, std::vector<u8>& extended_header, std::vector<NcmPackagedContentInfo>& infos);
Result ParseControlNca(const fs::FsPath& path, u64 id, void* nacp_out = nullptr, s64 nacp_size = 0, std::vector<u8>* icon_out = nullptr);
} // namespace sphaira::yati

View File

@@ -1,7 +1,18 @@
#include "ui/menus/main_menu.hpp"
#include "ui/error_box.hpp"
#include "ui/option_box.hpp"
#include "ui/bubbles.hpp"
#include "ui/sidebar.hpp"
#include "ui/popup_list.hpp"
#include "ui/option_box.hpp"
#include "ui/progress_box.hpp"
#include "ui/error_box.hpp"
#include "ui/menus/main_menu.hpp"
#include "ui/menus/irs_menu.hpp"
#include "ui/menus/themezer.hpp"
#include "ui/menus/ghdl.hpp"
#include "ui/menus/usb_menu.hpp"
#include "ui/menus/ftp_menu.hpp"
#include "ui/menus/gc_menu.hpp"
#include "app.hpp"
#include "log.hpp"
@@ -15,6 +26,7 @@
#include "defines.hpp"
#include "i18n.hpp"
#include "ftpsrv_helper.hpp"
#include "web.hpp"
#include <nanovg_dk.h>
#include <minIni.h>
@@ -544,7 +556,7 @@ auto App::GetThemeIndex() -> s64 {
return g_app->m_theme_index;
}
auto App::GetDefaultImage(int* w, int* h) -> int {
auto App::GetDefaultImage() -> int {
return g_app->m_default_image;
}
@@ -1374,15 +1386,6 @@ App::App(const char* argv0) {
const long old_launch_count = ini_getl(GetExePath(), "launch_count", 0, App::PLAYLOG_PATH);
ini_putl(GetExePath(), "launch_count", old_launch_count + 1, App::PLAYLOG_PATH);
s64 sd_free_space;
if (R_SUCCEEDED(fs.GetFreeSpace("/", &sd_free_space))) {
log_write("sd_free_space: %zd\n", sd_free_space);
}
s64 sd_total_space;
if (R_SUCCEEDED(fs.GetTotalSpace("/", &sd_total_space))) {
log_write("sd_total_space: %zd\n", sd_total_space);
}
// load default image
if (R_SUCCEEDED(romfsInit())) {
ON_SCOPE_EXIT(romfsExit());
@@ -1392,8 +1395,33 @@ App::App(const char* argv0) {
}
}
// soon (tm)
// ui::bubble::Init();
struct EventDay {
u8 day;
u8 month;
};
static constexpr EventDay event_days[] = {
{ .day = 1, .month = 1 }, // New years
{ .day = 3, .month = 3 }, // March 3 (switch 1)
{ .day = 10, .month = 5 }, // June 10 (switch 2)
{ .day = 15, .month = 5 }, // June 15
{ .day = 25, .month = 12 }, // Christmas
{ .day = 26, .month = 12 },
{ .day = 27, .month = 12 },
{ .day = 28, .month = 12 },
};
const auto time = std::time(nullptr);
const auto tm = std::localtime(&time);
for (auto e : event_days) {
if (e.day == tm->tm_mday && e.month == (tm->tm_mon + 1)) {
ui::bubble::Init();
break;
}
}
App::Push(std::make_shared<ui::menu::main::MainMenu>());
log_write("finished app constructor\n");
@@ -1412,6 +1440,179 @@ void App::PlaySoundEffect(SoundEffect effect) {
plsrPlayerPlay(id);
}
void App::DisplayThemeOptions(bool left_side) {
ui::SidebarEntryArray::Items theme_items{};
const auto theme_meta = App::GetThemeMetaList();
for (auto& p : theme_meta) {
theme_items.emplace_back(p.name);
}
auto options = std::make_shared<ui::Sidebar>("Theme Options"_i18n, left_side ? ui::Sidebar::Side::LEFT : ui::Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(options));
options->Add(std::make_shared<ui::SidebarEntryArray>("Select Theme"_i18n, theme_items, [theme_items](s64& index_out){
App::SetTheme(index_out);
}, App::GetThemeIndex()));
options->Add(std::make_shared<ui::SidebarEntryBool>("Music"_i18n, App::GetThemeMusicEnable(), [](bool& enable){
App::SetThemeMusicEnable(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<ui::SidebarEntryBool>("12 Hour Time"_i18n, App::Get12HourTimeEnable(), [](bool& enable){
App::Set12HourTimeEnable(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
}
void App::DisplayNetworkOptions(bool left_side) {
}
void App::DisplayMiscOptions(bool left_side) {
auto options = std::make_shared<ui::Sidebar>("Misc Options"_i18n, left_side ? ui::Sidebar::Side::LEFT : ui::Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(options));
options->Add(std::make_shared<ui::SidebarEntryCallback>("Themezer"_i18n, [](){
App::Push(std::make_shared<ui::menu::themezer::Menu>());
}));
options->Add(std::make_shared<ui::SidebarEntryCallback>("GitHub"_i18n, [](){
App::Push(std::make_shared<ui::menu::gh::Menu>());
}));
options->Add(std::make_shared<ui::SidebarEntryCallback>("Irs"_i18n, [](){
App::Push(std::make_shared<ui::menu::irs::Menu>());
}));
if (App::IsApplication()) {
options->Add(std::make_shared<ui::SidebarEntryCallback>("Web"_i18n, [](){
WebShow("https://lite.duckduckgo.com/lite");
}));
}
if (App::GetApp()->m_install.Get()) {
if (App::GetFtpEnable()) {
options->Add(std::make_shared<ui::SidebarEntryCallback>("Ftp Install"_i18n, [](){
App::Push(std::make_shared<ui::menu::ftp::Menu>());
}));
}
options->Add(std::make_shared<ui::SidebarEntryCallback>("Usb Install"_i18n, [](){
App::Push(std::make_shared<ui::menu::usb::Menu>());
}));
options->Add(std::make_shared<ui::SidebarEntryCallback>("GameCard Install"_i18n, [](){
App::Push(std::make_shared<ui::menu::gc::Menu>());
}));
}
}
void App::DisplayAdvancedOptions(bool left_side) {
auto options = std::make_shared<ui::Sidebar>("Advanced Options"_i18n, left_side ? ui::Sidebar::Side::LEFT : ui::Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(options));
ui::SidebarEntryArray::Items text_scroll_speed_items;
text_scroll_speed_items.push_back("Slow"_i18n);
text_scroll_speed_items.push_back("Normal"_i18n);
text_scroll_speed_items.push_back("Fast"_i18n);
options->Add(std::make_shared<ui::SidebarEntryBool>("Logging"_i18n, App::GetLogEnable(), [](bool& enable){
App::SetLogEnable(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<ui::SidebarEntryBool>("Replace hbmenu on exit"_i18n, App::GetReplaceHbmenuEnable(), [](bool& enable){
App::SetReplaceHbmenuEnable(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<ui::SidebarEntryArray>("Text scroll speed"_i18n, text_scroll_speed_items, [](s64& index_out){
App::SetTextScrollSpeed(index_out);
}, (s64)App::GetTextScrollSpeed()));
options->Add(std::make_shared<ui::SidebarEntryCallback>("Install options"_i18n, [left_side](){
App::DisplayInstallOptions(left_side);
}));
}
void App::DisplayInstallOptions(bool left_side) {
auto options = std::make_shared<ui::Sidebar>("Install Options"_i18n, left_side ? ui::Sidebar::Side::LEFT : ui::Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(options));
ui::SidebarEntryArray::Items install_items;
install_items.push_back("System memory"_i18n);
install_items.push_back("microSD card"_i18n);
options->Add(std::make_shared<ui::SidebarEntryBool>("Enable"_i18n, App::GetInstallEnable(), [](bool& enable){
App::SetInstallEnable(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<ui::SidebarEntryBool>("Show install warning"_i18n, App::GetInstallPrompt(), [](bool& enable){
App::SetInstallPrompt(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<ui::SidebarEntryArray>("Install location"_i18n, install_items, [](s64& index_out){
App::SetInstallSdEnable(index_out);
}, (s64)App::GetInstallSdEnable()));
options->Add(std::make_shared<ui::SidebarEntryBool>("Allow downgrade"_i18n, App::GetApp()->m_allow_downgrade.Get(), [](bool& enable){
App::GetApp()->m_allow_downgrade.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip if already installed"_i18n, App::GetApp()->m_skip_if_already_installed.Get(), [](bool& enable){
App::GetApp()->m_skip_if_already_installed.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<ui::SidebarEntryBool>("Ticket only"_i18n, App::GetApp()->m_ticket_only.Get(), [](bool& enable){
App::GetApp()->m_ticket_only.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip base"_i18n, App::GetApp()->m_skip_base.Get(), [](bool& enable){
App::GetApp()->m_skip_base.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip Patch"_i18n, App::GetApp()->m_skip_patch.Get(), [](bool& enable){
App::GetApp()->m_skip_patch.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip addon"_i18n, App::GetApp()->m_skip_addon.Get(), [](bool& enable){
App::GetApp()->m_skip_addon.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip data patch"_i18n, App::GetApp()->m_skip_data_patch.Get(), [](bool& enable){
App::GetApp()->m_skip_data_patch.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip ticket"_i18n, App::GetApp()->m_skip_ticket.Get(), [](bool& enable){
App::GetApp()->m_skip_ticket.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<ui::SidebarEntryBool>("skip NCA hash verify"_i18n, App::GetApp()->m_skip_nca_hash_verify.Get(), [](bool& enable){
App::GetApp()->m_skip_nca_hash_verify.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip RSA header verify"_i18n, App::GetApp()->m_skip_rsa_header_fixed_key_verify.Get(), [](bool& enable){
App::GetApp()->m_skip_rsa_header_fixed_key_verify.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip RSA NPDM verify"_i18n, App::GetApp()->m_skip_rsa_npdm_fixed_key_verify.Get(), [](bool& enable){
App::GetApp()->m_skip_rsa_npdm_fixed_key_verify.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<ui::SidebarEntryBool>("Ignore distribution bit"_i18n, App::GetApp()->m_ignore_distribution_bit.Get(), [](bool& enable){
App::GetApp()->m_ignore_distribution_bit.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<ui::SidebarEntryBool>("Convert to standard crypto"_i18n, App::GetApp()->m_convert_to_standard_crypto.Get(), [](bool& enable){
App::GetApp()->m_convert_to_standard_crypto.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<ui::SidebarEntryBool>("Lower master key"_i18n, App::GetApp()->m_lower_master_key.Get(), [](bool& enable){
App::GetApp()->m_lower_master_key.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<ui::SidebarEntryBool>("Lower system version"_i18n, App::GetApp()->m_lower_system_version.Get(), [](bool& enable){
App::GetApp()->m_lower_system_version.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
}
App::~App() {
log_write("starting to exit\n");

View File

@@ -12,14 +12,30 @@
#include <nx/vfs_nx.h>
#include <nx/utils.h>
namespace sphaira::ftpsrv {
namespace {
struct InstallSharedData {
std::mutex mutex;
std::deque<std::string> queued_files;
void* user;
OnInstallStart on_start;
OnInstallWrite on_write;
OnInstallClose on_close;
bool in_progress;
bool enabled;
};
const char* INI_PATH = "/config/ftpsrv/config.ini";
FtpSrvConfig g_ftpsrv_config = {0};
volatile bool g_should_exit = false;
bool g_is_running{false};
Thread g_thread;
std::mutex g_mutex{};
InstallSharedData g_shared_data{};
void ftp_log_callback(enum FTP_API_LOG_TYPE type, const char* msg) {
sphaira::App::NotifyFlashLed();
@@ -29,6 +45,235 @@ void ftp_progress_callback(void) {
sphaira::App::NotifyFlashLed();
}
const char* SUPPORTED_EXT[] = {
".nsp", ".xci", ".nsz", ".xcz",
};
struct VfsUserData {
char* path;
int valid;
};
// ive given up with good names.
void on_thing() {
log_write("[FTP] doing on_thing\n");
std::scoped_lock lock{g_shared_data.mutex};
log_write("[FTP] locked on_thing\n");
if (!g_shared_data.in_progress) {
if (!g_shared_data.queued_files.empty()) {
log_write("[FTP] pushing new file data\n");
if (!g_shared_data.on_start || !g_shared_data.on_start(g_shared_data.user, g_shared_data.queued_files[0].c_str())) {
g_shared_data.queued_files.clear();
} else {
log_write("[FTP] success on new file push\n");
g_shared_data.in_progress = true;
}
}
}
}
int vfs_install_open(void* user, const char* path, enum FtpVfsOpenMode mode) {
{
std::scoped_lock lock{g_shared_data.mutex};
auto data = static_cast<VfsUserData*>(user);
data->valid = 0;
if (mode != FtpVfsOpenMode_WRITE) {
errno = EACCES;
return -1;
}
if (!g_shared_data.enabled) {
errno = EACCES;
return -1;
}
const char* ext = strrchr(path, '.');
if (!ext) {
errno = EACCES;
return -1;
}
bool found = false;
for (size_t i = 0; i < std::size(SUPPORTED_EXT); i++) {
if (!strcasecmp(ext, SUPPORTED_EXT[i])) {
found = true;
break;
}
}
if (!found) {
errno = EINVAL;
return -1;
}
// check if we already have this file queued.
auto it = std::find(g_shared_data.queued_files.cbegin(), g_shared_data.queued_files.cend(), path);
if (it != g_shared_data.queued_files.cend()) {
errno = EEXIST;
return -1;
}
g_shared_data.queued_files.push_back(path);
data->path = strdup(path);
data->valid = true;
}
on_thing();
log_write("[FTP] got file: %s\n", path);
return 0;
}
int vfs_install_read(void* user, void* buf, size_t size) {
errno = EACCES;
return -1;
}
int vfs_install_write(void* user, const void* buf, size_t size) {
std::scoped_lock lock{g_shared_data.mutex};
if (!g_shared_data.enabled) {
errno = EACCES;
return -1;
}
auto data = static_cast<VfsUserData*>(user);
if (!data->valid) {
errno = EACCES;
return -1;
}
if (!g_shared_data.on_write || !g_shared_data.on_write(g_shared_data.user, buf, size)) {
errno = EIO;
return -1;
}
return size;
}
int vfs_install_seek(void* user, const void* buf, size_t size, size_t off) {
errno = ESPIPE;
return -1;
}
int vfs_install_isfile_open(void* user) {
std::scoped_lock lock{g_shared_data.mutex};
auto data = static_cast<VfsUserData*>(user);
return data->valid;
}
int vfs_install_isfile_ready(void* user) {
std::scoped_lock lock{g_shared_data.mutex};
auto data = static_cast<VfsUserData*>(user);
const auto ready = !g_shared_data.queued_files.empty() && data->path == g_shared_data.queued_files[0];
return ready;
}
int vfs_install_close(void* user) {
{
log_write("[FTP] closing file\n");
std::scoped_lock lock{g_shared_data.mutex};
auto data = static_cast<VfsUserData*>(user);
if (data->valid) {
log_write("[FTP] closing valid file\n");
auto it = std::find(g_shared_data.queued_files.cbegin(), g_shared_data.queued_files.cend(), data->path);
if (it != g_shared_data.queued_files.cend()) {
if (it == g_shared_data.queued_files.cbegin()) {
log_write("[FTP] closing current file\n");
if (g_shared_data.on_close) {
g_shared_data.on_close(g_shared_data.user);
}
g_shared_data.in_progress = false;
} else {
log_write("[FTP] closing other file...\n");
}
g_shared_data.queued_files.erase(it);
} else {
log_write("[FTP] could not find file in queue...\n");
}
if (data->path) {
free(data->path);
}
data->valid = 0;
}
memset(data, 0, sizeof(*data));
}
on_thing();
return 0;
}
int vfs_install_opendir(void* user, const char* path) {
return 0;
}
const char* vfs_install_readdir(void* user, void* user_entry) {
return NULL;
}
int vfs_install_dirlstat(void* user, const void* user_entry, const char* path, struct stat* st) {
st->st_nlink = 1;
st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
return 0;
}
int vfs_install_isdir_open(void* user) {
return 1;
}
int vfs_install_closedir(void* user) {
return 0;
}
int vfs_install_stat(const char* path, struct stat* st) {
st->st_nlink = 1;
st->st_mode = S_IFDIR | S_IWUSR | S_IWGRP | S_IWOTH;
return 0;
}
int vfs_install_mkdir(const char* path) {
return -1;
}
int vfs_install_unlink(const char* path) {
return -1;
}
int vfs_install_rmdir(const char* path) {
return -1;
}
int vfs_install_rename(const char* src, const char* dst) {
return -1;
}
FtpVfs g_vfs_install = {
.open = vfs_install_open,
.read = vfs_install_read,
.write = vfs_install_write,
.seek = vfs_install_seek,
.close = vfs_install_close,
.isfile_open = vfs_install_isfile_open,
.isfile_ready = vfs_install_isfile_ready,
.opendir = vfs_install_opendir,
.readdir = vfs_install_readdir,
.dirlstat = vfs_install_dirlstat,
.closedir = vfs_install_closedir,
.isdir_open = vfs_install_isdir_open,
.stat = vfs_install_stat,
.lstat = vfs_install_stat,
.mkdir = vfs_install_mkdir,
.unlink = vfs_install_unlink,
.rmdir = vfs_install_rmdir,
.rename = vfs_install_rename,
};
void loop(void* arg) {
while (!g_should_exit) {
ftpsrv_init(&g_ftpsrv_config);
@@ -44,8 +289,6 @@ void loop(void* arg) {
} // namespace
namespace sphaira::ftpsrv {
bool Init() {
std::scoped_lock lock{g_mutex};
if (g_is_running) {
@@ -84,6 +327,9 @@ bool Init() {
mount_bis = ini_getbool("Nx-App", "mount_bis", mount_bis, INI_PATH);
save_writable = ini_getbool("Nx-App", "save_writable", save_writable, INI_PATH);
mount_devices = true;
g_ftpsrv_config.timeout = 0;
if (!g_ftpsrv_config.port) {
return false;
}
@@ -93,7 +339,13 @@ bool Init() {
g_ftpsrv_config.anon = true;
}
vfs_nx_init(mount_devices, save_writable, mount_bis);
const VfsNxCustomPath custom = {
.name = "install",
.user = NULL,
.func = &g_vfs_install,
};
vfs_nx_init(&custom, mount_devices, save_writable, mount_bis);
Result rc;
if (R_FAILED(rc = threadCreate(&g_thread, loop, nullptr, nullptr, 1024*16, 0x2C, 2))) {
@@ -123,6 +375,40 @@ void Exit() {
fsdev_wrapUnmountAll();
}
void InitInstallMode(void* user, OnInstallStart on_start, OnInstallWrite on_write, OnInstallClose on_close) {
std::scoped_lock lock{g_shared_data.mutex};
g_shared_data.user = user;
g_shared_data.on_start = on_start;
g_shared_data.on_write = on_write;
g_shared_data.on_close = on_close;
g_shared_data.enabled = true;
}
void DisableInstallMode() {
std::scoped_lock lock{g_shared_data.mutex};
g_shared_data.enabled = false;
}
unsigned GetPort() {
std::scoped_lock lock{g_mutex};
return g_ftpsrv_config.port;
}
bool IsAnon() {
std::scoped_lock lock{g_mutex};
return g_ftpsrv_config.anon;
}
const char* GetUser() {
std::scoped_lock lock{g_mutex};
return g_ftpsrv_config.user;
}
const char* GetPass() {
std::scoped_lock lock{g_mutex};
return g_ftpsrv_config.pass;
}
} // namespace sphaira::ftpsrv
extern "C" {

View File

@@ -66,6 +66,9 @@ void userAppInit(void) {
if (R_FAILED(rc = ncmInitialize()))
diagAbortWithResult(rc);
// it doesn't matter if this fails.
appletSetScreenShotPermission(AppletScreenShotPermission_Enable);
log_nxlink_init();
}

View File

@@ -1005,7 +1005,7 @@ auto install_forwarder(ui::ProgressBox* pbox, OwoConfig& config, NcmStorageId st
}
auto install_forwarder(OwoConfig& config, NcmStorageId storage_id) -> Result {
App::Push(std::make_shared<ui::ProgressBox>("Installing Forwarder"_i18n, [config, storage_id](auto pbox) mutable -> bool {
App::Push(std::make_shared<ui::ProgressBox>(0, "Installing Forwarder"_i18n, config.name, [config, storage_id](auto pbox) mutable -> bool {
return R_SUCCEEDED(install_forwarder(pbox, config, storage_id));
}));
R_SUCCEED();

View File

@@ -34,8 +34,35 @@ auto List::ClampY(float y, s64 count) const -> float {
return y;
}
void List::OnUpdate(Controller* controller, TouchInfo* touch, s64 count, TouchCallback callback) {
if (touch->is_clicked && touch->in_range(GetPos())) {
void List::OnUpdate(Controller* controller, TouchInfo* touch, s64 index, s64 count, TouchCallback callback) {
const auto page_up_button = m_row == 1 ? Button::DPAD_LEFT : Button::L2;
const auto page_down_button = m_row == 1 ? Button::DPAD_RIGHT : Button::R2;
if (controller->GotDown(Button::DOWN)) {
if (ScrollDown(index, m_row, count)) {
callback(false, index);
}
} else if (controller->GotDown(Button::UP)) {
if (ScrollUp(index, m_row, count)) {
callback(false, index);
}
} else if (controller->GotDown(page_down_button)) {
if (ScrollDown(index, m_page, count)) {
callback(false, index);
}
} else if (controller->GotDown(page_up_button)) {
if (ScrollUp(index, m_page, count)) {
callback(false, index);
}
} else if (m_row > 1 && controller->GotDown(Button::RIGHT)) {
if (count && index < (count - 1) && (index + 1) % m_row != 0) {
callback(false, index + 1);
}
} else if (m_row > 1 && controller->GotDown(Button::LEFT)) {
if (count && index != 0 && (index % m_row) != 0) {
callback(false, index - 1);
}
} else if (touch->is_clicked && touch->in_range(GetPos())) {
auto v = m_v;
v.y -= ClampY(m_yoff + m_y_prog, count);
@@ -63,7 +90,7 @@ void List::OnUpdate(Controller* controller, TouchInfo* touch, s64 count, TouchCa
vv.h = std::min(v.y + v.h, m_pos.y + m_pos.h) - v.y;
if (touch->in_range(vv)) {
callback(i);
callback(true, i);
return;
}
}

View File

@@ -236,18 +236,15 @@ void DrawIcon(NVGcontext* vg, const LazyImage& l, const LazyImage& d, float x, f
bool crop = false;
if (iw < w || ih < h) {
rounded_image = false;
gfx::drawRect(vg, x, y, w, h, nvgRGB(i.first_pixel[0], i.first_pixel[1], i.first_pixel[2]), rounded);
gfx::drawRect(vg, x, y, w, h, nvgRGB(i.first_pixel[0], i.first_pixel[1], i.first_pixel[2]), rounded ? 15 : 0);
}
if (iw > w || ih > h) {
crop = true;
nvgSave(vg);
nvgIntersectScissor(vg, x, y, w, h);
}
if (rounded_image) {
gfx::drawImageRounded(vg, ix, iy, iw, ih, i.image);
} else {
gfx::drawImage(vg, ix, iy, iw, ih, i.image);
}
gfx::drawImage(vg, ix, iy, iw, ih, i.image, rounded_image ? 15 : 0);
if (crop) {
nvgRestore(vg);
}
@@ -769,7 +766,7 @@ void EntryMenu::UpdateOptions() {
};
const auto install = [this](){
App::Push(std::make_shared<ProgressBox>("Installing "_i18n + m_entry.title, [this](auto pbox){
App::Push(std::make_shared<ProgressBox>(m_entry.image.image, "Downloading "_i18n, m_entry.title, [this](auto pbox){
return InstallApp(pbox, m_entry);
}, [this](bool success){
if (success) {
@@ -782,7 +779,7 @@ void EntryMenu::UpdateOptions() {
};
const auto uninstall = [this](){
App::Push(std::make_shared<ProgressBox>("Uninstalling "_i18n + m_entry.title, [this](auto pbox){
App::Push(std::make_shared<ProgressBox>(m_entry.image.image, "Uninstalling "_i18n, m_entry.title, [this](auto pbox){
return UninstallApp(pbox, m_entry);
}, [this](bool success){
if (success) {
@@ -854,48 +851,6 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"AppStore"_i18n}
fs.CreateDirectoryRecursively("/switch/sphaira/cache/appstore/screens");
this->SetActions(
std::make_pair(Button::RIGHT, Action{[this](){
if (m_entries_current.empty()) {
return;
}
if (m_index < (m_entries_current.size() - 1) && (m_index + 1) % 3 != 0) {
SetIndex(m_index + 1);
App::PlaySoundEffect(SoundEffect_Scroll);
log_write("moved right\n");
}
}}),
std::make_pair(Button::LEFT, Action{[this](){
if (m_entries_current.empty()) {
return;
}
if (m_index != 0 && (m_index % 3) != 0) {
SetIndex(m_index - 1);
App::PlaySoundEffect(SoundEffect_Scroll);
log_write("moved left\n");
}
}}),
std::make_pair(Button::DOWN, Action{[this](){
if (m_list->ScrollDown(m_index, 3, m_entries_current.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::UP, Action{[this](){
if (m_list->ScrollUp(m_index, 3, m_entries_current.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::R2, Action{[this](){
if (m_list->ScrollDown(m_index, 9, m_entries_current.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::L2, Action{[this](){
if (m_list->ScrollUp(m_index, 9, m_entries_current.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::A, Action{"Info"_i18n, [this](){
if (m_entries_current.empty()) {
// log_write("pushing A when empty: size: %zu count: %zu\n", repo_json.size(), m_entries_current.size());
@@ -983,8 +938,8 @@ Menu::~Menu() {
void Menu::Update(Controller* controller, TouchInfo* touch) {
MenuBase::Update(controller, touch);
m_list->OnUpdate(controller, touch, m_entries_current.size(), [this](auto i) {
if (m_index == i) {
m_list->OnUpdate(controller, touch, m_index, m_entries_current.size(), [this](bool touch, auto i) {
if (touch && m_index == i) {
FireAction(Button::A);
} else {
App::PlaySoundEffect(SoundEffect_Focus);
@@ -1096,16 +1051,16 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
float i_size = 22;
switch (e.status) {
case EntryStatus::Get:
gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_get.image);
gfx::drawImage(vg, x + w - 30.f, y + 110, i_size, i_size, m_get.image, 15);
break;
case EntryStatus::Installed:
gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_installed.image);
gfx::drawImage(vg, x + w - 30.f, y + 110, i_size, i_size, m_installed.image, 15);
break;
case EntryStatus::Local:
gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_local.image);
gfx::drawImage(vg, x + w - 30.f, y + 110, i_size, i_size, m_local.image, 15);
break;
case EntryStatus::Update:
gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_update.image);
gfx::drawImage(vg, x + w - 30.f, y + 110, i_size, i_size, m_update.image, 15);
break;
}
});

View File

@@ -60,11 +60,29 @@ constexpr std::string_view INSTALL_EXTENSIONS[] = {
struct RomDatabaseEntry {
std::string_view folder;
std::string_view database;
// uses the naming scheme from retropie.
std::string_view folder{};
// uses the naming scheme from Retroarch.
std::string_view database{};
// custom alias, to make everyone else happy.
std::array<std::string_view, 4> alias{};
// compares against all of the above strings.
auto IsDatabase(std::string_view name) const {
if (name == folder || name == database) {
return true;
}
for (const auto& str : alias) {
if (!str.empty() && name == str) {
return true;
}
}
return false;
}
};
// using PathPair = std::pair<std::string_view, std::string_view>;
constexpr RomDatabaseEntry PATHS[]{
{ "3do", "The 3DO Company - 3DO"},
{ "atari800", "Atari - 8-bit"},
@@ -100,6 +118,14 @@ constexpr RomDatabaseEntry PATHS[]{
{ "pico8", "Sega - PICO"},
{ "wonderswan", "Bandai - WonderSwan"},
{ "wonderswancolor", "Bandai - WonderSwan Color"},
{ "mame", "MAME 2000", { "MAME", "mame-libretro", } },
{ "mame", "MAME 2003", { "MAME", "mame-libretro", } },
{ "mame", "MAME 2003-Plus", { "MAME", "mame-libretro", } },
{ "neogeo", "SNK - Neo Geo Pocket" },
{ "neogeo", "SNK - Neo Geo Pocket Color" },
{ "neogeo", "SNK - Neo Geo CD" },
};
constexpr fs::FsPath DAYBREAK_PATH{"/switch/daybreak.nro"};
@@ -120,49 +146,49 @@ auto IsExtension(std::string_view ext1, std::string_view ext2) -> bool {
// tries to find database path using folder name
// names are taken from retropie
// retroarch database names can also be used
auto GetRomDatabaseFromPath(std::string_view path) -> int {
using RomDatabaseIndexs = std::vector<size_t>;
auto GetRomDatabaseFromPath(std::string_view path) -> RomDatabaseIndexs {
if (path.length() <= 1) {
return -1;
return {};
}
// this won't fail :)
RomDatabaseIndexs indexs;
const auto db_name = path.substr(path.find_last_of('/') + 1);
// log_write("new path: %s\n", db_name.c_str());
for (int i = 0; i < std::size(PATHS); i++) {
auto& p = PATHS[i];
if ((
p.folder.length() == db_name.length() && !strncasecmp(p.folder.data(), db_name.data(), p.folder.length())) ||
(p.database.length() == db_name.length() && !strncasecmp(p.database.data(), db_name.data(), p.database.length()))) {
const auto& p = PATHS[i];
if (p.IsDatabase(db_name)) {
log_write("found it :) %.*s\n", (int)p.database.length(), p.database.data());
return i;
indexs.emplace_back(i);
}
}
// if we failed, try again but with the folder about
// "/roms/psx/scooby-doo/scooby-doo.bin", this will check psx
const auto last_off = path.substr(0, path.find_last_of('/'));
if (const auto off = last_off.find_last_of('/'); off != std::string_view::npos) {
const auto db_name2 = last_off.substr(off + 1);
// printf("got db: %s\n", db_name2.c_str());
for (int i = 0; i < std::size(PATHS); i++) {
auto& p = PATHS[i];
if ((
p.folder.length() == db_name2.length() && !strcasecmp(p.folder.data(), db_name2.data())) ||
(p.database.length() == db_name2.length() && !strcasecmp(p.database.data(), db_name2.data()))) {
log_write("found it :) %.*s\n", (int)p.database.length(), p.database.data());
return i;
if (indexs.empty()) {
const auto last_off = path.substr(0, path.find_last_of('/'));
if (const auto off = last_off.find_last_of('/'); off != std::string_view::npos) {
const auto db_name2 = last_off.substr(off + 1);
// printf("got db: %s\n", db_name2.c_str());
for (int i = 0; i < std::size(PATHS); i++) {
const auto& p = PATHS[i];
if (p.IsDatabase(db_name2)) {
log_write("found it :) %.*s\n", (int)p.database.length(), p.database.data());
indexs.emplace_back(i);
}
}
}
}
return -1;
return indexs;
}
//
auto GetRomIcon(fs::FsNative* fs, ProgressBox* pbox, std::string filename, std::string extension, int db_idx, const NroEntry& nro) {
auto GetRomIcon(fs::FsNative* fs, ProgressBox* pbox, std::string filename, const RomDatabaseIndexs& db_indexs, const NroEntry& nro) {
// if no db entries, use nro icon
if (db_idx < 0) {
if (db_indexs.empty()) {
log_write("using nro image\n");
return nro_get_icon(nro.path, nro.icon_size, nro.icon_offset);
}
@@ -184,49 +210,51 @@ auto GetRomIcon(fs::FsNative* fs, ProgressBox* pbox, std::string filename, std::
#define RA_THUMBNAIL_PATH "/retroarch/thumbnails/"
#define RA_BOXART_EXT ".png"
const auto system_name = std::string{PATHS[db_idx].database.data(), PATHS[db_idx].database.length()};//GetDatabaseFromExt(database, extension);
auto system_name_gh = system_name + "/master";
for (auto& c : system_name_gh) {
if (c == ' ') {
c = '_';
for (auto db_idx : db_indexs) {
const auto system_name = std::string{PATHS[db_idx].database.data(), PATHS[db_idx].database.length()};//GetDatabaseFromExt(database, extension);
auto system_name_gh = system_name + "/master";
for (auto& c : system_name_gh) {
if (c == ' ') {
c = '_';
}
}
}
std::string filename_gh;
filename_gh.reserve(filename.size());
for (auto c : filename) {
if (c == ' ') {
filename_gh += "%20";
} else {
filename_gh.push_back(c);
std::string filename_gh;
filename_gh.reserve(filename.size());
for (auto c : filename) {
if (c == ' ') {
filename_gh += "%20";
} else {
filename_gh.push_back(c);
}
}
}
const std::string thumbnail_path = system_name + RA_BOXART_NAME + filename + RA_BOXART_EXT;
const std::string ra_thumbnail_path = RA_THUMBNAIL_PATH + thumbnail_path;
const std::string ra_thumbnail_url = RA_BOXART_URL + thumbnail_path;
const std::string gh_thumbnail_url = GH_BOXART_URL + system_name_gh + RA_BOXART_NAME + filename_gh + RA_BOXART_EXT;
const std::string thumbnail_path = system_name + RA_BOXART_NAME + filename + RA_BOXART_EXT;
const std::string ra_thumbnail_path = RA_THUMBNAIL_PATH + thumbnail_path;
const std::string ra_thumbnail_url = RA_BOXART_URL + thumbnail_path;
const std::string gh_thumbnail_url = GH_BOXART_URL + system_name_gh + RA_BOXART_NAME + filename_gh + RA_BOXART_EXT;
log_write("starting image convert on: %s\n", ra_thumbnail_path.c_str());
// try and find icon locally
if (!pbox->ShouldExit()) {
pbox->NewTransfer("Trying to load "_i18n + ra_thumbnail_path);
std::vector<u8> image_file;
if (R_SUCCEEDED(fs->read_entire_file(ra_thumbnail_path.c_str(), image_file))) {
return image_file;
log_write("starting image convert on: %s\n", ra_thumbnail_path.c_str());
// try and find icon locally
if (!pbox->ShouldExit()) {
pbox->NewTransfer("Trying to load "_i18n + ra_thumbnail_path);
std::vector<u8> image_file;
if (R_SUCCEEDED(fs->read_entire_file(ra_thumbnail_path.c_str(), image_file))) {
return image_file;
}
}
}
// try and download icon
if (!pbox->ShouldExit()) {
pbox->NewTransfer("Downloading "_i18n + gh_thumbnail_url);
const auto result = curl::Api().ToMemory(
curl::Url{gh_thumbnail_url},
curl::OnProgress{pbox->OnDownloadProgressCallback()}
);
// try and download icon
if (!pbox->ShouldExit()) {
pbox->NewTransfer("Downloading "_i18n + gh_thumbnail_url);
const auto result = curl::Api().ToMemory(
curl::Url{gh_thumbnail_url},
curl::OnProgress{pbox->OnDownloadProgressCallback()}
);
if (result.success && !result.data.empty()) {
return result.data;
if (result.success && !result.data.empty()) {
return result.data;
}
}
}
@@ -251,26 +279,6 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i1
m_selected_count--;
}
}}),
std::make_pair(Button::DOWN, Action{[this](){
if (m_list->ScrollDown(m_index, 1, m_entries_current.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::UP, Action{[this](){
if (m_list->ScrollUp(m_index, 1, m_entries_current.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::DPAD_RIGHT, Action{[this](){
if (m_list->ScrollDown(m_index, 8, m_entries_current.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::DPAD_LEFT, Action{[this](){
if (m_list->ScrollUp(m_index, 8, m_entries_current.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::A, Action{"Open"_i18n, [this](){
if (m_entries_current.empty()) {
return;
@@ -598,8 +606,8 @@ Menu::~Menu() {
void Menu::Update(Controller* controller, TouchInfo* touch) {
MenuBase::Update(controller, touch);
m_list->OnUpdate(controller, touch, m_entries_current.size(), [this](auto i) {
if (m_index == i) {
m_list->OnUpdate(controller, touch, m_index, m_entries_current.size(), [this](bool touch, auto i) {
if (touch && m_index == i) {
FireAction(Button::A);
} else {
App::PlaySoundEffect(SoundEffect_Focus);
@@ -774,7 +782,7 @@ void Menu::InstallForwarder() {
if (op_index) {
const auto assoc = assoc_list[*op_index];
log_write("pushing it\n");
App::Push(std::make_shared<ProgressBox>("Installing Forwarder"_i18n, [assoc, this](auto pbox) -> bool {
App::Push(std::make_shared<ProgressBox>(0, "Installing Forwarder"_i18n, GetEntry().name, [assoc, this](auto pbox) -> bool {
log_write("inside callback\n");
NroEntry nro{};
@@ -784,8 +792,7 @@ void Menu::InstallForwarder() {
return false;
}
log_write("got nro data\n");
std::string file_name = GetEntry().GetInternalName();
std::string extension = GetEntry().GetInternalExtension();
auto file_name = assoc.use_base_name ? GetEntry().GetName() : GetEntry().GetInternalName();
if (auto pos = file_name.find_last_of('.'); pos != std::string::npos) {
log_write("got filename\n");
@@ -793,7 +800,7 @@ void Menu::InstallForwarder() {
log_write("got filename2: %s\n\n", file_name.c_str());
}
const auto db_idx = GetRomDatabaseFromPath(m_path);
const auto db_indexs = GetRomDatabaseFromPath(m_path);
OwoConfig config{};
config.nro_path = assoc.path.toString();
@@ -801,7 +808,8 @@ void Menu::InstallForwarder() {
config.name = nro.nacp.lang[0].name + std::string{" | "} + file_name;
// config.name = file_name;
config.nacp = nro.nacp;
config.icon = GetRomIcon(m_fs.get(), pbox, file_name, extension, db_idx, nro);
config.icon = GetRomIcon(m_fs.get(), pbox, file_name, db_indexs, nro);
pbox->SetImageDataConst(config.icon);
return R_SUCCEEDED(App::Install(pbox, config));
}));
@@ -822,7 +830,7 @@ void Menu::InstallFiles(const std::vector<FileEntry>& targets) {
if (op_index && *op_index) {
App::PopToMenu();
App::Push(std::make_shared<ui::ProgressBox>("Installing App"_i18n, [this, targets](auto pbox) mutable -> bool {
App::Push(std::make_shared<ui::ProgressBox>(0, "Installing "_i18n, "", [this, targets](auto pbox) mutable -> bool {
for (auto& e : targets) {
const auto rc = yati::InstallFromFile(pbox, &m_fs->m_fs, GetNewPath(e));
if (rc == yati::Result_Cancelled) {
@@ -912,32 +920,32 @@ auto Menu::Scan(const fs::FsPath& new_path, bool is_walk_up) -> Result {
auto Menu::FindFileAssocFor() -> std::vector<FileAssocEntry> {
// only support roms in correctly named folders, sorry!
const auto db_idx = GetRomDatabaseFromPath(m_path);
const auto db_indexs = GetRomDatabaseFromPath(m_path);
const auto& entry = GetEntry();
const auto extension = entry.internal_extension.empty() ? entry.extension : entry.internal_extension;
if (extension.empty()) {
const auto extension = entry.extension;
const auto internal_extension = entry.internal_extension.empty() ? entry.extension : entry.internal_extension;
if (extension.empty() && internal_extension.empty()) {
// log_write("failed to get extension for db: %s path: %s\n", database_entry.c_str(), m_path);
return {};
}
// log_write("got extension for db: %s path: %s\n", database_entry.c_str(), m_path);
std::vector<FileAssocEntry> out_entries;
if (db_idx >= 0) {
if (!db_indexs.empty()) {
// if database isn't empty, then we are in a valid folder
// search for an entry that matches the db and ext
for (const auto& assoc : m_assoc_entries) {
for (const auto& assoc_db : assoc.database) {
if (assoc_db == PATHS[db_idx].folder || assoc_db == PATHS[db_idx].database) {
for (const auto& assoc_ext : assoc.ext) {
if (assoc_ext == extension) {
log_write("found ext: %s assoc_ext: %s assoc.ext: %s\n", assoc.path.s, assoc_ext.c_str(), extension.c_str());
// if (assoc_db == PATHS[db_idx].folder || assoc_db == PATHS[db_idx].database) {
for (auto db_idx : db_indexs) {
if (PATHS[db_idx].IsDatabase(assoc_db)) {
if (assoc.IsExtension(extension, internal_extension)) {
out_entries.emplace_back(assoc);
goto jump;
}
}
}
}
jump:
}
} else {
// otherwise, if not in a valid folder, find an entry that doesn't
@@ -948,11 +956,9 @@ auto Menu::FindFileAssocFor() -> std::vector<FileAssocEntry> {
// to be in the correct folder, ie psx, to know what system that .iso is for.
for (const auto& assoc : m_assoc_entries) {
if (assoc.database.empty()) {
for (const auto& assoc_ext : assoc.ext) {
if (assoc_ext == extension) {
log_write("found ext: %s\n", assoc.path.s);
out_entries.emplace_back(assoc);
}
if (assoc.IsExtension(extension, internal_extension)) {
log_write("found ext: %s\n", assoc.path.s);
out_entries.emplace_back(assoc);
}
}
}
@@ -1009,6 +1015,10 @@ void Menu::LoadAssocEntriesPath(const fs::FsPath& path) {
}
}
}
} else if (!strcmp(Key, "use_base_name")) {
if (!strcmp(Value, "true") || !strcmp(Value, "1")) {
assoc->use_base_name = true;
}
}
return 1;
}, &assoc, full_path);
@@ -1044,7 +1054,7 @@ void Menu::LoadAssocEntriesPath(const fs::FsPath& path) {
continue;
}
// log_write("\tpath: %s\n", assoc.path.c_str());
// log_write("\tpath: %s\n", assoc.path.s);
// log_write("\tname: %s\n", assoc.name.c_str());
// for (const auto& ext : assoc.ext) {
// log_write("\t\text: %s\n", ext.c_str());
@@ -1197,7 +1207,7 @@ void Menu::OnDeleteCallback() {
Scan(m_path);
log_write("did delete\n");
} else {
App::Push(std::make_shared<ProgressBox>("Deleting"_i18n, [this](auto pbox){
App::Push(std::make_shared<ProgressBox>(0, "Deleting"_i18n, "", [this](auto pbox){
FsDirCollections collections;
// build list of dirs / files
@@ -1290,7 +1300,7 @@ void Menu::OnPasteCallback() {
Scan(m_path);
log_write("did paste\n");
} else {
App::Push(std::make_shared<ProgressBox>("Pasting"_i18n, [this](auto pbox){
App::Push(std::make_shared<ProgressBox>(0, "Pasting"_i18n, "", [this](auto pbox){
if (m_selected_type == SelectedType::Cut) {
for (const auto& p : m_selected_files) {

View File

@@ -0,0 +1,300 @@
#include "ui/menus/ftp_menu.hpp"
#include "yati/yati.hpp"
#include "app.hpp"
#include "defines.hpp"
#include "log.hpp"
#include "ui/nvg_util.hpp"
#include "i18n.hpp"
#include "ftpsrv_helper.hpp"
#include <cstring>
namespace sphaira::ui::menu::ftp {
namespace {
constexpr u64 MAX_BUFFER_SIZE = 1024*1024*32;
constexpr u64 SLEEPNS = 1000;
volatile bool IN_PUSH_THREAD{};
bool OnInstallStart(void* user, const char* path) {
auto menu = (Menu*)user;
log_write("[INSTALL] inside OnInstallStart()\n");
for (;;) {
mutexLock(&menu->m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&menu->m_mutex));
if (menu->m_state != State::Progress) {
break;
}
if (menu->GetToken().stop_requested()) {
return false;
}
svcSleepThread(1e+6);
}
log_write("[INSTALL] OnInstallStart() got state: %u\n", (u8)menu->m_state);
if (menu->m_source) {
log_write("[INSTALL] OnInstallStart() we have source\n");
for (;;) {
mutexLock(&menu->m_source->m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&menu->m_source->m_mutex));
if (!IN_PUSH_THREAD) {
break;
}
if (menu->GetToken().stop_requested()) {
return false;
}
svcSleepThread(1e+6);
}
log_write("[INSTALL] OnInstallStart() stopped polling source\n");
}
log_write("[INSTALL] OnInstallStart() doing make_shared\n");
menu->m_source = std::make_shared<StreamFtp>(path, menu->GetToken());
mutexLock(&menu->m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&menu->m_mutex));
menu->m_state = State::Connected;
log_write("[INSTALL] OnInstallStart() done make shared\n");
return true;
}
bool OnInstallWrite(void* user, const void* buf, size_t size) {
auto menu = (Menu*)user;
return menu->m_source->Push(buf, size);
}
void OnInstallClose(void* user) {
auto menu = (Menu*)user;
menu->m_source->Disable();
}
} // namespace
StreamFtp::StreamFtp(const fs::FsPath& path, std::stop_token token) {
m_path = path;
m_token = token;
m_buffer.reserve(MAX_BUFFER_SIZE);
m_active = true;
}
Result StreamFtp::ReadChunk(void* buf, s64 size, u64* bytes_read) {
while (!m_token.stop_requested()) {
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
if (m_buffer.empty()) {
if (!m_active) {
break;
}
svcSleepThread(SLEEPNS);
} else {
size = std::min<s64>(size, m_buffer.size());
std::memcpy(buf, m_buffer.data(), size);
m_buffer.erase(m_buffer.begin(), m_buffer.begin() + size);
*bytes_read = size;
R_SUCCEED();
}
}
return 0x1;
}
bool StreamFtp::Push(const void* buf, s64 size) {
IN_PUSH_THREAD = true;
ON_SCOPE_EXIT(IN_PUSH_THREAD = false);
while (!m_token.stop_requested()) {
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
if (!m_active) {
break;
}
if (m_buffer.size() + size >= MAX_BUFFER_SIZE) {
svcSleepThread(SLEEPNS);
} else {
const auto offset = m_buffer.size();
m_buffer.resize(offset + size);
std::memcpy(m_buffer.data() + offset, buf, size);
return true;
}
}
return false;
}
void StreamFtp::Disable() {
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
m_active = false;
}
Menu::Menu() : MenuBase{"FTP Install (EXPERIMENTAL)"_i18n} {
SetAction(Button::B, Action{"Back"_i18n, [this](){
SetPop();
}});
SetAction(Button::X, Action{"Options"_i18n, [this](){
App::DisplayInstallOptions(false);
}});
mutexInit(&m_mutex);
ftpsrv::InitInstallMode(this, OnInstallStart, OnInstallWrite, OnInstallClose);
m_port = ftpsrv::GetPort();
m_anon = ftpsrv::IsAnon();
if (!m_anon) {
m_user = ftpsrv::GetUser();
m_pass = ftpsrv::GetPass();
}
}
Menu::~Menu() {
// signal for thread to exit and wait.
ftpsrv::DisableInstallMode();
m_stop_source.request_stop();
if (m_source) {
m_source->Disable();
}
log_write("closing data!!!!\n");
}
void Menu::Update(Controller* controller, TouchInfo* touch) {
MenuBase::Update(controller, touch);
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
switch (m_state) {
case State::None:
break;
case State::Connected:
log_write("set to progress\n");
m_state = State::Progress;
log_write("got connection\n");
App::Push(std::make_shared<ui::ProgressBox>(0, "Installing "_i18n, "", [this](auto pbox) mutable -> bool {
log_write("inside progress box\n");
const auto rc = yati::InstallFromSource(pbox, m_source, m_source->m_path);
if (R_FAILED(rc)) {
m_source->Disable();
return false;
}
log_write("progress box is done\n");
return true;
}, [this](bool result){
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
if (result) {
App::Notify("Ftp install success!"_i18n);
m_state = State::Done;
} else {
App::Notify("Ftp install failed!"_i18n);
m_state = State::Failed;
}
}));
break;
case State::Progress:
case State::Done:
case State::Failed:
break;
}
}
void Menu::Draw(NVGcontext* vg, Theme* theme) {
MenuBase::Draw(vg, theme);
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
if (m_ip) {
if (m_type == NifmInternetConnectionType_WiFi) {
SetSubHeading("Connection Type: WiFi | Strength: "_i18n + std::to_string(m_strength));
} else {
SetSubHeading("Connection Type: Ethernet"_i18n);
}
} else {
SetSubHeading("Connection Type: None"_i18n);
}
const float start_x = 80;
const float font_size = 22;
const float spacing = 33;
float start_y = 125;
float bounds[4];
nvgFontSize(vg, font_size);
// note: textbounds strips spaces...todo: use nvgTextGlyphPositions() instead.
#define draw(key, ...) \
gfx::textBounds(vg, start_x, start_y, bounds, key.c_str()); \
gfx::drawTextArgs(vg, start_x, start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT), key.c_str()); \
gfx::drawTextArgs(vg, bounds[2], start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_SELECTED), __VA_ARGS__); \
start_y += spacing;
if (m_ip) {
draw("Host:"_i18n, " %u.%u.%u.%u", m_ip&0xFF, (m_ip>>8)&0xFF, (m_ip>>16)&0xFF, (m_ip>>24)&0xFF);
draw("Port:"_i18n, " %u", m_port);
if (!m_anon) {
draw("Username:"_i18n, " %s", m_user);
draw("Password:"_i18n, " %s", m_pass);
}
if (m_type == NifmInternetConnectionType_WiFi) {
NifmNetworkProfileData profile{};
if (R_SUCCEEDED(nifmGetCurrentNetworkProfile(&profile))) {
const auto& settings = profile.wireless_setting_data;
std::string passphrase;
std::transform(std::cbegin(settings.passphrase), std::cend(settings.passphrase), passphrase.begin(), toascii);
draw("SSID:"_i18n, " %.*s", settings.ssid_len, settings.ssid);
draw("Passphrase:"_i18n, " %s", passphrase.c_str());
}
}
}
#undef draw
switch (m_state) {
case State::None:
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Waiting for connection..."_i18n.c_str());
break;
case State::Connected:
break;
case State::Progress:
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Transferring data..."_i18n.c_str());
break;
case State::Done:
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Press B to exit..."_i18n.c_str());
break;
case State::Failed:
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Failed to install via FTP, press B to exit..."_i18n.c_str());
break;
}
}
void Menu::OnFocusGained() {
MenuBase::OnFocusGained();
}
} // namespace sphaira::ui::menu::ftp

View File

@@ -0,0 +1,499 @@
#include "ui/menus/gc_menu.hpp"
#include "yati/yati.hpp"
#include "yati/nx/nca.hpp"
#include "app.hpp"
#include "defines.hpp"
#include "log.hpp"
#include "ui/nvg_util.hpp"
#include "i18n.hpp"
#include <cstring>
namespace sphaira::ui::menu::gc {
namespace {
const char *g_option_list[] = {
"Nand Install",
"SD Card Install",
"Exit",
};
struct HashStr {
char str[0x21];
};
HashStr hexIdToStr(auto id) {
HashStr str{};
const auto id_lower = std::byteswap(*(u64*)id.c);
const auto id_upper = std::byteswap(*(u64*)(id.c + 0x8));
std::snprintf(str.str, 0x21, "%016lx%016lx", id_lower, id_upper);
return str;
}
// @Gc is the mount point, S is for secure partion, the remaining is the
// the gamecard handle value in lower-case hex.
auto BuildGcPath(const char* name, const FsGameCardHandle* handle, FsGameCardPartition partiton = FsGameCardPartition_Secure) -> fs::FsPath {
static const char mount_parition[] = {
[FsGameCardPartition_Update] = 'U',
[FsGameCardPartition_Normal] = 'N',
[FsGameCardPartition_Secure] = 'S',
[FsGameCardPartition_Logo] = 'L',
};
fs::FsPath path;
std::snprintf(path, sizeof(path), "@Gc%c%08x://%s", mount_parition[partiton], handle->value, name);
return path;
}
auto InRange(u64 off, u64 offset, u64 size) -> bool {
return off < offset + size && off >= offset;
}
struct GcSource final : yati::source::Base {
GcSource(const ApplicationEntry& entry, fs::FsNativeGameCard* fs, bool sd_install);
~GcSource();
Result Read(void* buf, s64 off, s64 size, u64* bytes_read);
yati::container::Collections m_collections{};
yati::ConfigOverride m_config{};
fs::FsNativeGameCard* m_fs{};
FsFile m_file{};
s64 m_offset{};
s64 m_size{};
};
GcSource::GcSource(const ApplicationEntry& entry, fs::FsNativeGameCard* fs, bool sd_install)
: m_fs{fs} {
m_offset = -1;
s64 offset{};
const auto add_collections = [&](const auto& collections) {
for (auto collection : collections) {
collection.offset = offset;
m_collections.emplace_back(collection);
offset += collection.size;
}
};
const auto add_entries = [&](const auto& entries) {
for (auto& e : entries) {
add_collections(e);
}
};
// yati can handle all of this for use, however, yati lacks information
// for ncas until it installs the cnmt and parses it.
// as we already have this info, we can only send yati what we want to install.
if (App::GetApp()->m_ticket_only.Get()) {
add_collections(entry.tickets);
} else {
if (!App::GetApp()->m_skip_base.Get()) {
add_entries(entry.application);
}
if (!App::GetApp()->m_skip_patch.Get()) {
add_entries(entry.patch);
}
if (!App::GetApp()->m_skip_addon.Get()) {
add_entries(entry.add_on);
}
if (!App::GetApp()->m_skip_data_patch.Get()) {
add_entries(entry.data_patch);
}
if (!App::GetApp()->m_skip_ticket.Get()) {
add_collections(entry.tickets);
}
}
// we don't need to verify the nca's, this speeds up installs.
m_config.sd_card_install = sd_install;
m_config.skip_nca_hash_verify = true;
m_config.skip_rsa_header_fixed_key_verify = true;
m_config.skip_rsa_npdm_fixed_key_verify = true;
}
GcSource::~GcSource() {
fsFileClose(&m_file);
}
Result GcSource::Read(void* buf, s64 off, s64 size, u64* bytes_read) {
// check is we need to open a new file.
if (!InRange(off, m_offset, m_size)) {
fsFileClose(&m_file);
m_file = {};
// find new file based on the offset.
bool found = false;
for (auto& collection : m_collections) {
if (InRange(off, collection.offset, collection.size)) {
found = true;
m_offset = collection.offset;
m_size = collection.size;
R_TRY(m_fs->OpenFile(fs::AppendPath("/", collection.name), FsOpenMode_Read, &m_file));
break;
}
}
// this will never fail, unless i break something in yati.
R_UNLESS(found, 0x1);
}
return fsFileRead(&m_file, off - m_offset, buf, size, 0, bytes_read);
}
} // namespace
auto ApplicationEntry::GetSize(const std::vector<GcCollections>& entries) const -> s64 {
s64 size{};
for (auto& e : entries) {
for (auto& collection : e) {
size += collection.size;
}
}
return size;
}
auto ApplicationEntry::GetSize() const -> s64 {
s64 size{};
size += GetSize(application);
size += GetSize(patch);
size += GetSize(add_on);
size += GetSize(data_patch);
return size;
}
Menu::Menu() : MenuBase{"GameCard"_i18n} {
this->SetActions(
std::make_pair(Button::B, Action{"Back"_i18n, [this](){
SetPop();
}}),
std::make_pair(Button::X, Action{"Options"_i18n, [this](){
App::DisplayInstallOptions(false);
}})
);
const Vec4 v{485, 275, 720, 70};
const Vec2 pad{0, 125 - v.h};
m_list = std::make_unique<List>(1, 3, m_pos, v, pad);
fsOpenDeviceOperator(std::addressof(m_dev_op));
UpdateStorageSize();
}
Menu::~Menu() {
GcUnmount();
fsDeviceOperatorClose(std::addressof(m_dev_op));
}
void Menu::Update(Controller* controller, TouchInfo* touch) {
// poll for the gamecard first before handling inputs as the gamecard
// may have been removed, thus pressing A would fail.
bool inserted{};
GcPoll(&inserted);
if (m_mounted != inserted) {
log_write("gc state changed\n");
m_mounted = inserted;
if (m_mounted) {
log_write("trying to mount\n");
m_mounted = R_SUCCEEDED(GcMount());
} else {
log_write("trying to unmount\n");
GcUnmount();
}
}
MenuBase::Update(controller, touch);
m_list->OnUpdate(controller, touch, m_option_index, std::size(g_option_list), [this](bool touch, auto i) {
if (touch && m_option_index == i) {
FireAction(Button::A);
} else {
App::PlaySoundEffect(SoundEffect_Focus);
m_option_index = i;
}
});
}
void Menu::Draw(NVGcontext* vg, Theme* theme) {
MenuBase::Draw(vg, theme);
#define STORAGE_BAR_W 325
#define STORAGE_BAR_H 14
const auto size_sd_gb = (double)m_size_free_sd / 0x40000000;
const auto size_nand_gb = (double)m_size_free_nand / 0x40000000;
gfx::drawTextArgs(vg, 490, 135, 23.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "System memory %.1f GB", size_nand_gb);
gfx::drawRect(vg, 480, 170, STORAGE_BAR_W, STORAGE_BAR_H, theme->GetColour(ThemeEntryID_TEXT));
gfx::drawRect(vg, 480 + 1, 170 + 1, STORAGE_BAR_W - 2, STORAGE_BAR_H - 2, theme->GetColour(ThemeEntryID_BACKGROUND));
gfx::drawRect(vg, 480 + 2, 170 + 2, STORAGE_BAR_W - (((double)m_size_free_nand / (double)m_size_total_nand) * STORAGE_BAR_W) - 4, STORAGE_BAR_H - 4, theme->GetColour(ThemeEntryID_TEXT));
gfx::drawTextArgs(vg, 870, 135, 23.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "microSD card %.1f GB", size_sd_gb);
gfx::drawRect(vg, 860, 170, STORAGE_BAR_W, STORAGE_BAR_H, theme->GetColour(ThemeEntryID_TEXT));
gfx::drawRect(vg, 860 + 1, 170 + 1, STORAGE_BAR_W - 2, STORAGE_BAR_H - 2, theme->GetColour(ThemeEntryID_BACKGROUND));
gfx::drawRect(vg, 860 + 2, 170 + 2, STORAGE_BAR_W - (((double)m_size_free_sd / (double)m_size_total_sd) * STORAGE_BAR_W) - 4, STORAGE_BAR_H - 4, theme->GetColour(ThemeEntryID_TEXT));
gfx::drawRect(vg, 30, 90, 375, 555, theme->GetColour(ThemeEntryID_GRID));
if (!m_entries.empty()) {
const auto& e = m_entries[m_entry_index];
const auto size = e.GetSize();
gfx::drawImage(vg, 90, 130, 256, 256, m_icon ? m_icon : App::GetDefaultImage());
nvgSave(vg);
nvgIntersectScissor(vg, 50, 90, 325, 555);
gfx::drawTextArgs(vg, 50, 415, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%s", m_lang_entry.name);
gfx::drawTextArgs(vg, 50, 455, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%s", m_lang_entry.author);
gfx::drawTextArgs(vg, 50, 495, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "App-ID: 0%lX", e.app_id);
gfx::drawTextArgs(vg, 50, 535, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Key-Gen: %u (%s)", e.key_gen, nca::GetKeyGenStr(e.key_gen));
gfx::drawTextArgs(vg, 50, 575, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Size: %.2f GB", (double)size / 0x40000000);
gfx::drawTextArgs(vg, 50, 615, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Base: %zu Patch: %zu Addon: %zu Data: %zu", e.application.size(), e.patch.size(), e.add_on.size(), e.data_patch.size());
nvgRestore(vg);
}
m_list->Draw(vg, theme, std::size(g_option_list), [this](auto* vg, auto* theme, auto v, auto i) {
const auto& [x, y, w, h] = v;
const auto text_y = y + (h / 2.f);
auto colour = ThemeEntryID_TEXT;
if (i == m_option_index) {
gfx::drawRectOutline(vg, theme, 4.f, v);
// g_background.selected_bar = create_shape(Colour_Nintendo_Cyan, 90, 230, 4, 45, true);
// draw_shape_position(&g_background.selected_bar, 485, g_options[i].text->rect.y - 10);
gfx::drawRect(vg, 490, text_y - 45.f / 2.f, 2, 45, theme->GetColour(ThemeEntryID_TEXT_SELECTED));
colour = ThemeEntryID_TEXT_SELECTED;
}
if (i != 2 && !m_mounted) {
colour = ThemeEntryID_TEXT_INFO;
}
gfx::drawTextArgs(vg, x + 15, y + (h / 2.f), 23.f, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(colour), "%s", g_option_list[i]);
});
}
Result Menu::GcMount() {
GcUnmount();
R_TRY(fsDeviceOperatorGetGameCardHandle(std::addressof(m_dev_op), std::addressof(m_handle)));
m_fs = std::make_unique<fs::FsNativeGameCard>(std::addressof(m_handle), FsGameCardPartition_Secure, false);
R_TRY(m_fs->GetFsOpenResult());
FsDir dir;
R_TRY(m_fs->OpenDirectory("/", FsDirOpenMode_ReadFiles, std::addressof(dir)));
ON_SCOPE_EXIT(fsDirClose(std::addressof(dir)));
s64 count;
R_TRY(m_fs->DirGetEntryCount(std::addressof(dir), std::addressof(count)));
std::vector<FsDirectoryEntry> buf(count);
s64 total_entries;
R_TRY(m_fs->DirRead(std::addressof(dir), std::addressof(total_entries), buf.size(), buf.data()));
R_UNLESS(buf.size() == total_entries, 0x1);
yati::container::Collections ticket_collections;
for (const auto& e : buf) {
if (!std::string_view(e.name).ends_with(".tik") && !std::string_view(e.name).ends_with(".cert")) {
continue;
}
ticket_collections.emplace_back(e.name, 0, e.file_size);
}
for (const auto& e : buf) {
// we could use ncm to handle finding all the ncas for us
// however, we can parse faster than ncm.
// not only that, the first few calls trying to mount ncm db for
// the gamecard will fail as it has not yet been parsed (or it's locked?).
// we could, of course, just wait until ncm is ready, which is about
// 32ms, but i already have code for manually parsing cnmt so lets re-use it.
if (!std::string_view(e.name).ends_with(".cnmt.nca")) {
continue;
}
// we don't yet use the header or extended header.
ncm::PackagedContentMeta header;
std::vector<u8> extended_header;
std::vector<NcmPackagedContentInfo> infos;
const auto path = BuildGcPath(e.name, &m_handle);
R_TRY(yati::ParseCnmtNca(path, header, extended_header, infos));
u8 key_gen;
FsRightsId rights_id;
R_TRY(fsGetRightsIdAndKeyGenerationByPath(path, FsContentAttributes_All, &key_gen, &rights_id));
// always add tickets, yati will ignore them if not needed.
GcCollections collections;
// add cnmt file.
collections.emplace_back(e.name, e.file_size, NcmContentType_Meta);
for (const auto& info : infos) {
// these don't exist for gamecards, however i may copy/paste this code
// somewhere so i'm future proofing against myself.
if (info.info.content_type == NcmContentType_DeltaFragment) {
continue;
}
// find the nca file, this will never fail for gamecards, see above comment.
const auto str = hexIdToStr(info.info.content_id);
const auto it = std::find_if(buf.cbegin(), buf.cend(), [str](auto& e){
return !std::strncmp(str.str, e.name, std::strlen(str.str));
});
R_UNLESS(it != buf.cend(), yati::Result_NcaNotFound);
collections.emplace_back(it->name, it->file_size, info.info.content_type);
}
const auto app_id = ncm::GetAppId(header);
ApplicationEntry* app_entry{};
for (auto& app : m_entries) {
if (app.app_id == app_id) {
app_entry = &app;
break;
}
}
if (!app_entry) {
app_entry = &m_entries.emplace_back(app_id, header.title_version);
}
app_entry->version = std::max(app_entry->version, header.title_version);
app_entry->key_gen = std::max(app_entry->key_gen, key_gen);
if (header.meta_type == NcmContentMetaType_Application) {
app_entry->application.emplace_back(collections);
} else if (header.meta_type == NcmContentMetaType_Patch) {
app_entry->patch.emplace_back(collections);
} else if (header.meta_type == NcmContentMetaType_AddOnContent) {
app_entry->add_on.emplace_back(collections);
} else if (header.meta_type == NcmContentMetaType_DataPatch) {
app_entry->data_patch.emplace_back(collections);
}
}
R_UNLESS(m_entries.size(), 0x1);
// append tickets to every application, yati will ignore if undeeded.
for (auto& e : m_entries) {
e.tickets = ticket_collections;
}
SetAction(Button::A, Action{"OK"_i18n, [this](){
if (m_option_index == 2) {
SetPop();
} else {
if (m_mounted) {
App::Push(std::make_shared<ui::ProgressBox>(m_icon, "Installing "_i18n, m_lang_entry.name, [this](auto pbox) mutable -> bool {
auto source = std::make_shared<GcSource>(m_entries[m_entry_index], m_fs.get(), m_option_index == 1);
return R_SUCCEEDED(yati::InstallFromCollections(pbox, source, source->m_collections, source->m_config));
}, [this](bool result){
if (result) {
App::Notify("Gc install success!"_i18n);
} else {
App::Notify("Gc install failed!"_i18n);
}
UpdateStorageSize();
}));
}
}
}});
if (m_entries.size() > 1) {
SetAction(Button::L, Action{"Prev"_i18n, [this](){
if (m_entry_index != 0) {
OnChangeIndex(m_entry_index - 1);
}
}});
SetAction(Button::R, Action{"Next"_i18n, [this](){
if (m_entry_index < m_entries.size()) {
OnChangeIndex(m_entry_index + 1);
}
}});
}
OnChangeIndex(0);
R_SUCCEED();
}
void Menu::GcUnmount() {
m_fs.reset();
m_entries.clear();
m_entry_index = 0;
m_mounted = false;
m_lang_entry = {};
FreeImage();
RemoveAction(Button::L);
RemoveAction(Button::R);
}
Result Menu::GcPoll(bool* inserted) {
R_TRY(fsDeviceOperatorIsGameCardInserted(&m_dev_op, inserted));
// if the handle changed, re-mount the game card.
if (*inserted && m_mounted) {
FsGameCardHandle handle;
R_TRY(fsDeviceOperatorGetGameCardHandle(std::addressof(m_dev_op), std::addressof(handle)));
if (handle.value != m_handle.value) {
R_TRY(GcMount());
}
}
R_SUCCEED();
}
Result Menu::UpdateStorageSize() {
fs::FsNativeContentStorage fs_nand{FsContentStorageId_User};
fs::FsNativeContentStorage fs_sd{FsContentStorageId_SdCard};
R_TRY(fs_sd.GetFreeSpace("/", &m_size_free_sd));
R_TRY(fs_sd.GetTotalSpace("/", &m_size_total_sd));
R_TRY(fs_nand.GetFreeSpace("/", &m_size_free_nand));
R_TRY(fs_nand.GetTotalSpace("/", &m_size_total_nand));
R_SUCCEED();
}
void Menu::FreeImage() {
if (m_icon) {
nvgDeleteImage(App::GetVg(), m_icon);
m_icon = 0;
}
}
void Menu::OnChangeIndex(s64 new_index) {
FreeImage();
m_entry_index = new_index;
const auto index = m_entries.empty() ? 0 : m_entry_index + 1;
this->SetSubHeading(std::to_string(index) + " / " + std::to_string(m_entries.size()));
// nsGetApplicationControlData() will fail if it's the first time
// mounting a gamecard if the image is not already cached.
// waiting 1-2s after mount, then calling seems to work.
// however, we can just manually parse the nca to get the data we need,
// which always works and *is* faster too ;)
for (auto& e : m_entries[m_entry_index].application) {
for (auto& collection : e) {
if (collection.type == NcmContentType_Control) {
NacpStruct nacp;
std::vector<u8> icon;
const auto path = BuildGcPath(collection.name.c_str(), &m_handle);
if (R_SUCCEEDED(yati::ParseControlNca(path, m_entries[m_entry_index].app_id, &nacp, sizeof(nacp), &icon))) {
log_write("managed to parse control nca %s\n", path.s);
NacpLanguageEntry* lang_entry{};
nacpGetLanguageEntry(&nacp, &lang_entry);
if (lang_entry) {
m_lang_entry = *lang_entry;
}
m_icon = nvgCreateImageMem(App::GetVg(), 0, icon.data(), icon.size());
if (m_icon > 0) {
return;
}
} else {
log_write("\tFAILED to parse control nca %s\n", path.s);
}
}
}
}
}
} // namespace sphaira::ui::menu::gc

View File

@@ -247,27 +247,6 @@ Menu::Menu() : MenuBase{"GitHub"_i18n} {
fs::FsNativeSd().CreateDirectoryRecursively(CACHE_PATH);
this->SetActions(
std::make_pair(Button::DOWN, Action{[this](){
if (m_list->ScrollDown(m_index, 1, m_entries.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::UP, Action{[this](){
if (m_list->ScrollUp(m_index, 1, m_entries.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::DPAD_RIGHT, Action{[this](){
if (m_list->ScrollDown(m_index, 8, m_entries.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::DPAD_LEFT, Action{[this](){
if (m_list->ScrollUp(m_index, 8, m_entries.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::A, Action{"Download"_i18n, [this](){
if (m_entries.empty()) {
return;
@@ -277,7 +256,7 @@ Menu::Menu() : MenuBase{"GitHub"_i18n} {
static GhApiEntry gh_entry;
gh_entry = {};
App::Push(std::make_shared<ProgressBox>("Downloading "_i18n + GetEntry().repo, [this](auto pbox){
App::Push(std::make_shared<ProgressBox>(0, "Downloading "_i18n, GetEntry().repo, [this](auto pbox){
return DownloadAssetJson(pbox, GenerateApiUrl(GetEntry()), gh_entry);
}, [this](bool success){
if (success) {
@@ -325,7 +304,7 @@ Menu::Menu() : MenuBase{"GitHub"_i18n} {
}
const auto func = [this, &asset_entry, ptr](){
App::Push(std::make_shared<ProgressBox>("Downloading "_i18n + GetEntry().repo, [this, &asset_entry, ptr](auto pbox){
App::Push(std::make_shared<ProgressBox>(0, "Downloading "_i18n, GetEntry().repo, [this, &asset_entry, ptr](auto pbox){
return DownloadApp(pbox, asset_entry, ptr);
}, [this, ptr](bool success){
if (success) {
@@ -373,8 +352,8 @@ Menu::~Menu() {
void Menu::Update(Controller* controller, TouchInfo* touch) {
MenuBase::Update(controller, touch);
m_list->OnUpdate(controller, touch, m_entries.size(), [this](auto i) {
if (m_index == i) {
m_list->OnUpdate(controller, touch, m_index, m_entries.size(), [this](bool touch, auto i) {
if (touch && m_index == i) {
FireAction(Button::A);
} else {
App::PlaySoundEffect(SoundEffect_Focus);

View File

@@ -28,40 +28,6 @@ auto GenerateStarPath(const fs::FsPath& nro_path) -> fs::FsPath {
Menu::Menu() : MenuBase{"Homebrew"_i18n} {
this->SetActions(
std::make_pair(Button::RIGHT, Action{[this](){
if (m_index < (m_entries.size() - 1) && (m_index + 1) % 3 != 0) {
SetIndex(m_index + 1);
App::PlaySoundEffect(SoundEffect_Scroll);
log_write("moved right\n");
}
}}),
std::make_pair(Button::LEFT, Action{[this](){
if (m_index != 0 && (m_index % 3) != 0) {
SetIndex(m_index - 1);
App::PlaySoundEffect(SoundEffect_Scroll);
log_write("moved left\n");
}
}}),
std::make_pair(Button::DOWN, Action{[this](){
if (m_list->ScrollDown(m_index, 3, m_entries.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::UP, Action{[this](){
if (m_list->ScrollUp(m_index, 3, m_entries.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::R2, Action{[this](){
if (m_list->ScrollDown(m_index, 9, m_entries.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::L2, Action{[this](){
if (m_list->ScrollUp(m_index, 9, m_entries.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::A, Action{"Launch"_i18n, [this](){
nro_launch(m_entries[m_index].path);
}}),
@@ -157,8 +123,8 @@ Menu::~Menu() {
void Menu::Update(Controller* controller, TouchInfo* touch) {
MenuBase::Update(controller, touch);
m_list->OnUpdate(controller, touch, m_entries.size(), [this](auto i) {
if (m_index == i) {
m_list->OnUpdate(controller, touch, m_index, m_entries.size(), [this](bool touch, auto i) {
if (touch && m_index == i) {
FireAction(Button::A);
} else {
App::PlaySoundEffect(SoundEffect_Focus);
@@ -202,7 +168,7 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
}
const float image_size = 115;
gfx::drawImageRounded(vg, x + 20, y + 20, image_size, image_size, e.image ? e.image : App::GetDefaultImage());
gfx::drawImage(vg, x + 20, y + 20, image_size, image_size, e.image ? e.image : App::GetDefaultImage(), 15);
nvgSave(vg);
nvgIntersectScissor(vg, x, y, w - 30.f, h); // clip

View File

@@ -1,8 +1,4 @@
#include "ui/menus/main_menu.hpp"
#include "ui/menus/irs_menu.hpp"
#include "ui/menus/themezer.hpp"
#include "ui/menus/ghdl.hpp"
#include "ui/menus/usb_menu.hpp"
#include "ui/sidebar.hpp"
#include "ui/popup_list.hpp"
@@ -14,7 +10,6 @@
#include "log.hpp"
#include "download.hpp"
#include "defines.hpp"
#include "web.hpp"
#include "i18n.hpp"
#include <cstring>
@@ -233,48 +228,29 @@ MainMenu::MainMenu() {
language_items.push_back("Swedish"_i18n);
language_items.push_back("Vietnamese"_i18n);
options->Add(std::make_shared<SidebarEntryCallback>("Theme"_i18n, [this](){
SidebarEntryArray::Items theme_items{};
const auto theme_meta = App::GetThemeMetaList();
for (auto& p : theme_meta) {
theme_items.emplace_back(p.name);
}
auto options = std::make_shared<Sidebar>("Theme Options"_i18n, Sidebar::Side::LEFT);
ON_SCOPE_EXIT(App::Push(options));
options->Add(std::make_shared<SidebarEntryArray>("Select Theme"_i18n, theme_items, [this, theme_items](s64& index_out){
App::SetTheme(index_out);
}, App::GetThemeIndex()));
options->Add(std::make_shared<SidebarEntryBool>("Music"_i18n, App::GetThemeMusicEnable(), [this](bool& enable){
App::SetThemeMusicEnable(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("12 Hour Time"_i18n, App::Get12HourTimeEnable(), [this](bool& enable){
App::Set12HourTimeEnable(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryCallback>("Theme"_i18n, [](){
App::DisplayThemeOptions();
}));
options->Add(std::make_shared<SidebarEntryCallback>("Network"_i18n, [this](){
auto options = std::make_shared<Sidebar>("Network Options"_i18n, Sidebar::Side::LEFT);
ON_SCOPE_EXIT(App::Push(options));
options->Add(std::make_shared<SidebarEntryBool>("Ftp"_i18n, App::GetFtpEnable(), [this](bool& enable){
options->Add(std::make_shared<SidebarEntryBool>("Ftp"_i18n, App::GetFtpEnable(), [](bool& enable){
App::SetFtpEnable(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("Mtp"_i18n, App::GetMtpEnable(), [this](bool& enable){
options->Add(std::make_shared<SidebarEntryBool>("Mtp"_i18n, App::GetMtpEnable(), [](bool& enable){
App::SetMtpEnable(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("Nxlink"_i18n, App::GetNxlinkEnable(), [this](bool& enable){
options->Add(std::make_shared<SidebarEntryBool>("Nxlink"_i18n, App::GetNxlinkEnable(), [](bool& enable){
App::SetNxlinkEnable(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
if (m_update_state == UpdateState::Update) {
options->Add(std::make_shared<SidebarEntryCallback>("Download update: "_i18n + m_update_version, [this](){
App::Push(std::make_shared<ProgressBox>("Downloading "_i18n + m_update_version, [this](auto pbox){
App::Push(std::make_shared<ProgressBox>(0, "Downloading "_i18n, "Sphaira v" + m_update_version, [this](auto pbox){
return InstallUpdate(pbox, m_update_url, m_update_version);
}, [this](bool success){
if (success) {
@@ -293,144 +269,16 @@ MainMenu::MainMenu() {
}
}));
options->Add(std::make_shared<SidebarEntryArray>("Language"_i18n, language_items, [this](s64& index_out){
options->Add(std::make_shared<SidebarEntryArray>("Language"_i18n, language_items, [](s64& index_out){
App::SetLanguage(index_out);
}, (s64)App::GetLanguage()));
options->Add(std::make_shared<SidebarEntryCallback>("Misc"_i18n, [this](){
auto options = std::make_shared<Sidebar>("Misc Options"_i18n, Sidebar::Side::LEFT);
ON_SCOPE_EXIT(App::Push(options));
options->Add(std::make_shared<SidebarEntryCallback>("Themezer"_i18n, [](){
App::Push(std::make_shared<menu::themezer::Menu>());
}));
options->Add(std::make_shared<SidebarEntryCallback>("GitHub"_i18n, [](){
App::Push(std::make_shared<menu::gh::Menu>());
}));
options->Add(std::make_shared<SidebarEntryCallback>("Irs"_i18n, [](){
App::Push(std::make_shared<menu::irs::Menu>());
}));
if (App::IsApplication()) {
options->Add(std::make_shared<SidebarEntryCallback>("Web"_i18n, [](){
WebShow("https://lite.duckduckgo.com/lite");
}));
}
if (App::GetApp()->m_install.Get()) {
options->Add(std::make_shared<SidebarEntryCallback>("Usb Install"_i18n, [](){
App::Push(std::make_shared<menu::usb::Menu>());
}));
}
options->Add(std::make_shared<SidebarEntryCallback>("Misc"_i18n, [](){
App::DisplayMiscOptions();
}));
options->Add(std::make_shared<SidebarEntryCallback>("Advanced"_i18n, [this](){
auto options = std::make_shared<Sidebar>("Advanced Options"_i18n, Sidebar::Side::LEFT);
ON_SCOPE_EXIT(App::Push(options));
SidebarEntryArray::Items text_scroll_speed_items;
text_scroll_speed_items.push_back("Slow"_i18n);
text_scroll_speed_items.push_back("Normal"_i18n);
text_scroll_speed_items.push_back("Fast"_i18n);
options->Add(std::make_shared<SidebarEntryBool>("Logging"_i18n, App::GetLogEnable(), [this](bool& enable){
App::SetLogEnable(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("Replace hbmenu on exit"_i18n, App::GetReplaceHbmenuEnable(), [this](bool& enable){
App::SetReplaceHbmenuEnable(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryArray>("Text scroll speed"_i18n, text_scroll_speed_items, [this](s64& index_out){
App::SetTextScrollSpeed(index_out);
}, (s64)App::GetTextScrollSpeed()));
options->Add(std::make_shared<SidebarEntryCallback>("Install options"_i18n, [this](){
auto options = std::make_shared<Sidebar>("Install Options"_i18n, Sidebar::Side::LEFT);
ON_SCOPE_EXIT(App::Push(options));
SidebarEntryArray::Items install_items;
install_items.push_back("System memory"_i18n);
install_items.push_back("microSD card"_i18n);
options->Add(std::make_shared<SidebarEntryBool>("Enable"_i18n, App::GetInstallEnable(), [this](bool& enable){
App::SetInstallEnable(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("Show install warning"_i18n, App::GetInstallPrompt(), [this](bool& enable){
App::SetInstallPrompt(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryArray>("Install location"_i18n, install_items, [this](s64& index_out){
App::SetInstallSdEnable(index_out);
}, (s64)App::GetInstallSdEnable()));
options->Add(std::make_shared<SidebarEntryBool>("Allow downgrade"_i18n, App::GetApp()->m_allow_downgrade.Get(), [this](bool& enable){
App::GetApp()->m_allow_downgrade.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("Skip if already installed"_i18n, App::GetApp()->m_skip_if_already_installed.Get(), [this](bool& enable){
App::GetApp()->m_skip_if_already_installed.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("Ticket only"_i18n, App::GetApp()->m_ticket_only.Get(), [this](bool& enable){
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));
options->Add(std::make_shared<SidebarEntryBool>("Skip Patch"_i18n, App::GetApp()->m_skip_patch.Get(), [this](bool& enable){
App::GetApp()->m_skip_patch.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("Skip addon"_i18n, App::GetApp()->m_skip_addon.Get(), [this](bool& enable){
App::GetApp()->m_skip_addon.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("Skip data patch"_i18n, App::GetApp()->m_skip_data_patch.Get(), [this](bool& enable){
App::GetApp()->m_skip_data_patch.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("Skip ticket"_i18n, App::GetApp()->m_skip_ticket.Get(), [this](bool& enable){
App::GetApp()->m_skip_ticket.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("skip NCA hash verify"_i18n, App::GetApp()->m_skip_nca_hash_verify.Get(), [this](bool& enable){
App::GetApp()->m_skip_nca_hash_verify.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("Skip RSA header verify"_i18n, App::GetApp()->m_skip_rsa_header_fixed_key_verify.Get(), [this](bool& enable){
App::GetApp()->m_skip_rsa_header_fixed_key_verify.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("Skip RSA NPDM verify"_i18n, App::GetApp()->m_skip_rsa_npdm_fixed_key_verify.Get(), [this](bool& enable){
App::GetApp()->m_skip_rsa_npdm_fixed_key_verify.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("Ignore distribution bit"_i18n, App::GetApp()->m_ignore_distribution_bit.Get(), [this](bool& enable){
App::GetApp()->m_ignore_distribution_bit.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("Convert to standard crypto"_i18n, App::GetApp()->m_convert_to_standard_crypto.Get(), [this](bool& enable){
App::GetApp()->m_convert_to_standard_crypto.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("Lower master key"_i18n, App::GetApp()->m_lower_master_key.Get(), [this](bool& enable){
App::GetApp()->m_lower_master_key.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("Lower system version"_i18n, App::GetApp()->m_lower_system_version.Get(), [this](bool& enable){
App::GetApp()->m_lower_system_version.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
}));
options->Add(std::make_shared<SidebarEntryCallback>("Advanced"_i18n, [](){
App::DisplayAdvancedOptions();
}));
}})
);

View File

@@ -18,17 +18,18 @@ MenuBase::~MenuBase() {
void MenuBase::Update(Controller* controller, TouchInfo* touch) {
Widget::Update(controller, touch);
// update every second.
if (m_poll_timestamp.GetSeconds() >= 1) {
UpdateVars();
}
}
void MenuBase::Draw(NVGcontext* vg, Theme* theme) {
DrawElement(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, ThemeEntryID_BACKGROUND);
Widget::Draw(vg, theme);
// update every second, do this in Draw because Update() isn't called if it
// doesn't have focus.
if (m_poll_timestamp.GetSeconds() >= 1) {
UpdateVars();
}
const float start_y = 70;
const float font_size = 22;
const float spacing = 30;

View File

@@ -375,45 +375,6 @@ Menu::Menu() : MenuBase{"Themezer"_i18n} {
}});
this->SetActions(
std::make_pair(Button::RIGHT, Action{[this](){
const auto& page = m_pages[m_page_index];
if (m_index < (page.m_packList.size() - 1) && (m_index + 1) % 3 != 0) {
SetIndex(m_index + 1);
App::PlaySoundEffect(SoundEffect_Scroll);
log_write("moved right\n");
}
}}),
std::make_pair(Button::LEFT, Action{[this](){
if (m_index != 0 && (m_index % 3) != 0) {
SetIndex(m_index - 1);
App::PlaySoundEffect(SoundEffect_Scroll);
log_write("moved left\n");
}
}}),
std::make_pair(Button::DOWN, Action{[this](){
const auto& page = m_pages[m_page_index];
if (m_list->ScrollDown(m_index, 3, page.m_packList.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::UP, Action{[this](){
const auto& page = m_pages[m_page_index];
if (m_list->ScrollUp(m_index, 3, page.m_packList.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::R2, Action{[this](){
const auto& page = m_pages[m_page_index];
if (m_list->ScrollDown(m_index, 6, page.m_packList.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::L2, Action{[this](){
const auto& page = m_pages[m_page_index];
if (m_list->ScrollUp(m_index, 6, page.m_packList.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::A, Action{"Download"_i18n, [this](){
App::Push(std::make_shared<OptionBox>(
"Download theme?"_i18n,
@@ -424,7 +385,7 @@ Menu::Menu() : MenuBase{"Themezer"_i18n} {
const auto& entry = page.m_packList[m_index];
const auto url = apiBuildUrlDownloadPack(entry);
App::Push(std::make_shared<ProgressBox>("Installing "_i18n + entry.details.name, [this, &entry](auto pbox){
App::Push(std::make_shared<ProgressBox>(entry.themes[0].preview.lazy_image.image, "Downloading "_i18n, entry.details.name, [this, &entry](auto pbox){
return InstallTheme(pbox, entry);
}, [this, &entry](bool success){
if (success) {
@@ -532,8 +493,8 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
return;
}
m_list->OnUpdate(controller, touch, page.m_packList.size(), [this](auto i) {
if (m_index == i) {
m_list->OnUpdate(controller, touch, m_index, page.m_packList.size(), [this](bool touch, auto i) {
if (touch && m_index == i) {
FireAction(Button::A);
} else {
App::PlaySoundEffect(SoundEffect_Focus);
@@ -642,7 +603,7 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
}
}
gfx::drawImageRounded(vg, x + xoff, y, 320, 180, image.image ? image.image : App::GetDefaultImage());
gfx::drawImage(vg, x + xoff, y, 320, 180, image.image ? image.image : App::GetDefaultImage(), 15);
}
nvgSave(vg);

View File

@@ -43,6 +43,10 @@ Menu::Menu() : MenuBase{"USB"_i18n} {
SetPop();
}});
SetAction(Button::X, Action{"Options"_i18n, [this](){
App::DisplayInstallOptions(false);
}});
// if mtp is enabled, disable it for now.
m_was_mtp_enabled = App::GetMtpEnable();
if (m_was_mtp_enabled) {
@@ -99,10 +103,18 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
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 {
App::Push(std::make_shared<ui::ProgressBox>(0, "Installing "_i18n, "", [this](auto pbox) mutable -> bool {
log_write("inside progress box\n");
for (u32 i = 0; i < m_usb_count; i++) {
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 +127,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 +165,11 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
break;
case State::Done:
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Press B to Exit..."_i18n.c_str());
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Press B to exit..."_i18n.c_str());
break;
case State::Failed:
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Failed to init usb..."_i18n.c_str());
this->SetPop();
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Failed to init usb, press B to exit..."_i18n.c_str());
break;
}
}

View File

@@ -34,24 +34,16 @@ constexpr std::array buttons = {
};
// NEW ---------------------
void drawRectIntenal(NVGcontext* vg, const Vec4& v, const NVGcolor& c, bool rounded) {
void drawRectIntenal(NVGcontext* vg, const Vec4& v, const NVGcolor& c, float rounded) {
nvgBeginPath(vg);
if (rounded) {
nvgRoundedRect(vg, v.x, v.y, v.w, v.h, 15);
} else {
nvgRect(vg, v.x, v.y, v.w, v.h);
}
nvgRoundedRect(vg, v.x, v.y, v.w, v.h, rounded);
nvgFillColor(vg, c);
nvgFill(vg);
}
void drawRectIntenal(NVGcontext* vg, const Vec4& v, const NVGpaint& p, bool rounded) {
void drawRectIntenal(NVGcontext* vg, const Vec4& v, const NVGpaint& p, float rounded) {
nvgBeginPath(vg);
if (rounded) {
nvgRoundedRect(vg, v.x, v.y, v.w, v.h, 15);
} else {
nvgRect(vg, v.x, v.y, v.w, v.h);
}
nvgRoundedRect(vg, v.x, v.y, v.w, v.h, rounded);
nvgFillPaint(vg, p);
nvgFill(vg);
}
@@ -164,25 +156,13 @@ void drawTextArgs(NVGcontext* vg, float x, float y, float size, int align, const
drawText(vg, x, y, size, buffer, nullptr, align, c);
}
void drawImage(NVGcontext* vg, const Vec4& v, int texture) {
void drawImage(NVGcontext* vg, const Vec4& v, int texture, float rounded) {
const auto paint = nvgImagePattern(vg, v.x, v.y, v.w, v.h, 0, texture, 1.f);
drawRect(vg, v, paint, false);
drawRect(vg, v, paint, rounded);
}
void drawImage(NVGcontext* vg, float x, float y, float w, float h, int texture) {
drawImage(vg, Vec4(x, y, w, h), texture);
}
void drawImageRounded(NVGcontext* vg, const Vec4& v, int texture) {
const auto paint = nvgImagePattern(vg, v.x, v.y, v.w, v.h, 0, texture, 1.f);
nvgBeginPath(vg);
nvgRoundedRect(vg, v.x, v.y, v.w, v.h, 15);
nvgFillPaint(vg, paint);
nvgFill(vg);
}
void drawImageRounded(NVGcontext* vg, float x, float y, float w, float h, int texture) {
drawImageRounded(vg, Vec4(x, y, w, h), texture);
void drawImage(NVGcontext* vg, float x, float y, float w, float h, int texture, float rounded) {
drawImage(vg, Vec4(x, y, w, h), texture, rounded);
}
void drawTextBox(NVGcontext* vg, float x, float y, float size, float bound, const NVGcolor& c, const char* str, int align, const char* end) {
@@ -208,19 +188,19 @@ void dimBackground(NVGcontext* vg) {
drawRectIntenal(vg, {0.f,0.f,SCREEN_WIDTH,SCREEN_HEIGHT}, nvgRGBA(0, 0, 0, 180), false);
}
void drawRect(NVGcontext* vg, float x, float y, float w, float h, const NVGcolor& c, bool rounded) {
void drawRect(NVGcontext* vg, float x, float y, float w, float h, const NVGcolor& c, float rounded) {
drawRectIntenal(vg, {x,y,w,h}, c, rounded);
}
void drawRect(NVGcontext* vg, const Vec4& v, const NVGcolor& c, bool rounded) {
void drawRect(NVGcontext* vg, const Vec4& v, const NVGcolor& c, float rounded) {
drawRectIntenal(vg, v, c, rounded);
}
void drawRect(NVGcontext* vg, float x, float y, float w, float h, const NVGpaint& p, bool rounded) {
void drawRect(NVGcontext* vg, float x, float y, float w, float h, const NVGpaint& p, float rounded) {
drawRectIntenal(vg, {x,y,w,h}, p, rounded);
}
void drawRect(NVGcontext* vg, const Vec4& v, const NVGpaint& p, bool rounded) {
void drawRect(NVGcontext* vg, const Vec4& v, const NVGpaint& p, float rounded) {
drawRectIntenal(vg, v, p, rounded);
}

View File

@@ -62,16 +62,6 @@ PopupList::PopupList(std::string title, Items items, Callback cb, s64 index)
, m_callback{cb}
, m_index{index} {
this->SetActions(
std::make_pair(Button::DOWN, Action{[this](){
if (m_list->ScrollDown(m_index, 1, m_items.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::UP, Action{[this](){
if (m_list->ScrollUp(m_index, 1, m_items.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::A, Action{"Select"_i18n, [this](){
if (m_callback) {
m_callback(m_index);
@@ -103,9 +93,11 @@ PopupList::PopupList(std::string title, Items items, Callback cb, s64 index)
auto PopupList::Update(Controller* controller, TouchInfo* touch) -> void {
Widget::Update(controller, touch);
m_list->OnUpdate(controller, touch, m_items.size(), [this](auto i) {
m_list->OnUpdate(controller, touch, m_index, m_items.size(), [this](bool touch, auto i) {
SetIndex(i);
FireAction(Button::A);
if (touch) {
FireAction(Button::A);
}
});
}

View File

@@ -5,6 +5,7 @@
#include "defines.hpp"
#include "log.hpp"
#include "i18n.hpp"
#include <cstring>
namespace sphaira::ui {
namespace {
@@ -17,7 +18,7 @@ void threadFunc(void* arg) {
} // namespace
ProgressBox::ProgressBox(const std::string& title, ProgressBoxCallback callback, ProgressBoxDoneCallback done, int cpuid, int prio, int stack_size) {
ProgressBox::ProgressBox(int image, const std::string& action, const std::string& title, ProgressBoxCallback callback, ProgressBoxDoneCallback done, int cpuid, int prio, int stack_size) {
SetAction(Button::B, Action{"Back"_i18n, [this](){
App::Push(std::make_shared<OptionBox>("Are you sure you wish to cancel?"_i18n, "No"_i18n, "Yes"_i18n, 1, [this](auto op_index){
if (op_index && *op_index) {
@@ -27,11 +28,6 @@ ProgressBox::ProgressBox(const std::string& title, ProgressBoxCallback callback,
}));
}});
m_pos.w = 770.f;
m_pos.h = 430.f;
m_pos.x = 255;
m_pos.y = 145;
m_pos.w = 770.f;
m_pos.h = 295.f;
m_pos.x = (SCREEN_WIDTH / 2.f) - (m_pos.w / 2.f);
@@ -39,6 +35,8 @@ ProgressBox::ProgressBox(const std::string& title, ProgressBoxCallback callback,
m_done = done;
m_title = title;
m_action = action;
m_image = image;
m_thread_data.pbox = this;
m_thread_data.callback = callback;
@@ -60,6 +58,7 @@ ProgressBox::~ProgressBox() {
log_write("failed to close thread\n");
}
FreeImage();
m_done(m_thread_data.result);
}
@@ -73,12 +72,28 @@ auto ProgressBox::Update(Controller* controller, TouchInfo* touch) -> void {
auto ProgressBox::Draw(NVGcontext* vg, Theme* theme) -> void {
mutexLock(&m_mutex);
std::vector<u8> image_data{};
std::swap(m_image_data, image_data);
if (m_timestamp.GetSeconds()) {
m_timestamp.Update();
m_speed = m_offset - m_last_offset;
m_last_offset = m_offset;
}
const auto title = m_title;
const auto transfer = m_transfer;
const auto size = m_size;
const auto offset = m_offset;
const auto speed = m_speed;
const auto last_offset = m_last_offset;
mutexUnlock(&m_mutex);
if (!image_data.empty()) {
FreeImage();
m_image = nvgCreateImageMem(vg, 0, image_data.data(), image_data.size());
m_own_image = true;
}
gfx::dimBackground(vg);
gfx::drawRect(vg, m_pos, theme->GetColour(ThemeEntryID_POPUP));
@@ -86,20 +101,62 @@ auto ProgressBox::Draw(NVGcontext* vg, Theme* theme) -> void {
// const Vec4 box = { 255, 145, 770, 430 };
const auto center_x = m_pos.x + m_pos.w/2;
const auto end_y = m_pos.y + m_pos.h;
const Vec4 prog_bar = { 400, end_y - 80, 480, 12 };
const auto progress_bar_w = m_pos.w - 230;
const Vec4 prog_bar = { center_x - progress_bar_w / 2, end_y - 100, progress_bar_w, 12 };
nvgSave(vg);
nvgIntersectScissor(vg, GetX(), GetY(), GetW(), GetH());
if (m_image) {
gfx::drawImage(vg, GetX() + 30, GetY() + 30, 128, 128, m_image, 10);
}
// shapes.
if (offset && size) {
gfx::drawRect(vg, prog_bar, theme->GetColour(ThemeEntryID_PROGRESSBAR_BACKGROUND));
const auto font_size = 18.F;
const auto pad = 15.F;
const float rounding = 5;
gfx::drawRect(vg, prog_bar, theme->GetColour(ThemeEntryID_PROGRESSBAR_BACKGROUND), rounding);
const u32 percentage = ((double)offset / (double)size) * 100.0;
gfx::drawRect(vg, prog_bar.x, prog_bar.y, ((float)offset / (float)size) * prog_bar.w, prog_bar.h, theme->GetColour(ThemeEntryID_PROGRESSBAR));
gfx::drawTextArgs(vg, prog_bar.x + prog_bar.w + 10, prog_bar.y, 20, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%u%%", percentage);
gfx::drawRect(vg, prog_bar.x, prog_bar.y, ((float)offset / (float)size) * prog_bar.w, prog_bar.h, theme->GetColour(ThemeEntryID_PROGRESSBAR), rounding);
gfx::drawTextArgs(vg, prog_bar.x + prog_bar.w + pad, prog_bar.y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%u%%", percentage);
const double speed_mb = (double)speed / (1024.0 * 1024.0);
const double speed_kb = (double)speed / (1024.0);
char speed_str[32];
if (speed_mb >= 0.01) {
std::snprintf(speed_str, sizeof(speed_str), "%.2f MiB/s", speed_mb);
} else {
std::snprintf(speed_str, sizeof(speed_str), "%.2f KiB/s", speed_kb);
}
const auto left = size - last_offset;
const auto left_seconds = left / speed;
const auto hours = left_seconds / (60 * 60);
const auto minutes = left_seconds % (60 * 60) / 60;
const auto seconds = left_seconds % 60;
char time_str[64];
if (hours) {
std::snprintf(time_str, sizeof(time_str), "%zu hours %zu minutes remaining", hours, minutes);
} else if (minutes) {
std::snprintf(time_str, sizeof(time_str), "%zu minutes %zu seconds remaining", minutes, seconds);
} else {
std::snprintf(time_str, sizeof(time_str), "%zu seconds remaining", seconds);
}
gfx::drawTextArgs(vg, center_x, prog_bar.y + prog_bar.h + 30, 18, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%s (%s)", time_str, speed_str);
}
gfx::drawTextArgs(vg, center_x, m_pos.y + 60, 25, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), title.c_str());
gfx::drawTextArgs(vg, center_x, m_pos.y + 40, 24, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), m_action.c_str());
gfx::drawTextArgs(vg, center_x, m_pos.y + 100, 22, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), title.c_str());
if (!transfer.empty()) {
gfx::drawTextArgs(vg, center_x, prog_bar.y - 15 - 20 * 1.5F, 20, NVG_ALIGN_CENTER, theme->GetColour(ThemeEntryID_TEXT), "%s", transfer.c_str());
gfx::drawTextArgs(vg, center_x, m_pos.y + 150, 18, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT_INFO), "%s", transfer.c_str());
}
nvgRestore(vg);
}
auto ProgressBox::SetTitle(const std::string& title) -> ProgressBox& {
@@ -115,6 +172,8 @@ auto ProgressBox::NewTransfer(const std::string& transfer) -> ProgressBox& {
m_transfer = transfer;
m_size = 0;
m_offset = 0;
m_last_offset = 0;
m_timestamp.Update();
mutexUnlock(&m_mutex);
Yield();
return *this;
@@ -129,6 +188,21 @@ auto ProgressBox::UpdateTransfer(s64 offset, s64 size) -> ProgressBox& {
return *this;
}
auto ProgressBox::SetImageData(std::vector<u8>& data) -> ProgressBox& {
mutexLock(&m_mutex);
std::swap(m_image_data, data);
mutexUnlock(&m_mutex);
return *this;
}
auto ProgressBox::SetImageDataConst(std::span<const u8> data) -> ProgressBox& {
mutexLock(&m_mutex);
m_image_data.resize(data.size());
std::memcpy(m_image_data.data(), data.data(), m_image_data.size());
mutexUnlock(&m_mutex);
return *this;
}
void ProgressBox::RequestExit() {
m_stop_source.request_stop();
}
@@ -184,4 +258,13 @@ void ProgressBox::Yield() {
svcSleepThread(YieldType_WithoutCoreMigration);
}
void ProgressBox::FreeImage() {
if (m_image && m_own_image) {
nvgDeleteImage(App::GetVg(), m_image);
}
m_image = 0;
m_own_image = false;
}
} // namespace sphaira::ui

View File

@@ -259,9 +259,11 @@ auto Sidebar::Update(Controller* controller, TouchInfo* touch) -> void {
if (touch->is_clicked && !touch->in_range(GetPos())) {
App::PopToMenu();
} else {
m_list->OnUpdate(controller, touch, m_items.size(), [this](auto i) {
m_list->OnUpdate(controller, touch, m_index, m_items.size(), [this](bool touch, auto i) {
SetIndex(i);
FireAction(Button::A);
if (touch) {
FireAction(Button::A);
}
});
}
@@ -334,18 +336,6 @@ void Sidebar::SetupButtons() {
// add default actions, overriding if needed.
this->SetActions(
std::make_pair(Button::DOWN, Action{[this](){
auto index = m_index;
if (m_list->ScrollDown(index, 1, m_items.size())) {
SetIndex(index);
}
}}),
std::make_pair(Button::UP, Action{[this](){
auto index = m_index;
if (m_list->ScrollUp(index, 1, m_items.size())) {
SetIndex(index);
}
}}),
// each item has it's own Action, but we take over B
std::make_pair(Button::B, Action{"Back"_i18n, [this](){
SetPop();

View File

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

View File

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

View File

@@ -106,13 +106,13 @@ Result DecryptTitleKey(keys::KeyEntry& out, u8 key_gen, const keys::Keys& keys)
// todo: i thought i already wrote the code for this??
// todo: patch the ticket.
Result PatchTicket(std::span<u8> ticket, const keys::Keys& keys, bool convert_personalised) {
Result PatchTicket(std::span<u8> ticket, const keys::Keys& keys) {
TicketData data;
R_TRY(GetTicketData(ticket, &data));
if (data.title_key_type == es::TicketTitleKeyType_Common) {
// todo: verify common signature
} else if (data.title_key_type == es::TicketTitleKeyType_Personalized && convert_personalised) {
} else if (data.title_key_type == es::TicketTitleKeyType_Personalized) {
}

View File

@@ -151,4 +151,30 @@ Result VerifyFixedKey(const Header& header) {
R_SUCCEED();
}
auto GetKeyGenStr(u8 key_gen) -> const char* {
switch (key_gen) {
case KeyGenerationOld_100: return "1.0.0";
case KeyGenerationOld_300: return "3.0.0";
case KeyGeneration_301: return "3.0.1";
case KeyGeneration_400: return "4.0.0";
case KeyGeneration_500: return "5.0.0";
case KeyGeneration_600: return "6.0.0";
case KeyGeneration_620: return "6.2.0";
case KeyGeneration_700: return "7.0.0";
case KeyGeneration_810: return "8.1.0";
case KeyGeneration_900: return "9.0.0";
case KeyGeneration_910: return "9.1.0";
case KeyGeneration_1210: return "12.1.0";
case KeyGeneration_1300: return "13.0.0";
case KeyGeneration_1400: return "14.0.0";
case KeyGeneration_1500: return "15.0.0";
case KeyGeneration_1600: return "16.0.0";
case KeyGeneration_1700: return "17.0.0";
case KeyGeneration_1800: return "18.0.0";
case KeyGeneration_1900: return "19.0.0";
}
return "Unknown";
}
} // namespace sphaira::nca

View File

@@ -7,16 +7,24 @@ namespace {
} // namespace
auto GetAppId(const NcmContentMetaKey& key) -> u64 {
if (key.type == NcmContentMetaType_Patch) {
return key.id ^ 0x800;
} else if (key.type == NcmContentMetaType_AddOnContent) {
return (key.id ^ 0x1000) & ~0xFFF;
auto GetAppId(u8 meta_type, u64 id) -> u64 {
if (meta_type == NcmContentMetaType_Patch) {
return id ^ 0x800;
} else if (meta_type == NcmContentMetaType_AddOnContent) {
return (id ^ 0x1000) & ~0xFFF;
} else {
return key.id;
return id;
}
}
auto GetAppId(const NcmContentMetaKey& key) -> u64 {
return GetAppId(key.type, key.id);
}
auto GetAppId(const PackagedContentMeta& meta) -> u64 {
return GetAppId(meta.meta_type, meta.title_id);
}
Result Delete(NcmContentStorage* cs, const NcmContentId *content_id) {
bool has;
R_TRY(ncmContentStorageHas(cs, std::addressof(has), content_id));

View File

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

View File

@@ -0,0 +1,41 @@
#include "yati/source/stream.hpp"
#include "defines.hpp"
#include "log.hpp"
namespace sphaira::yati::source {
Result Stream::Read(void* _buf, s64 off, s64 size, u64* bytes_read_out) {
// streams don't allow for random access (seeking backwards).
R_UNLESS(off >= m_offset, 0x1);
auto buf = static_cast<u8*>(_buf);
*bytes_read_out = 0;
// check if we already have some data in the buffer.
while (size) {
// while it is invalid to seek backwards, it is valid to seek forwards.
// this can be done to skip padding, skip undeeded files etc.
// to handle this, simply read the data into a buffer and discard it.
if (off > m_offset) {
const auto skip_size = off - m_offset;
std::vector<u8> temp_buf(skip_size);
u64 bytes_read;
R_TRY(ReadChunk(temp_buf.data(), temp_buf.size(), &bytes_read));
m_offset += bytes_read;
} else {
u64 bytes_read;
R_TRY(ReadChunk(buf, size, &bytes_read));
*bytes_read_out += bytes_read;
buf += bytes_read;
off += bytes_read;
m_offset += bytes_read;
size -= bytes_read;
}
}
R_SUCCEED();
}
} // namespace sphaira::yati::source

View File

@@ -0,0 +1,23 @@
#include "yati/source/stream_file.hpp"
#include "log.hpp"
namespace sphaira::yati::source {
StreamFile::StreamFile(FsFileSystem* fs, const fs::FsPath& path) {
m_open_result = fsFsOpenFile(fs, path, FsOpenMode_Read, std::addressof(m_file));
}
StreamFile::~StreamFile() {
if (R_SUCCEEDED(GetOpenResult())) {
fsFileClose(std::addressof(m_file));
}
}
Result StreamFile::ReadChunk(void* buf, s64 size, u64* bytes_read) {
R_TRY(GetOpenResult());
const auto rc = fsFileRead(std::addressof(m_file), m_offset, buf, size, 0, bytes_read);
m_offset += *bytes_read;
return rc;
}
} // namespace sphaira::yati::source

View File

@@ -22,7 +22,7 @@ namespace sphaira::yati::source {
namespace {
constexpr u32 MAGIC = 0x53504841;
constexpr u32 VERSION = 1;
constexpr u32 VERSION = 2;
struct SendHeader {
u32 magic;
@@ -224,6 +224,28 @@ Result Usb::WaitForConnection(u64 timeout, u32& speed, u32& count) {
R_SUCCEED();
}
Result Usb::GetFileInfo(std::string& name_out, u64& size_out) {
struct {
u64 size;
u64 name_length;
} file_info_meta;
alignas(0x1000) u8 aligned[0x1000]{};
// receive meta.
u32 transferredSize;
R_TRY(TransferPacketImpl(true, aligned, sizeof(file_info_meta), &transferredSize, m_transfer_timeout));
std::memcpy(&file_info_meta, aligned, sizeof(file_info_meta));
R_UNLESS(file_info_meta.name_length < sizeof(aligned), 0x1);
R_TRY(TransferPacketImpl(true, aligned, file_info_meta.name_length, &transferredSize, m_transfer_timeout));
name_out.resize(file_info_meta.name_length);
std::memcpy(name_out.data(), aligned, name_out.size());
size_out = file_info_meta.size;
R_SUCCEED();
}
bool Usb::GetConfigured() const {
UsbState usb_state;
usbDsGetState(std::addressof(usb_state));

View File

@@ -1,5 +1,6 @@
#include "yati/yati.hpp"
#include "yati/source/file.hpp"
#include "yati/source/stream_file.hpp"
#include "yati/source/stdio.hpp"
#include "yati/container/nsp.hpp"
#include "yati/container/xci.hpp"
@@ -36,13 +37,13 @@ struct CustomVectorAllocator {
public:
// https://en.cppreference.com/w/cpp/memory/new/operator_new
auto allocate(std::size_t n) -> T* {
log_write("allocating ptr size: %zu\n", n);
// log_write("allocating ptr size: %zu\n", n);
return new(align) T[n];
}
// https://en.cppreference.com/w/cpp/memory/new/operator_delete
auto deallocate(T* p, std::size_t n) noexcept -> void {
log_write("deleting ptr size: %zu\n", n);
// log_write("deleting ptr size: %zu\n", n);
::operator delete[] (p, n, align);
}
@@ -62,17 +63,6 @@ using PageAlignedVector = std::vector<u8, PageAllocator<u8>>;
constexpr u32 KEYGEN_LIMIT = 0x20;
#if 0
struct FwVersion {
u32 value;
auto relstep() const -> u8 { return (value >> 0) & 0xFFFF; }
auto micro() const -> u8 { return (value >> 16) & 0x000F; }
auto minor() const -> u8 { return (value >> 20) & 0x003F; }
auto major() const -> u8 { return (value >> 26) & 0x003F; }
auto hos() const -> u32 { return MAKEHOSVERSION(major(), minor(), micro()); }
};
#endif
struct NcaCollection : container::CollectionEntry {
// NcmContentType
u8 type{};
@@ -284,7 +274,7 @@ struct Yati {
Yati(ui::ProgressBox*, std::shared_ptr<source::Base>);
~Yati();
Result Setup(container::Collections& out);
Result Setup(const ConfigOverride& override);
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);
@@ -535,7 +549,7 @@ Result Yati::decompressFuncInternal(ThreadData* t) {
t->write_size = header.size;
R_TRY(ncmContentStorageSetPlaceHolderSize(std::addressof(cs), std::addressof(t->nca->placeholder_id), header.size));
if (header.distribution_type == nca::DistributionType_GameCard) {
if (!config.ignore_distribution_bit && header.distribution_type == nca::DistributionType_GameCard) {
header.distribution_type = nca::DistributionType_System;
t->nca->modified = true;
}
@@ -778,24 +792,23 @@ Yati::~Yati() {
appletSetMediaPlaybackState(false);
}
Result Yati::Setup(container::Collections& out) {
config.sd_card_install = App::GetApp()->m_install_sd.Get();
Result Yati::Setup(const ConfigOverride& override) {
config.sd_card_install = override.sd_card_install.value_or(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();
config.skip_data_patch = App::GetApp()->m_skip_data_patch.Get();
config.skip_ticket = App::GetApp()->m_skip_ticket.Get();
config.skip_nca_hash_verify = App::GetApp()->m_skip_nca_hash_verify.Get();
config.skip_rsa_header_fixed_key_verify = App::GetApp()->m_skip_rsa_header_fixed_key_verify.Get();
config.skip_rsa_npdm_fixed_key_verify = App::GetApp()->m_skip_rsa_npdm_fixed_key_verify.Get();
config.ignore_distribution_bit = App::GetApp()->m_ignore_distribution_bit.Get();
config.convert_to_standard_crypto = App::GetApp()->m_convert_to_standard_crypto.Get();
config.lower_master_key = App::GetApp()->m_lower_master_key.Get();
config.lower_system_version = App::GetApp()->m_lower_system_version.Get();
config.skip_nca_hash_verify = override.skip_nca_hash_verify.value_or(App::GetApp()->m_skip_nca_hash_verify.Get());
config.skip_rsa_header_fixed_key_verify = override.skip_rsa_header_fixed_key_verify.value_or(App::GetApp()->m_skip_rsa_header_fixed_key_verify.Get());
config.skip_rsa_npdm_fixed_key_verify = override.skip_rsa_npdm_fixed_key_verify.value_or(App::GetApp()->m_skip_rsa_npdm_fixed_key_verify.Get());
config.ignore_distribution_bit = override.ignore_distribution_bit.value_or(App::GetApp()->m_ignore_distribution_bit.Get());
config.convert_to_standard_crypto = override.convert_to_standard_crypto.value_or(App::GetApp()->m_convert_to_standard_crypto.Get());
config.lower_master_key = override.lower_master_key.value_or(App::GetApp()->m_lower_master_key.Get());
config.lower_system_version = override.lower_system_version.value_or(App::GetApp()->m_lower_system_version.Get());
storage_id = config.sd_card_install ? NcmStorageId_SdCard : NcmStorageId_BuiltInUser;
R_TRY(source->GetOpenResult());
@@ -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");
@@ -926,37 +925,9 @@ Result Yati::InstallCnmtNca(std::span<TikCollection> tickets, CnmtCollection& cn
R_TRY(ncmContentStorageFlushPlaceHolder(std::addressof(cs)));
R_TRY(ncmContentStorageGetPlaceHolderPath(std::addressof(cs), path, sizeof(path), std::addressof(cnmt.placeholder_id)));
FsFileSystem fs;
R_TRY(fsOpenFileSystem(std::addressof(fs), FsFileSystemType_ContentMeta, path));
ON_SCOPE_EXIT(fsFsClose(std::addressof(fs)));
FsDir dir;
R_TRY(fsFsOpenDirectory(std::addressof(fs), fs::FsPath{"/"}, FsDirOpenMode_ReadFiles, std::addressof(dir)));
ON_SCOPE_EXIT(fsDirClose(std::addressof(dir)));
s64 total_entries;
FsDirectoryEntry buf;
R_TRY(fsDirRead(std::addressof(dir), std::addressof(total_entries), 1, std::addressof(buf)));
FsFile file;
R_TRY(fsFsOpenFile(std::addressof(fs), fs::AppendPath("/", buf.name), FsOpenMode_Read, std::addressof(file)));
ON_SCOPE_EXIT(fsFileClose(std::addressof(file)));
s64 offset{};
u64 bytes_read;
ncm::PackagedContentMeta header;
R_TRY(fsFileRead(std::addressof(file), offset, std::addressof(header), sizeof(header), 0, std::addressof(bytes_read)));
offset += bytes_read;
// read extended header
cnmt.extended_header.resize(header.meta_header.extended_header_size);
R_TRY(fsFileRead(std::addressof(file), offset, cnmt.extended_header.data(), cnmt.extended_header.size(), 0, std::addressof(bytes_read)));
offset += bytes_read;
// read infos.
std::vector<NcmPackagedContentInfo> infos(header.meta_header.content_count);
R_TRY(fsFileRead(std::addressof(file), offset, infos.data(), infos.size() * sizeof(NcmPackagedContentInfo), 0, std::addressof(bytes_read)));
offset += bytes_read;
std::vector<NcmPackagedContentInfo> infos;
R_TRY(ParseCnmtNca(path, header, cnmt.extended_header, infos));
for (const auto& info : infos) {
if (info.info.content_type == NcmContentType_DeltaFragment) {
@@ -1021,32 +992,20 @@ Result Yati::InstallControlNca(std::span<TikCollection> tickets, const CnmtColle
R_TRY(ncmContentStorageFlushPlaceHolder(std::addressof(cs)));
R_TRY(ncmContentStorageGetPlaceHolderPath(std::addressof(cs), path, sizeof(path), std::addressof(nca.placeholder_id)));
log_write("got control path: %s\n", path.s);
FsFileSystem fs;
R_TRY(fsOpenFileSystemWithId(std::addressof(fs), ncm::GetAppId(cnmt.key), FsFileSystemType_ContentControl, path, FsContentAttributes_All));
ON_SCOPE_EXIT(fsFsClose(std::addressof(fs)));
log_write("opened control path fs: %s\n", path.s);
FsFile file;
R_TRY(fsFsOpenFile(std::addressof(fs), fs::FsPath{"/control.nacp"}, FsOpenMode_Read, std::addressof(file)));
ON_SCOPE_EXIT(fsFileClose(std::addressof(file)));
log_write("got control path file: %s\n", path.s);
// this can fail if it's not a valid control nca, examples are mario 3d all stars.
// there are 4 control ncas, only 1 is valid (InvalidNcaId 0x235E02).
NacpLanguageEntry entry;
u64 bytes_read;
R_TRY(fsFileRead(&file, 0, &entry, sizeof(entry), 0, &bytes_read));
pbox->SetTitle("Installing "_i18n + entry.name);
std::vector<u8> icon;
if (R_SUCCEEDED(yati::ParseControlNca(path, ncm::GetAppId(cnmt.key), &entry, sizeof(entry), &icon))) {
pbox->SetTitle(entry.name).SetImageData(icon);
} else {
log_write("\tWARNING: failed to parse control nca!\n");
}
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 +1020,221 @@ 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);
version_out = cnmt.key.version;
for (auto& db : ncm_db) {
s32 db_list_total;
s32 db_list_count;
std::vector<NcmContentMetaKey> keys(1);
if (R_SUCCEEDED(ncmContentMetaDatabaseList(std::addressof(db), std::addressof(db_list_total), std::addressof(db_list_count), keys.data(), keys.size(), NcmContentMetaType_Unknown, app_id, 0, UINT64_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(), NcmContentMetaType_Unknown, app_id, 0, UINT64_MAX, NcmContentInstallType_Full));
}
}
for (auto& key : keys) {
log_write("found record: %016lX type: %u version: %u\n", key.id, key.type, key.version);
if (key.id == cnmt.key.id && cnmt.key.version == 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 == key.type && cnmt.key.version < key.version && !config.allow_downgrade) {
log_write("skipping due to it being lower\n");
skip = true;
}
} else {
version_out = std::max(version_out, 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;
// 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, const ConfigOverride& override) {
auto yati = std::make_unique<Yati>(pbox, source);
R_TRY(yati->Setup(override));
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 +1250,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 +1270,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, const ConfigOverride& override) {
auto yati = std::make_unique<Yati>(pbox, source);
R_TRY(yati->Setup(override));
// 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 +1376,107 @@ 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 InstallFromFile(ui::ProgressBox* pbox, FsFileSystem* fs, const fs::FsPath& path, const ConfigOverride& override) {
return InstallFromSource(pbox, std::make_shared<source::File>(fs, path), path, override);
// return InstallFromSource(pbox, std::make_shared<source::StreamFile>(fs, path), path, override);
}
Result InstallFromStdioFile(const char* path) {
return InstallFromSource(std::make_shared<source::Stdio>(path));
Result InstallFromStdioFile(ui::ProgressBox* pbox, const fs::FsPath& path, const ConfigOverride& override) {
return InstallFromSource(pbox, std::make_shared<source::Stdio>(path), path, override);
}
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));
}));
Result InstallFromSource(ui::ProgressBox* pbox, std::shared_ptr<source::Base> source, const fs::FsPath& path, const ConfigOverride& override) {
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), override);
} else if (!strcasecmp(ext, ".xci") || !strcasecmp(ext, ".xcz")) {
return InstallFromContainer(pbox, std::make_unique<container::Xci>(source), override);
}
R_THROW(Result_ContainerNotFound);
}
Result InstallFromContainer(ui::ProgressBox* pbox, std::shared_ptr<container::Base> container, const ConfigOverride& override) {
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, const ConfigOverride& override) {
if (source->IsStream()) {
return InstallInternalStream(pbox, source, collections, override);
} else {
return InstallInternal(pbox, source, collections, override);
}
}
Result ParseCnmtNca(const fs::FsPath& path, ncm::PackagedContentMeta& header, std::vector<u8>& extended_header, std::vector<NcmPackagedContentInfo>& infos) {
FsFileSystem fs;
R_TRY(fsOpenFileSystem(std::addressof(fs), FsFileSystemType_ContentMeta, path));
ON_SCOPE_EXIT(fsFsClose(std::addressof(fs)));
FsDir dir;
R_TRY(fsFsOpenDirectory(std::addressof(fs), fs::FsPath{"/"}, FsDirOpenMode_ReadFiles, std::addressof(dir)));
ON_SCOPE_EXIT(fsDirClose(std::addressof(dir)));
s64 total_entries;
FsDirectoryEntry buf;
R_TRY(fsDirRead(std::addressof(dir), std::addressof(total_entries), 1, std::addressof(buf)));
FsFile file;
R_TRY(fsFsOpenFile(std::addressof(fs), fs::AppendPath("/", buf.name), FsOpenMode_Read, std::addressof(file)));
ON_SCOPE_EXIT(fsFileClose(std::addressof(file)));
s64 offset{};
u64 bytes_read;
R_TRY(fsFileRead(std::addressof(file), offset, std::addressof(header), sizeof(header), 0, std::addressof(bytes_read)));
offset += bytes_read;
// read extended header
extended_header.resize(header.meta_header.extended_header_size);
R_TRY(fsFileRead(std::addressof(file), offset, extended_header.data(), extended_header.size(), 0, std::addressof(bytes_read)));
offset += bytes_read;
// read infos.
infos.resize(header.meta_header.content_count);
R_TRY(fsFileRead(std::addressof(file), offset, infos.data(), infos.size() * sizeof(NcmPackagedContentInfo), 0, std::addressof(bytes_read)));
offset += bytes_read;
R_SUCCEED();
}
Result InstallFromFile(ui::ProgressBox* pbox, FsFileSystem* fs, const fs::FsPath& path) {
return InstallFromSource(pbox, std::make_shared<source::File>(fs, path));
}
Result ParseControlNca(const fs::FsPath& path, u64 id, void* nacp_out, s64 nacp_size, std::vector<u8>* icon_out) {
FsFileSystem fs;
R_TRY(fsOpenFileSystemWithId(std::addressof(fs), id, FsFileSystemType_ContentControl, path, FsContentAttributes_All));
ON_SCOPE_EXIT(fsFsClose(std::addressof(fs)));
Result InstallFromStdioFile(ui::ProgressBox* pbox, const char* path) {
return InstallFromSource(pbox, std::make_shared<source::Stdio>(path));
}
// read nacp.
if (nacp_out) {
FsFile file;
R_TRY(fsFsOpenFile(std::addressof(fs), fs::FsPath{"/control.nacp"}, FsOpenMode_Read, std::addressof(file)));
ON_SCOPE_EXIT(fsFileClose(std::addressof(file)));
Result InstallFromSource(ui::ProgressBox* pbox, std::shared_ptr<source::Base> source) {
return InstallInternal(pbox, source);
u64 bytes_read;
R_TRY(fsFileRead(&file, 0, nacp_out, nacp_size, 0, &bytes_read));
}
// read icon.
if (icon_out) {
FsFile file;
R_TRY(fsFsOpenFile(std::addressof(fs), fs::FsPath{"/icon_AmericanEnglish.dat"}, FsOpenMode_Read, std::addressof(file)));
ON_SCOPE_EXIT(fsFileClose(std::addressof(file)));
s64 size;
R_TRY(fsFileGetSize(std::addressof(file), std::addressof(size)));
icon_out->resize(size);
u64 bytes_read;
R_TRY(fsFileRead(&file, 0, icon_out->data(), icon_out->size(), 0, &bytes_read));
}
R_SUCCEED();
}
} // namespace sphaira::yati

1
tools/requirements.txt Normal file
View File

@@ -0,0 +1 @@
pyusb

View File

@@ -6,11 +6,12 @@ import usb.core
import usb.util
import time
import glob
from pathlib import Path
# magic number (SPHA) for the script and switch.
MAGIC = 0x53504841
# version of the usb script.
VERSION = 1
VERSION = 2
# list of supported extensions.
EXTS = (".nsp", ".xci", ".nsz", ".xcz")
@@ -28,6 +29,15 @@ def verify_switch(bcdUSB, count, in_ep, out_ep):
send_data = struct.pack('<IIII', MAGIC, VERSION, bcdUSB, count)
out_ep.write(data=send_data, timeout=0)
def send_file_info(path, in_ep, out_ep):
file_name = Path(path).name
file_size = Path(path).stat().st_size
file_name_len = len(file_name)
send_data = struct.pack('<QQ', file_size, file_name_len)
out_ep.write(data=send_data, timeout=0)
out_ep.write(data=file_name, timeout=0)
def wait_for_input(path, in_ep, out_ep):
buf = None
predicted_off = 0
@@ -124,6 +134,7 @@ if __name__ == '__main__':
for file in files:
print("installing file: {}".format(file))
send_file_info(file, in_ep, out_ep)
wait_for_input(file, in_ep, out_ep)
dev.reset()
except Exception as inst: