huge changes to everything (see below).

Changelog:
- re-enable use in release build.
- remove ftpsrv and untitled from builtin ghdl options, as both packages are available in the appstore.
- add image viewer (png, jpg, bmp)
- add music player (bfstm, bfwav, mp3, wav, ogg)
- add idv3 tag parsing support for mp3.
- add "decyption" of GTA Vice City mp3.
- add usbdvd support for music playback and file browsing.
- add nsz export support (solid, block, ldm).
- add xcz export support (same as above).
- add nro fs proper mount support (romfs, nacp, icon).
- add program nca fs support.
- add bfsar fs support.
- re-write the usb protocol, still wip. replaces tinfoil protocol.
- all threads are now create with pre-emptive support with the proper affinity mask set.
- fix oob crash in libpulsar when a bfwav was opened that had more than 2 channels.
- bump yyjson version.
- bump usbhsfs version.
- disable nvjpg.
- add support for theme music of any supported playback type (bfstm, bfwav, mp3, wav, ogg).
- add support for setting background music.
- add async exit to blocking threads (download, nxlink, ftpsrv) to reduce exit time.
- add support for dumping to pc via usb.
- add null, deflate, zstd hash options, mainly used for benchmarking.
- add sidebar slider (currently unused).
- file_viwer can now be used with any filesystem.
- filebrowser will only ever stat file once. previously it would keep stat'ing until it succeeded.
- disabled themezer due to the api breaking and i am not willing to keep maintaining it.
- disable zlt handling in usbds as it's not needed for my api's because the size is always known.
- remove usbds enums and GetSpeed() as i pr'd it to libnx.
- added support for mounting nca's from any source, including files, memory, nsps, xcis etc.
- split the lru cache into it's own header as it's now used in multiple places (nsz, all mounted options).
- add support for fetching and decrypting es personalised tickets.
- fix es common ticket converting where i forgot to also convert the cert chain as well.
- remove the download default music option.
- improve performance of libpulsar when opening a bfsar by remove the large setvbuf option. instead, use the default 1k buffer and handle large buffers manually in sphaira by using a lru cache (todo: just write my own bfsar parser).
- during app init and exit, load times have been halved as i now load/exit async. timestamps have also been added to measure how long everything takes.
- download now async loads / exits the etag json file to improve init times.
- add custom zip io to dumper to support writing a zip to any dest (such as usb).
- dumper now returns a proper error if the transfer was cancelled by the user.
- fatfs mount now sets the timestamp for files.
- fatfs mount handles folders with the archive bit by reporting them as a file.
- ftpsrv config is async loaded to speed up load times.
- nxlink now tries attempt to connect/accept by handling blocking rather than just bailing out.
- added support for minini floats.
- thread_file_transfer now spawns 3 threads rather than 2, to have the middle thread be a optional processor (mainly used for compressing/decompressing).
- added spinner to progress box, taken from nvg demo.
- progress box disables sleep mode on init.
- add gamecard detection to game menu to detect a refresh.
- handle xci that have the key area prepended.
- change gamecard mount fs to use the xci mount code instead of native fs, that way we can see all the partitions rather than just secure.
- reformat the ghdl entries to show the timestamp first.
- support for exporting saves to pc via usb.
- zip fs now uses lru cache.
This commit is contained in:
ITotalJustice
2025-08-28 23:12:34 +01:00
parent cd6fed6aae
commit f0bdc01156
127 changed files with 14623 additions and 13020 deletions

View File

@@ -2,13 +2,13 @@
#include "nanovg.h"
#include "nanovg/dk_renderer.hpp"
#include "pulsar.h"
#include "ui/widget.hpp"
#include "ui/notification.hpp"
#include "owo.hpp"
#include "option.hpp"
#include "fs.hpp"
#include "log.hpp"
#include "utils/audio.hpp"
#ifdef USE_NVJPG
#include <nvjpg.hpp>
@@ -22,16 +22,7 @@
namespace sphaira {
enum SoundEffect {
SoundEffect_Music,
SoundEffect_Focus,
SoundEffect_Scroll,
SoundEffect_Limit,
SoundEffect_Startup,
SoundEffect_Install,
SoundEffect_Error,
SoundEffect_MAX,
};
using SoundEffect = audio::SoundEffect;
enum class LaunchType {
Normal,
@@ -108,6 +99,10 @@ public:
static auto GetLanguage() -> long;
static auto GetTextScrollSpeed() -> long;
static auto GetNszCompressLevel() -> u8;
static auto GetNszThreadCount() -> u8;
static auto GetNszBlockExponent() -> u8;
static void SetMtpEnable(bool enable);
static void SetFtpEnable(bool enable);
static void SetNxlinkEnable(bool enable);
@@ -153,8 +148,12 @@ public:
void LoadTheme(const ThemeMeta& meta);
void CloseTheme();
void CloseThemeBackgroundMusic();
void ScanThemes(const std::string& path);
void ScanThemeEntries();
void LoadAndPlayThemeMusic();
static Result SetDefaultBackgroundMusic(fs::Fs* fs, const fs::FsPath& path);
static void SetBackgroundMusicPause(bool pause);
// helper that converts 1.2.3 to a u32 used for comparisons.
static auto GetVersionFromString(const char* str) -> u32;
@@ -294,6 +293,7 @@ public:
option::OptionBool m_log_enabled{INI_SECTION, "log_enabled", false};
option::OptionBool m_replace_hbmenu{INI_SECTION, "replace_hbmenu", false};
option::OptionString m_default_music{INI_SECTION, "default_music", "/config/sphaira/themes/default_music.bfstm"};
option::OptionString m_theme_path{INI_SECTION, "theme", DEFAULT_THEME_PATH};
option::OptionBool m_theme_music{INI_SECTION, "theme_music", true};
option::OptionBool m_12hour_time{INI_SECTION, "12hour_time", false};
@@ -328,13 +328,18 @@ public:
option::OptionBool m_dump_append_folder_with_xci{"dump", "append_folder_with_xci", true};
option::OptionBool m_dump_trim_xci{"dump", "trim_xci", false};
option::OptionBool m_dump_label_trim_xci{"dump", "label_trim_xci", false};
option::OptionBool m_dump_usb_transfer_stream{"dump", "usb_transfer_stream", true, false};
option::OptionBool m_dump_convert_to_common_ticket{"dump", "convert_to_common_ticket", true};
option::OptionLong m_nsz_compress_level{"dump", "nsz_compress_level", 3};
option::OptionLong m_nsz_compress_threads{"dump", "nsz_compress_threads", 3};
option::OptionBool m_nsz_compress_ldm{"dump", "nsz_compress_ldm", true};
option::OptionBool m_nsz_compress_block{"dump", "nsz_compress_block", false};
option::OptionLong m_nsz_compress_block_exponent{"dump", "nsz_compress_block_exponent", 6};
// todo: move this into it's own menu
option::OptionLong m_text_scroll_speed{"accessibility", "text_scroll_speed", 1}; // normal
PLSR_PlayerSoundId m_sound_ids[SoundEffect_MAX]{};
std::shared_ptr<fs::FsNativeSd> m_fs{};
audio::SongID m_background_music{};
#ifdef USE_NVJPG
nj::Decoder m_decoder;

View File

@@ -577,6 +577,7 @@ enum class SphairaResult : Result {
UsbDsBadDeviceSpeed,
NcaBadMagic,
NspBadMagic,
XciBadMagic,
XciSecurePartitionNotFound,
@@ -648,6 +649,17 @@ enum class SphairaResult : Result {
YatiNcmDbCorruptHeader,
// unable to total infos from ncm database.
YatiNcmDbCorruptInfos,
NszFailedCreateCctx,
NszFailedSetCompressionLevel,
NszFailedSetThreadCount,
NszFailedSetLongDistanceMode,
NszFailedResetCctx,
NszFailedCompress2,
NszFailedCompressStream2,
NszTooManyBlocks,
// set when nca finished but not all blocks were handled.
NszMissingBlocks,
};
#define MAKE_SPHAIRA_RESULT_ENUM(x) Result_##x = MAKERESULT(Module_Sphaira, (Result)SphairaResult::x)
@@ -717,8 +729,11 @@ enum : Result {
MAKE_SPHAIRA_RESULT_ENUM(ThemezerFailedToDownloadTheme),
MAKE_SPHAIRA_RESULT_ENUM(MainFailedToDownloadUpdate),
MAKE_SPHAIRA_RESULT_ENUM(UsbDsBadDeviceSpeed),
MAKE_SPHAIRA_RESULT_ENUM(NspBadMagic),
MAKE_SPHAIRA_RESULT_ENUM(XciBadMagic),
MAKE_SPHAIRA_RESULT_ENUM(NcaBadMagic),
MAKE_SPHAIRA_RESULT_ENUM(XciSecurePartitionNotFound),
MAKE_SPHAIRA_RESULT_ENUM(EsBadTitleKeyType),
MAKE_SPHAIRA_RESULT_ENUM(EsPersonalisedTicketDeviceIdMissmatch),
@@ -729,7 +744,9 @@ enum : Result {
MAKE_SPHAIRA_RESULT_ENUM(EsInvalidTicketFromatVersion),
MAKE_SPHAIRA_RESULT_ENUM(EsInvalidTicketKeyType),
MAKE_SPHAIRA_RESULT_ENUM(EsInvalidTicketKeyRevision),
MAKE_SPHAIRA_RESULT_ENUM(OwoBadArgs),
MAKE_SPHAIRA_RESULT_ENUM(UsbCancelled),
MAKE_SPHAIRA_RESULT_ENUM(UsbBadMagic),
MAKE_SPHAIRA_RESULT_ENUM(UsbBadVersion),
@@ -744,6 +761,7 @@ enum : Result {
MAKE_SPHAIRA_RESULT_ENUM(UsbUploadBadTransferSize),
MAKE_SPHAIRA_RESULT_ENUM(UsbUploadBadTotalSize),
MAKE_SPHAIRA_RESULT_ENUM(UsbUploadBadCommand),
MAKE_SPHAIRA_RESULT_ENUM(YatiContainerNotFound),
MAKE_SPHAIRA_RESULT_ENUM(YatiNcaNotFound),
MAKE_SPHAIRA_RESULT_ENUM(YatiInvalidNcaReadSize),
@@ -765,6 +783,16 @@ enum : Result {
MAKE_SPHAIRA_RESULT_ENUM(YatiCertNotFound),
MAKE_SPHAIRA_RESULT_ENUM(YatiNcmDbCorruptHeader),
MAKE_SPHAIRA_RESULT_ENUM(YatiNcmDbCorruptInfos),
MAKE_SPHAIRA_RESULT_ENUM(NszFailedCreateCctx),
MAKE_SPHAIRA_RESULT_ENUM(NszFailedSetCompressionLevel),
MAKE_SPHAIRA_RESULT_ENUM(NszFailedSetThreadCount),
MAKE_SPHAIRA_RESULT_ENUM(NszFailedSetLongDistanceMode),
MAKE_SPHAIRA_RESULT_ENUM(NszFailedResetCctx),
MAKE_SPHAIRA_RESULT_ENUM(NszFailedCompress2),
MAKE_SPHAIRA_RESULT_ENUM(NszFailedCompressStream2),
MAKE_SPHAIRA_RESULT_ENUM(NszTooManyBlocks),
MAKE_SPHAIRA_RESULT_ENUM(NszMissingBlocks),
};
#undef MAKE_SPHAIRA_RESULT_ENUM
@@ -799,16 +827,6 @@ enum : Result {
// #define ON_SCOPE_FAIL(_f) std::experimental::scope_exit ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE_){[&] { if (R_FAILED(rc)) { _f; } }};
// #define ON_SCOPE_SUCCESS(_f) std::experimental::scope_exit ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE_){[&] { if (R_SUCCEEDED(rc)) { _f; } }};
// threading helpers.
#define PRIO_PREEMPTIVE 0x3B
// threading affinity, use with svcSetThreadCoreMask().
#define THREAD_AFFINITY_CORE0 BIT(0)
#define THREAD_AFFINITY_CORE1 BIT(1)
#define THREAD_AFFINITY_CORE2 BIT(2)
#define THREAD_AFFINITY_DEFAULT(core) (BIT(core)|THREAD_AFFINITY_CORE1|THREAD_AFFINITY_CORE2)
#define THREAD_AFFINITY_ALL (THREAD_AFFINITY_CORE0|THREAD_AFFINITY_CORE1|THREAD_AFFINITY_CORE2)
// mutex helpers.
#define SCOPED_MUTEX(mutex) \
mutexLock(mutex); \

View File

@@ -142,6 +142,7 @@ struct DownloadEventData {
auto Init() -> bool;
void Exit();
void ExitSignal();
// sync functions
auto ToMemory(const Api& e) -> ApiResult;

View File

@@ -2,16 +2,21 @@
#include "fs.hpp"
#include "location.hpp"
#include "ui/progress_box.hpp"
#include <switch.h>
#include <vector>
#include <memory>
#include <functional>
#include <minizip/ioapi.h>
namespace sphaira::dump {
enum DumpLocationType {
// dump using native fs.
DumpLocationType_SdCard,
// dump to usb pc.
DumpLocationType_Usb,
// dump to usb using tinfoil protocol.
DumpLocationType_UsbS2S,
// speed test, only reads the data, doesn't write anything.
@@ -24,11 +29,12 @@ enum DumpLocationType {
enum DumpLocationFlag {
DumpLocationFlag_SdCard = 1 << DumpLocationType_SdCard,
DumpLocationFlag_Usb = 1 << DumpLocationType_Usb,
DumpLocationFlag_UsbS2S = 1 << DumpLocationType_UsbS2S,
DumpLocationFlag_DevNull = 1 << DumpLocationType_DevNull,
DumpLocationFlag_Stdio = 1 << DumpLocationType_Stdio,
DumpLocationFlag_Network = 1 << DumpLocationType_Network,
DumpLocationFlag_All = DumpLocationFlag_SdCard | DumpLocationFlag_UsbS2S | DumpLocationFlag_DevNull | DumpLocationFlag_Stdio | DumpLocationFlag_Network,
DumpLocationFlag_All = DumpLocationFlag_SdCard | DumpLocationFlag_Usb | DumpLocationFlag_UsbS2S | DumpLocationFlag_DevNull | DumpLocationFlag_Stdio | DumpLocationFlag_Network,
};
struct DumpEntry {
@@ -48,17 +54,36 @@ struct BaseSource {
virtual auto GetName(const std::string& path) const -> std::string = 0;
virtual auto GetSize(const std::string& path) const -> s64 = 0;
virtual auto GetIcon(const std::string& path) const -> int { return 0; }
Result Read(const std::string& path, void* buf, s64 off, s64 size) {
u64 bytes_read;
return Read(path, buf, off, size, &bytes_read);
}
};
struct WriteSource {
virtual ~WriteSource() = default;
virtual Result Write(const void* buf, s64 off, s64 size) = 0;
virtual Result SetSize(s64 size) = 0;
};
// called after dump has finished.
using OnExit = std::function<void(Result rc)>;
using OnLocation = std::function<void(const DumpLocation& loc)>;
using CustomTransfer = std::function<Result(ui::ProgressBox* pbox, BaseSource* source, WriteSource* writer, const fs::FsPath& path)>;
// prompts the user to select dump location, calls on_loc on success with the selected location.
void DumpGetLocation(const std::string& title, u32 location_flags, const OnLocation& on_loc);
void DumpGetLocation(const std::string& title, u32 location_flags, const OnLocation& on_loc, const CustomTransfer& custom_transfer = nullptr);
Result Dump(ui::ProgressBox* pbox, const std::shared_ptr<BaseSource>& source, const DumpLocation& location, const std::vector<fs::FsPath>& paths, const CustomTransfer& custom_transfer = nullptr);
// dumps to a fetched location using DumpGetLocation().
void Dump(const std::shared_ptr<BaseSource>& source, const DumpLocation& location, const std::vector<fs::FsPath>& paths, const OnExit& on_exit);
void Dump(const std::shared_ptr<BaseSource>& source, const DumpLocation& location, const std::vector<fs::FsPath>& paths, const OnExit& on_exit, const CustomTransfer& custom_transfer = nullptr);
// DumpGetLocation() + Dump() all in one.
void Dump(const std::shared_ptr<BaseSource>& source, const std::vector<fs::FsPath>& paths, const OnExit& on_exit = [](Result){}, u32 location_flags = DumpLocationFlag_All);
void Dump(const std::shared_ptr<BaseSource>& source, const std::vector<fs::FsPath>& paths, const OnExit& on_exit = nullptr, u32 location_flags = DumpLocationFlag_All);
void Dump(const std::shared_ptr<BaseSource>& source, const std::vector<fs::FsPath>& paths, const CustomTransfer& custom_transfer, const OnExit& on_exit = nullptr, u32 location_flags = DumpLocationFlag_All);
void FileFuncWriter(WriteSource* writer, zlib_filefunc64_def* funcs);
} // namespace sphaira::dump

View File

@@ -1,8 +1,6 @@
#pragma once
#include <switch.h>
#include <vector>
#include <string>
namespace sphaira::fatfs {

View File

@@ -322,6 +322,7 @@ struct Fs {
virtual bool FileExists(const FsPath& path) = 0;
virtual bool DirExists(const FsPath& path) = 0;
virtual bool IsNative() const = 0;
virtual bool IsSd() const { return false; }
virtual FsPath Root() const { return "/"; }
Result OpenFile(const fs::FsPath& path, u32 mode, File* f) {
@@ -510,6 +511,8 @@ struct FsNativeSd final : FsNative {
FsNativeSd(bool ignore_read_only = true) : FsNative{fsdevGetDeviceFileSystem("sdmc:"), false, ignore_read_only} {
m_open_result = 0;
}
bool IsSd() const override { return true; }
};
#endif

View File

@@ -14,6 +14,9 @@ enum class Type {
Md5,
Sha1,
Sha256,
Null,
Deflate,
Zstd,
};
struct BaseSource {

View File

@@ -5,6 +5,7 @@
namespace sphaira::haze {
bool Init();
bool IsInit();
void Exit();
using OnInstallStart = std::function<bool(const char* path)>;

View File

@@ -9,6 +9,11 @@
namespace sphaira {
struct NroData {
NroStart start;
NroHeader header;
};
struct Hbini {
u64 timestamp{}; // timestamp of last launch
bool hidden{};
@@ -27,9 +32,6 @@ struct NroEntry {
u64 icon_size{};
u64 icon_offset{};
u64 romfs_size{};
u64 romfs_offset{};
FsTimeStampRaw timestamp{};
Hbini hbini{};

View File

@@ -36,6 +36,7 @@ private:
using OptionBool = OptionBase<bool>;
using OptionLong = OptionBase<long>;
using OptionFloat = OptionBase<float>;
using OptionString = OptionBase<std::string>;
} // namespace sphaira::option

View File

@@ -15,7 +15,10 @@ enum class Mode {
SingleThreadedIfSmaller,
};
using DecompressWriteCallback = std::function<Result(const void* data, s64 size)>;
using ReadCallback = std::function<Result(void* data, s64 off, s64 size, u64* bytes_read)>;
using DecompressCallback = std::function<Result(void* data, s64 off, s64 size, const DecompressWriteCallback& callback)>;
using WriteCallback = std::function<Result(const void* data, s64 off, s64 size)>;
// used for pull api
@@ -33,6 +36,7 @@ using StartCallback2 = std::function<Result(StartThreadCallback start, PullCallb
// reads data from rfunc into wfunc.
Result Transfer(ui::ProgressBox* pbox, s64 size, const ReadCallback& rfunc, const WriteCallback& wfunc, Mode mode = Mode::MultiThreaded);
Result Transfer(ui::ProgressBox* pbox, s64 size, const ReadCallback& rfunc, const DecompressCallback& dfunc, const WriteCallback& wfunc, Mode mode = Mode::MultiThreaded);
// reads data from rfunc, pull data from provided pull() callback.
Result TransferPull(ui::ProgressBox* pbox, s64 size, const ReadCallback& rfunc, const StartCallback& sfunc, Mode mode = Mode::MultiThreaded);

View File

@@ -7,7 +7,7 @@
namespace sphaira::ui::menu::fileview {
struct Menu final : MenuBase {
Menu(const fs::FsPath& path);
Menu(fs::Fs* fs, const fs::FsPath& path);
auto GetShortTitle() const -> const char* override { return "File"; };
void Update(Controller* controller, TouchInfo* touch) override;
@@ -15,8 +15,8 @@ struct Menu final : MenuBase {
void OnFocusGained() override;
private:
fs::Fs* const m_fs;
const fs::FsPath m_path;
fs::FsNativeSd m_fs{};
fs::File m_file{};
s64 m_file_size{};
s64 m_file_offset{};

View File

@@ -106,6 +106,7 @@ struct FileEntry final : FsDirectoryEntry {
bool checked_extension{}; // did we already search for an ext?
bool checked_internal_extension{}; // did we already search for an ext?
bool selected{}; // is this file selected?
bool done_stat{}; // have we checked file_size / count.
auto IsFile() const -> bool {
return type == FsDirEntryType_File;
@@ -305,9 +306,11 @@ struct FsView final : Widget {
void DisplayOptions();
void DisplayAdvancedOptions();
void MountNspFs();
void MountXciFs();
void MountZipFs();
using MountFsFunc = Result(*)(fs::Fs *fs, const fs::FsPath &path, fs::FsPath &out_path);
// using MountFsFunc = std::function<Result(fs::Fs *fs, const fs::FsPath &path, fs::FsPath &out_path)>;
using UmountFsFunc = std::function<void(const fs::FsPath &mount)>;
void MountFileFs(const MountFsFunc& mount_func, const UmountFsFunc& umount_func);
// private:
Base* m_menu{};
@@ -437,6 +440,9 @@ protected:
auto CreateFs(const FsEntry& fs_entry) -> std::shared_ptr<fs::Fs>;
private:
void Init(const std::shared_ptr<fs::Fs>& fs, const FsEntry& fs_entry, const fs::FsPath& path, bool is_custom);
protected:
static constexpr inline const char* INI_SECTION = "filebrowser";
@@ -480,7 +486,6 @@ auto IsSamePath(std::string_view a, std::string_view b) -> bool;
auto IsExtension(std::string_view ext1, std::string_view ext2) -> bool;
auto IsExtension(std::string_view ext, std::span<const std::string_view> list) -> bool;
struct FsStdioWrapper final : fs::FsStdio {
using OnExit = std::function<void(void)>;
FsStdioWrapper(const fs::FsPath& root, const OnExit& on_exit) : fs::FsStdio{true, root}, m_on_exit{on_exit} {

View File

@@ -17,7 +17,7 @@ namespace sphaira::ui::menu::game {
struct Entry {
u64 app_id{};
u8 type{};
u8 last_event{};
NacpLanguageEntry lang{};
int image{};
bool selected{};
@@ -84,7 +84,8 @@ private:
}
void DeleteGames();
void DumpGames(u32 flags);
void ExportOptions(bool to_nsz);
void DumpGames(u32 flags, bool to_nsz);
void CreateSaves(AccountUid uid);
private:
@@ -98,6 +99,10 @@ private:
bool m_is_reversed{};
bool m_dirty{};
// use for detection game card removal to force a refresh.
Event m_gc_event{};
FsEventNotifier m_gc_event_notifier{};
option::OptionLong m_sort{INI_SECTION, "sort", SortType::SortType_Updated};
option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Descending};
option::OptionLong m_layout{INI_SECTION, "layout", LayoutType::LayoutType_Grid};
@@ -160,13 +165,13 @@ struct ContentInfoEntry {
std::vector<NcmRightsId> ncm_rights_id{};
};
auto BuildNspPath(const Entry& e, const NsApplicationContentMetaStatus& status) -> fs::FsPath;
Result BuildContentEntry(const NsApplicationContentMetaStatus& status, ContentInfoEntry& out);
Result BuildNspEntry(const Entry& e, const ContentInfoEntry& info, const keys::Keys& keys, NspEntry& out);
Result BuildNspEntries(Entry& e, const title::MetaEntries& meta_entries, std::vector<NspEntry>& out);
Result BuildNspEntries(Entry& e, u32 flags, std::vector<NspEntry>& out);
auto BuildNspPath(const Entry& e, const NsApplicationContentMetaStatus& status, bool to_nsz = false) -> fs::FsPath;
Result BuildContentEntry(const NsApplicationContentMetaStatus& status, ContentInfoEntry& out, bool to_nsz = false);
Result BuildNspEntry(const Entry& e, const ContentInfoEntry& info, const keys::Keys& keys, NspEntry& out, bool to_nsz = false);
Result BuildNspEntries(Entry& e, const title::MetaEntries& meta_entries, std::vector<NspEntry>& out, bool to_nsz = false);
Result BuildNspEntries(Entry& e, u32 flags, std::vector<NspEntry>& out, bool to_nsz = false);
// dumps the array of nsp entries.
void DumpNsp(const std::vector<NspEntry>& entries);
void DumpNsp(const std::vector<NspEntry>& entries, bool to_nsz);
} // namespace sphaira::ui::menu::game

View File

@@ -91,7 +91,7 @@ private:
return GetEntry(m_index);
}
void DumpGames();
void DumpGames(bool to_nsz);
void DeleteGames();
Result ResetRequiredSystemVersion(MetaEntry& entry) const;
Result GetNcmSizeOfMetaStatus(MetaEntry& entry) const;

View File

@@ -7,7 +7,8 @@
#include <span>
#include <memory>
namespace sphaira::ui::menu::gc {
// todo: pr to libnx
extern "C" {
typedef enum {
FsGameCardPartitionRaw_None = -1,
@@ -15,6 +16,13 @@ typedef enum {
FsGameCardPartitionRaw_Secure = 1,
} FsGameCardPartitionRaw;
Result fsOpenGameCardStorage(FsStorage* out, const FsGameCardHandle* handle, FsGameCardPartitionRaw partition);
Result fsOpenGameCardDetectionEventNotifier(FsEventNotifier* out);
}
namespace sphaira::ui::menu::gc {
////////////////////////////////////////////////
// The below structs are taken from nxdumptool./
////////////////////////////////////////////////
@@ -88,6 +96,33 @@ typedef struct {
static_assert(sizeof(GameCardInitialData) == 0x200);
/// Encrypted using AES-128-CTR with the key and IV/counter from the `GameCardTitleKeyAreaEncryption` section. Assumed to be all zeroes in retail gamecards.
typedef struct {
u8 titlekey[0x10]; ///< Decrypted titlekey from the `GameCardInitialData` section.
u8 reserved[0xCF0];
} GameCardTitleKeyArea;
static_assert(sizeof(GameCardTitleKeyArea) == 0xD00);
/// Encrypted using RSA-2048-OAEP and a private OAEP key from AuthoringTool. Assumed to be all zeroes in retail gamecards.
typedef struct {
u8 titlekey_encryption_key[0x10]; ///< Used as the AES-128-CTR key for the `GameCardTitleKeyArea` section. Randomly generated during XCI creation by AuthoringTool.
u8 titlekey_encryption_iv[0x10]; ///< Used as the AES-128-CTR IV/counter for the `GameCardTitleKeyArea` section. Randomly generated during XCI creation by AuthoringTool.
u8 reserved[0xE0];
} GameCardTitleKeyAreaEncryption;
static_assert(sizeof(GameCardTitleKeyAreaEncryption) == 0x100);
/// Used to secure communications between the Lotus and the inserted gamecard.
/// Supposedly precedes the gamecard header.
typedef struct {
GameCardInitialData initial_data;
GameCardTitleKeyArea titlekey_area;
GameCardTitleKeyAreaEncryption titlekey_area_encryption;
} GameCardKeyArea;
static_assert(sizeof(GameCardKeyArea) == 0x1000);
typedef struct {
u8 maker_code; ///< GameCardUidMakerCode.
u8 version; ///< TODO: determine whether this matches GameCardVersion or not.
@@ -198,6 +233,7 @@ private:
void FreeImage();
void OnChangeIndex(s64 new_index);
Result DumpGames(u32 flags);
Result DumpXcz(u32 flags);
Result MountGcFs();
@@ -222,12 +258,12 @@ private:
FsStorage m_storage{};
// size of normal partition.
s64 m_parition_normal_size{};
s64 m_partition_normal_size{};
// size of secure partition.
s64 m_parition_secure_size{};
s64 m_partition_secure_size{};
// used size reported in the xci header.
s64 m_storage_trimmed_size{};
// total size of m_parition_normal_size + m_parition_secure_size.
// total size of m_partition_normal_size + m_partition_secure_size.
s64 m_storage_total_size{};
// reported size via rom_size in the xci header.
s64 m_storage_full_size{};

View File

@@ -71,7 +71,7 @@ private:
return m_sort.Get() >= SortType_UpdatedStar;
}
Result MountRomfsFs();
Result MountNroFs();
private:
static constexpr inline const char* INI_SECTION = "homebrew";

View File

@@ -0,0 +1,36 @@
#pragma once
#include "ui/widget.hpp"
#include "fs.hpp"
#include <vector>
namespace sphaira::ui::menu::imageview {
struct Menu final : Widget {
Menu(fs::Fs* fs, const fs::FsPath& path);
~Menu();
void Update(Controller* controller, TouchInfo* touch) override;
void Draw(NVGcontext* vg, Theme* theme) override;
auto IsMenu() const -> bool override {
return true;
}
void UpdateSize();
private:
const fs::FsPath m_path;
int m_image{};
float m_image_width{};
float m_image_height{};
// for zoom, 0.1 - 1.0
float m_zoom{1};
// for pan.
float m_xoff{};
float m_yoff{};
};
} // namespace sphaira::ui::menu::imageview

View File

@@ -87,7 +87,8 @@ private:
auto BuildSavePath(const Entry& e, bool is_auto) const -> fs::FsPath;
Result RestoreSaveInternal(ProgressBox* pbox, const Entry& e, const fs::FsPath& path) const;
Result BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& location, const Entry& e, bool compressed, bool is_auto = false) const;
Result BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& location, Entry& e, bool compressed, bool is_auto = false) const;
Result BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& location, std::span<const std::reference_wrapper<Entry>> entries, bool compressed, bool is_auto = false) const;
Result MountSaveFs();

View File

@@ -1,3 +1,4 @@
#if 0
#pragma once
#include "ui/menus/menu_base.hpp"
@@ -175,3 +176,4 @@ private:
};
} // namespace sphaira::ui::menu::themezer
#endif

View File

@@ -0,0 +1,45 @@
#pragma once
#include "ui/widget.hpp"
#include "ui/scrolling_text.hpp"
#include "fs.hpp"
#include "utils/audio.hpp"
#include <memory>
namespace sphaira::ui::music {
struct Menu final : Widget {
Menu(fs::Fs* fs, const fs::FsPath& path);
~Menu();
void Update(Controller* controller, TouchInfo* touch) override;
void Draw(NVGcontext* vg, Theme* theme) override;
private:
void PauseToggle();
void SeekForward();
void SeekBack();
void IncreaseVolume();
void DecreaseVolume();
private:
audio::SongID m_song{};
audio::Info m_info{};
audio::Meta m_meta{};
// only set if metadata was loaded.
int m_icon{};
ScrollingText m_scroll_title{};
ScrollingText m_scroll_artist{};
ScrollingText m_scroll_album{};
// from movienx
static constexpr Vec4 osd_progress_bar{400.f, 550, 1280.f - (400.f * 2.f), 10.f};
// static constexpr Vec4 osd_progress_bar{300.f, SCREEN_HEIGHT / 2 - 15 / 2, 1280.f - (300.f * 2.f), 10.f};
static constexpr Vec2 osd_time_text_left{osd_progress_bar.x - 12.f, osd_progress_bar.y - 2.f};
static constexpr Vec2 osd_time_text_right{osd_progress_bar.x + osd_progress_bar.w + 12.f, osd_progress_bar.y - 2.f};
static constexpr Vec4 osd_bar_outline{osd_time_text_left.x - 80, osd_progress_bar.y - 30, osd_progress_bar.w + 80 * 2 + 30, osd_progress_bar.h + 30 + 30};
};
} // namespace sphaira::ui::music

View File

@@ -17,8 +17,7 @@ struct ProgressBox final : Widget {
int image,
const std::string& action,
const std::string& title,
const ProgressBoxCallback& callback, const ProgressBoxDoneCallback& done = [](Result rc){},
int cpuid = 1, int prio = PRIO_PREEMPTIVE, int stack_size = 1024*128
const ProgressBoxCallback& callback, const ProgressBoxDoneCallback& done = nullptr
);
~ProgressBox();
@@ -28,6 +27,8 @@ struct ProgressBox final : Widget {
auto SetActionName(const std::string& action) -> ProgressBox&;
auto SetTitle(const std::string& title) -> ProgressBox&;
auto NewTransfer(const std::string& transfer) -> ProgressBox&;
// zeros the saved offset.
auto ResetTranfser() -> ProgressBox&;
auto UpdateTransfer(s64 offset, s64 size) -> ProgressBox&;
// not const in order to avoid copy by using std::swap
auto SetImage(int image) -> ProgressBox&;
@@ -44,10 +45,6 @@ struct ProgressBox final : Widget {
auto CopyFile(const fs::FsPath& src, const fs::FsPath& dst, bool single_threaded = false) -> Result;
void Yield();
auto GetCpuId() const {
return m_cpuid;
}
auto OnDownloadProgressCallback() {
return [this](s64 dltotal, s64 dlnow, s64 ultotal, s64 ulnow){
if (this->ShouldExit()) {
@@ -103,7 +100,6 @@ private:
ScrollingText m_scroll_title{};
ScrollingText m_scroll_transfer{};
int m_cpuid{};
int m_image{};
bool m_own_image{};
};

View File

@@ -91,6 +91,25 @@ private:
std::string m_false_str;
};
class SidebarEntrySlider final : public SidebarEntryBase {
public:
using Callback = std::function<void(float&)>;
public:
explicit SidebarEntrySlider(const std::string& title, float value, float min, float max, int steps, const Callback& cb, const std::string& info = "");
void Draw(NVGcontext* vg, Theme* theme, const Vec4& root_pos, bool left) override;
private:
float m_value;
float m_min;
float m_max;
int m_steps;
Callback m_callback;
float m_duration;
float m_inc;
};
class SidebarEntryCallback final : public SidebarEntryBase {
public:
using Callback = std::function<void()>;

View File

@@ -1,7 +1,6 @@
#pragma once
#include "nanovg.h"
#include "pulsar.h"
#include "fs.hpp"
#include <switch.h>
@@ -229,6 +228,7 @@ struct ThemeMeta {
struct Theme {
ThemeMeta meta;
ElementEntry elements[ThemeEntryID_MAX];
fs::FsPath music_path;
auto GetColour(ThemeEntryID id) const {
return elements[id].colour;
@@ -291,11 +291,13 @@ enum class Button : u64 {
LS_RIGHT = static_cast<u64>(HidNpadButton_StickLRight),
LS_UP = static_cast<u64>(HidNpadButton_StickLUp),
LS_DOWN = static_cast<u64>(HidNpadButton_StickLDown),
LS_ANY = LS_LEFT | LS_RIGHT | LS_UP | LS_DOWN,
RS_LEFT = static_cast<u64>(HidNpadButton_StickRLeft),
RS_RIGHT = static_cast<u64>(HidNpadButton_StickRRight),
RS_UP = static_cast<u64>(HidNpadButton_StickRUp),
RS_DOWN = static_cast<u64>(HidNpadButton_StickRDown),
RS_ANY = RS_LEFT | RS_RIGHT | RS_UP | RS_DOWN,
ANY_LEFT = static_cast<u64>(HidNpadButton_AnyLeft),
ANY_RIGHT = static_cast<u64>(HidNpadButton_AnyRight),

View File

@@ -1,57 +0,0 @@
#pragma once
#include <switch.h>
namespace sphaira::usb::tinfoil {
enum Magic : u32 {
Magic_List0 = 0x304C5554, // TUL0 (Tinfoil Usb List 0)
Magic_Command0 = 0x30435554, // TUC0 (Tinfoil USB Command 0)
};
enum USBCmdType : u8 {
REQUEST = 0,
RESPONSE = 1
};
enum USBCmdId : u32 {
EXIT = 0,
FILE_RANGE = 1
};
// extension flags for sphaira.
enum USBFlag : u8 {
USBFlag_NONE = 0,
// stream install, does not allow for random access.
// allows the upload to be multi threaded., do not modify!
// the order of the file list must be kept as-is.
USBFlag_STREAM = 1 << 0,
};
struct TUSHeader {
u32 magic; // TUL0 (Tinfoil Usb List 0)
u32 nspListSize;
u8 flags;
u8 padding[0x7];
};
struct NX_PACKED USBCmdHeader {
u32 magic; // TUC0 (Tinfoil USB Command 0)
USBCmdType type;
u8 padding[0x3];
u32 cmdId;
u64 dataSize;
u8 reserved[0xC];
};
struct FileRangeCmdHeader {
u64 size;
u64 offset;
u64 nspNameLen;
u64 padding;
};
static_assert(sizeof(TUSHeader) == 0x10, "TUSHeader must be 0x10!");
static_assert(sizeof(USBCmdHeader) == 0x20, "USBCmdHeader must be 0x20!");
} // namespace sphaira::usb::tinfoil

View File

@@ -0,0 +1,64 @@
#pragma once
#include <switch.h>
#include "defines.hpp"
namespace sphaira::usb::api {
enum : u32 {
MAGIC = 0x53504830,
PACKET_SIZE = 16,
};
enum : u32 {
CMD_QUIT = 0,
CMD_OPEN = 1,
CMD_EXPORT = 1,
};
enum : u32 {
RESULT_OK = 0,
RESULT_ERROR = 1,
};
enum : u32 {
FLAG_NONE = 0,
FLAG_STREAM = 1 << 0,
};
struct SendHeader {
u32 magic;
u32 cmd;
u32 arg3;
u32 arg4;
Result Verify() const {
R_UNLESS(magic == MAGIC, Result_UsbBadMagic);
R_SUCCEED();
}
};
struct ResultHeader {
u32 magic;
u32 result;
u32 arg3;
u32 arg4;
Result Verify() const {
R_UNLESS(magic == MAGIC, Result_UsbBadMagic);
R_UNLESS(result == RESULT_OK, 1); // todo: create error code.
R_SUCCEED();
}
};
struct SendDataHeader {
u64 offset;
u32 size;
u32 crc32c;
};
static_assert(sizeof(SendHeader) == PACKET_SIZE);
static_assert(sizeof(ResultHeader) == PACKET_SIZE);
static_assert(sizeof(SendDataHeader) == PACKET_SIZE);
} // namespace sphaira::usb::api

View File

@@ -0,0 +1,37 @@
#pragma once
#include "usb/usbds.hpp"
#include "usb/usb_api.hpp"
#include <vector>
#include <memory>
#include <string_view>
#include <switch.h>
namespace sphaira::usb::dump {
struct Usb {
Usb(u64 transfer_timeout);
~Usb();
Result Write(const void* buf, u64 off, u32 size);
void SignalCancel();
// waits for connection and then sends file list.
Result IsUsbConnected(u64 timeout);
Result WaitForConnection(std::string_view path, u64 timeout);
// Result OpenFile(u32 index, s64& file_size);
Result CloseFile();
private:
Result SendAndVerify(const void* data, u32 size, u64 timeout, api::ResultHeader* out = nullptr);
Result SendAndVerify(const void* data, u32 size, api::ResultHeader* out = nullptr);
private:
std::unique_ptr<usb::UsbDs> m_usb{};
Result m_open_result{};
bool m_was_connected{};
};
} // namespace sphaira::usb::dumpl

View File

@@ -0,0 +1,39 @@
#pragma once
#include "usb/usbds.hpp"
#include "usb/usb_api.hpp"
#include <string>
#include <vector>
#include <memory>
#include <switch.h>
namespace sphaira::usb::install {
struct Usb {
Usb(u64 transfer_timeout);
~Usb();
Result Read(void* buf, u64 off, u32 size, u64* bytes_read);
u32 GetFlags() const;
void SignalCancel();
// waits for connection and then sends file list.
Result IsUsbConnected(u64 timeout);
Result WaitForConnection(u64 timeout, std::vector<std::string>& names);
Result OpenFile(u32 index, s64& file_size);
Result CloseFile();
private:
Result SendAndVerify(const void* data, u32 size, u64 timeout, api::ResultHeader* out = nullptr);
Result SendAndVerify(const void* data, u32 size, api::ResultHeader* out = nullptr);
private:
std::unique_ptr<usb::UsbDs> m_usb{};
Result m_open_result{};
bool m_was_connected{};
u32 m_flags{};
};
} // namespace sphaira::usb::install

View File

@@ -13,25 +13,30 @@ struct Usb {
Usb(u64 transfer_timeout);
virtual ~Usb();
virtual Result Read(const std::string& path, void* buf, s64 off, s64 size, u64* bytes_read) = 0;
virtual Result Read(void* buf, u64 off, u32 size, u64* bytes_read) = 0;
virtual Result Open(u32 index, s64& out_size, u16& out_flags) = 0;
Result IsUsbConnected(u64 timeout) {
return m_usb->IsUsbConnected(timeout);
}
// waits for connection and then sends file list.
Result WaitForConnection(u64 timeout, u8 flags, std::span<const std::string> names);
Result WaitForConnection(u64 timeout, std::span<const std::string> names);
// polls for command, executes transfer if possible.
// will return Result_Exit if exit command is recieved.
Result PollCommands();
private:
Result FileRangeCmd(u64 data_size);
Result file_transfer_loop();
private:
std::unique_ptr<usb::UsbHs> m_usb;
std::vector<u8> m_buf;
Result SendResult(u32 result, u32 arg3 = 0, u32 arg4 = 0);
private:
std::unique_ptr<usb::UsbHs> m_usb{};
std::vector<u8> m_buf{};
Result m_open_result{};
bool m_was_connected{};
};
} // namespace sphaira::usb::upload

View File

@@ -2,11 +2,6 @@
#include "base.hpp"
// TODO: remove these when libnx pr is merged.
enum { UsbDeviceSpeed_None = 0x0 };
enum { UsbDeviceSpeed_Low = 0x1 };
Result usbDsGetSpeed(UsbDeviceSpeed *out);
auto GetUsbDsStateStr(UsbState state) -> const char*;
auto GetUsbDsSpeedStr(UsbDeviceSpeed speed) -> const char*;

View File

@@ -0,0 +1,13 @@
#pragma once
#include <switch.h>
#include "location.hpp"
namespace sphaira::usbdvd {
Result MountAll();
void UnmountAll();
bool GetMountPoint(location::StdioEntry& out);
} // namespace sphaira::usbdvd

View File

@@ -0,0 +1,81 @@
#pragma once
#include "fs.hpp"
#include "image.hpp"
#include <string>
#include <memory>
namespace sphaira::audio {
enum class State {
Free, // private use.
Playing, // song is playing.
Paused, // song has paused.
Finished, // song has finished.
Error, // error in playback.
};
struct Progress {
u64 played;
};
struct Info {
u64 sample_count;
u32 sample_rate;
u32 channels;
u32 loop_start;
bool looping;
};
struct Meta {
std::string title{};
std::string album{};
std::string artist{};
std::vector<u8> image{};
};
enum class SoundEffect {
Focus,
Scroll,
Limit,
Startup,
Install,
Error,
MAX,
};
enum Flag {
Flag_None = 0,
// plays the song for ever.
Flag_Loop = 1 << 0,
};
using SongID = void*;
Result Init();
void ExitSignal();
void Exit();
Result PlaySoundEffect(SoundEffect effect);
Result OpenSong(fs::Fs* fs, const fs::FsPath& path, u32 flags, SongID* id);
Result CloseSong(SongID* id);
Result PlaySong(SongID id);
Result PauseSong(SongID id);
Result SeekSong(SongID id, u64 target);
// todo:
// 0.0 -> 2.0.
Result GetVolumeSong(SongID id, float* out);
Result SetVolumeSong(SongID id, float in);
// todo:
Result GetPitchSong(SongID id, float* out);
Result SetPitchSong(SongID id, float in);
Result GetInfo(SongID id, Info* out);
Result GetMeta(SongID id, Meta* out);
Result GetProgress(SongID id, Progress* out_progress, State* out_state);
} // namespace sphaira::audio

View File

@@ -1,15 +1,17 @@
#pragma once
#include <switch.h>
#include "fs.hpp"
#include "yati/source/base.hpp"
#include <switch.h>
#include <memory>
namespace sphaira::devoptab {
// mounts to "lower_case_hex_id:/"
Result MountFromSavePath(u64 id, fs::FsPath& out_path);
Result MountSaveSystem(u64 id, fs::FsPath& out_path);
void UnmountSave(u64 id);
// todo:
Result MountZip(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path);
void UmountZip(const fs::FsPath& mount);
@@ -17,6 +19,17 @@ Result MountNsp(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path);
void UmountNsp(const fs::FsPath& mount);
Result MountXci(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path);
Result MountXciSource(const std::shared_ptr<sphaira::yati::source::Base>& source, s64 size, const fs::FsPath& path, fs::FsPath& out_path);
void UmountXci(const fs::FsPath& mount);
Result MountNca(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path);
Result MountNcaNcm(NcmContentStorage* cs, const NcmContentId* id, fs::FsPath& out_path);
void UmountNca(const fs::FsPath& mount);
Result MountBfsar(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path);
void UmountBfsar(const fs::FsPath& mount);
Result MountNro(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path);
void UmountNro(const fs::FsPath& mount);
} // namespace sphaira::devoptab

View File

@@ -1,35 +1,84 @@
#pragma once
#include "yati/source/file.hpp"
#include "utils/lru.hpp"
#include <memory>
#include <span>
namespace sphaira::devoptab::common {
// max entries per devoptab, should be enough.
enum { MAX_ENTRIES = 4 };
// buffers data in 512k chunks to maximise throughput.
// not suitable if random access >= 512k is common.
// if that is needed, see the LRU cache varient used for fatfs.
struct BufferedData final : yati::source::Base {
static constexpr inline u64 CHUNK_SIZE = 1024 * 512;
BufferedData(std::unique_ptr<yati::source::Base>&& _source, u64 _size)
: source{std::forward<decltype(_source)>(_source)}
struct BufferedDataBase : yati::source::Base {
BufferedDataBase(const std::shared_ptr<yati::source::Base>& _source, u64 _size)
: source{_source}
, capacity{_size} {
}
Result Read(void *buf, s64 off, s64 size);
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override;
virtual Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override {
return source->Read(buf, off, size, bytes_read);
}
protected:
std::shared_ptr<yati::source::Base> source;
const u64 capacity;
};
// buffers data in 512k chunks to maximise throughput.
// not suitable if random access >= 512k is common.
// if that is needed, see the LRU cache varient used for fatfs.
struct BufferedData : BufferedDataBase {
BufferedData(const std::shared_ptr<yati::source::Base>& _source, u64 _size, u64 _alloc = 1024 * 512)
: BufferedDataBase{_source, _size} {
m_data.resize(_alloc);
}
virtual Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override;
private:
std::unique_ptr<yati::source::Base> source;
const u64 capacity;
u64 m_off{};
u64 m_size{};
u8 m_data[CHUNK_SIZE]{};
std::vector<u8> m_data{};
};
struct BufferedFileData {
u8* data{};
u64 off{};
u64 size{};
~BufferedFileData() {
if (data) {
free(data);
}
}
void Allocate(u64 new_size) {
data = (u8*)realloc(data, new_size * sizeof(*data));
off = 0;
size = 0;
}
};
constexpr u64 CACHE_LARGE_ALLOC_SIZE = 1024 * 512;
constexpr u64 CACHE_LARGE_SIZE = 1024 * 16;
struct LruBufferedData : BufferedDataBase {
LruBufferedData(const std::shared_ptr<yati::source::Base>& _source, u64 _size, u32 small = 1024, u32 large = 2)
: BufferedDataBase{_source, _size} {
buffered_small.resize(small);
buffered_large.resize(large);
lru_cache[0].Init(buffered_small);
lru_cache[1].Init(buffered_large);
}
virtual Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override;
private:
utils::Lru<BufferedFileData> lru_cache[2]{};
std::vector<BufferedFileData> buffered_small{}; // 1MiB (usually).
std::vector<BufferedFileData> buffered_large{}; // 1MiB
};
bool fix_path(const char* str, char* out);

View File

@@ -0,0 +1,41 @@
#pragma once
#include "yati/source/base.hpp"
#include <memory>
#include <span>
#include <vector>
#include <string_view>
#include <sys/stat.h>
namespace sphaira::devoptab::romfs {
struct RomfsCollection {
romfs_header header;
std::vector<u8> dir_table;
std::vector<u8> file_table;
u64 offset;
};
struct FileEntry {
const romfs_file* romfs;
u64 offset;
u64 size;
};
struct DirEntry {
const RomfsCollection* romfs_collection;
const romfs_dir* romfs_root; // start of the dir.
u32 romfs_childDir;
u32 romfs_childFile;
};
bool find_file(const RomfsCollection& romfs, std::string_view path, FileEntry& out);
bool find_dir(const RomfsCollection& romfs, std::string_view path, DirEntry& out);
// helper
void dirreset(DirEntry& entry);
bool dirnext(DirEntry& entry, char* filename, struct stat *filestat);
Result LoadRomfsCollection(yati::source::Base* source, u64 offset, RomfsCollection& out);
} // namespace sphaira::devoptab::romfs

View File

@@ -0,0 +1,76 @@
#pragma once
#include <vector>
#include <span>
namespace sphaira::utils {
template<typename T>
struct LinkedList {
T* data;
LinkedList* next;
LinkedList* prev;
};
template<typename T>
struct Lru {
using ListEntry = LinkedList<T>;
// pass span of the data.
void Init(std::span<T> data) {
list_flat_array.clear();
list_flat_array.resize(data.size());
auto list_entry = list_head = list_flat_array.data();
for (size_t i = 0; i < data.size(); i++) {
list_entry = list_flat_array.data() + i;
list_entry->data = data.data() + i;
if (i + 1 < data.size()) {
list_entry->next = &list_flat_array[i + 1];
}
if (i) {
list_entry->prev = &list_flat_array[i - 1];
}
}
list_tail = list_entry->prev->next;
}
// moves entry to the front of the list.
void Update(ListEntry* entry) {
// only update position if we are not the head.
if (list_head != entry) {
entry->prev->next = entry->next;
if (entry->next) {
entry->next->prev = entry->prev;
} else {
list_tail = entry->prev;
}
// update head.
auto head_temp = list_head;
list_head = entry;
list_head->prev = nullptr;
list_head->next = head_temp;
head_temp->prev = list_head;
}
}
// moves last entry (tail) to the front of the list.
auto GetNextFree() {
Update(list_tail);
return list_head->data;
}
auto begin() const { return list_head; }
auto end() const { return list_tail; }
private:
ListEntry* list_head{};
ListEntry* list_tail{};
std::vector<ListEntry> list_flat_array{};
};
} // namespace sphaira::utils

View File

@@ -0,0 +1,23 @@
#pragma once
#include "fs.hpp"
#include "defines.hpp"
#include "dumper.hpp"
#include "ui/progress_box.hpp"
#include "yati/nx/keys.hpp"
#include "yati/nx/nca.hpp"
#include "yati/container/base.hpp"
#include <functional>
namespace sphaira::utils::nsz {
using Collection = yati::container::CollectionEntry;
using Collections = yati::container::Collections;
using NcaReaderCreator = std::function<std::unique_ptr<nca::NcaReader>(const nca::Header& header, const keys::KeyEntry& title_key, const Collection& collection)>;
Result NszExport(ui::ProgressBox* pbox, const NcaReaderCreator& nca_creator, s64& read_offset, s64& write_offset, Collections& collections, const keys::Keys& keys, dump::BaseSource* source, dump::WriteSource* writer, const fs::FsPath& path);
} // namespace sphaira::utils::nsz

View File

@@ -5,12 +5,16 @@
namespace sphaira::utils {
struct ScopedTimestampProfile {
struct ScopedTimestampProfile final {
ScopedTimestampProfile(const std::string& name) : m_name{name} {
}
~ScopedTimestampProfile() {
Log();
}
void Log() {
log_write("\t[%s] time taken: %.2fs %.2fms\n", m_name.c_str(), m_ts.GetSecondsD(), m_ts.GetMsD());
}

View File

@@ -0,0 +1,58 @@
#pragma once
#include "defines.hpp"
#include <functional>
#include <atomic>
namespace sphaira::utils {
static inline Result CreateThread(Thread *t, ThreadFunc entry, void *arg, size_t stack_sz = 1024*128, int prio = 0x3B) {
u64 core_mask = 0;
R_TRY(svcGetInfo(&core_mask, InfoType_CoreMask, CUR_PROCESS_HANDLE, 0));
R_TRY(threadCreate(t, entry, arg, nullptr, stack_sz, prio, -2));
R_TRY(svcSetThreadCoreMask(t->handle, -1, core_mask));
R_SUCCEED();
}
struct Async final {
using Callback = std::function<void(void)>;
// core0=main, core1=audio, core2=servers (ftp,mtp,nxlink)
Async(Callback&& callback) : m_callback{std::forward<Callback>(callback)} {
m_running = true;
if (R_FAILED(CreateThread(&m_thread, thread_func, &m_callback))) {
m_running = false;
return;
}
if (R_FAILED(threadStart(&m_thread))) {
threadClose(&m_thread);
m_running = false;
}
}
~Async() {
WaitForExit();
}
void WaitForExit() {
if (m_running) {
threadWaitForExit(&m_thread);
threadClose(&m_thread);
m_running = false;
}
}
private:
static void thread_func(void* arg) {
(*static_cast<Callback*>(arg))();
}
private:
Callback m_callback;
Thread m_thread{};
std::atomic_bool m_running{};
};
} // namespace sphaira::utils

View File

@@ -0,0 +1,25 @@
#pragma once
#include "ui/types.hpp"
namespace sphaira::utils {
struct HashStr {
char str[0x21];
};
HashStr hexIdToStr(FsRightsId id);
HashStr hexIdToStr(NcmRightsId id);
HashStr hexIdToStr(NcmContentId id);
template<typename T>
constexpr inline T AlignUp(T value, T align) {
return (value + (align - 1)) &~ (align - 1);
}
template<typename T>
constexpr inline T AlignDown(T value, T align) {
return value &~ (align - 1);
}
} // namespace sphaira::utils

View File

@@ -9,9 +9,10 @@ namespace sphaira::yati::container {
struct Nsp final : Base {
using Base::Base;
Result GetCollections(Collections& out) override;
Result GetCollections(Collections& out, s64 off);
// builds nsp meta data and the size of the entier nsp.
static auto Build(std::span<CollectionEntry> collections, s64& size) -> std::vector<u8>;
static auto Build(std::span<const CollectionEntry> collections, s64& size) -> std::vector<u8>;
};
} // namespace sphaira::yati::container

View File

@@ -9,18 +9,62 @@ namespace sphaira::yati::container {
struct Xci final : Base {
struct Hfs0Header {
u32 magic;
u32 total_files;
u32 string_table_size;
u32 padding;
};
struct Hfs0FileTableEntry {
u64 data_offset;
u64 data_size;
u32 name_offset;
u32 hash_size;
u64 padding;
u8 hash[0x20];
};
struct Hfs0 {
Hfs0Header header{};
std::vector<Hfs0FileTableEntry> file_table{};
std::vector<std::string> string_table{};
s64 data_offset{};
auto GetHfs0Size() const {
return sizeof(header) + file_table.size() * sizeof(Hfs0FileTableEntry) + header.string_table_size;
}
auto GetHfs0Data() const -> std::vector<u8>;
};
struct Partition {
// name of the partition.
std::string name;
// offset of this hfs0.
s64 hfs0_offset;
s64 hfs0_size;
Hfs0 hfs0;
// all the collections for this partition, may be empty.
Collections collections;
};
struct Root {
// offset of this hfs0.
s64 hfs0_offset;
Hfs0 hfs0;
std::vector<Partition> partitions;
};
using Partitions = std::vector<Partition>;
using Base::Base;
Result GetCollections(Collections& out) override;
Result GetPartitions(Partitions& out);
Result GetRoot(Root& out);
private:
Result Hfs0GetPartition(source::Base* source, s64 off, Hfs0& out);
Result ReadPartitionFromHfs0(source::Base* source, const Hfs0& root, u32 index, Partition& out);
};
} // namespace sphaira::yati::container

View File

@@ -54,4 +54,15 @@ static inline void cryptoAes128Xts(const void* in, void* out, const u8* key, u64
Aes128Xts(key, is_encryptor).Run(out, in, sector, sector_size, data_size);
}
static inline void UpdateCtr(u8* counter, u64 offset) {
const u64 swp = __bswap64(offset >> 4);
std::memcpy(&counter[0x8], &swp, 0x8);
}
static inline void SetCtr(u8* counter, u64 ctr, u64 offset = 0) {
const u64 swp = __bswap64(ctr);
std::memcpy(&counter[0x0], &swp, 0x8);
UpdateCtr(counter, offset);
}
} // namespace sphaira::crypto

View File

@@ -201,7 +201,8 @@ Result EncryptTitleKey(keys::KeyEntry& out, u8 key_gen, const keys::Keys& keys);
Result ShouldPatchTicket(const TicketData& data, std::span<const u8> ticket, std::span<const u8> cert_chain, bool patch_personalised, bool& should_patch);
Result ShouldPatchTicket(std::span<const u8> ticket, std::span<const u8> cert_chain, bool patch_personalised, bool& should_patch);
Result PatchTicket(std::vector<u8>& ticket, std::span<const u8> cert_chain, u8 key_gen, const keys::Keys& keys, bool patch_personalised);
// cert chain may be modified if the ticket is converted to common ticket.
Result PatchTicket(std::vector<u8>& ticket, std::vector<u8>& cert_chain, u8 key_gen, const keys::Keys& keys, bool patch_personalised);
// fills out with the list of common / personalised rights ids.
Result GetCommonTickets(std::vector<FsRightsId>& out);
@@ -218,6 +219,11 @@ bool IsRightsIdFound(const FsRightsId& id, std::span<const FsRightsId> ids);
// wrapper around ipc.
Result GetCommonTicketAndCertificate(const FsRightsId& rights_id, std::vector<u8>& tik_out, std::vector<u8>& cert_out);
// fetches data from system es save.
Result GetPersonalisedTicketData(u64 *size_out, void *tik_data, u64 tik_size, const FsRightsId* rightsId);
Result GetPersonalisedTicketAndCertificate(const FsRightsId& rights_id, std::vector<u8>& tik_out, std::vector<u8>& cert_out);
// fills out with the decrypted title key.
Result GetTitleKeyDecrypted(const FsRightsId& rights_id, u8 key_gen, const keys::Keys& keys, keys::KeyEntry& out);
Result GetTitleKeyDecrypted(std::span<const u8> ticket, const FsRightsId& rights_id, u8 key_gen, const keys::Keys& keys, keys::KeyEntry& out);
} // namespace sphaira::es

View File

@@ -3,8 +3,11 @@
#include "fs.hpp"
#include "keys.hpp"
#include "ncm.hpp"
#include "yati/source/base.hpp"
#include <switch.h>
#include <vector>
#include <memory>
namespace sphaira::nca {
@@ -15,7 +18,7 @@ namespace sphaira::nca {
#define NCA_SECTOR_SIZE 0x200
#define NCA_XTS_SECTION_SIZE 0xC00
#define NCA_SECTION_TOTAL 0x4
#define NCA_MEDIA_REAL(x)((x * 0x200))
#define NCA_MEDIA_REAL(x)((u64(x) * 0x200))
#define NCA_PROGRAM_LOGO_OFFSET 0x8000
#define NCA_META_CNMT_OFFSET 0xC20
@@ -94,6 +97,22 @@ struct SectionTableEntry {
u32 media_end_offset; // divided by 0x200.
u8 _0x8[0x4]; // unknown.
u8 _0xC[0x4]; // unknown.
auto IsValid() const -> bool {
return media_start_offset && media_end_offset;
}
auto GetOffset() const -> u64 {
return NCA_MEDIA_REAL(media_start_offset);
}
auto GetOffsetEnd() const -> u64 {
return NCA_MEDIA_REAL(media_end_offset);
}
auto GetSize() const -> u64 {
return GetOffsetEnd() - GetOffset();
}
};
struct LayerRegion {
@@ -139,6 +158,55 @@ static_assert(sizeof(HierarchicalSha256Data) == 0xF8);
static_assert(sizeof(IntegrityMetaInfo) == 0xF8);
static_assert(sizeof(HierarchicalSha256Data) == sizeof(IntegrityMetaInfo));
struct BucketTreeHeader {
u32 magic; // BKTR
u32 version;
u32 count;
u8 _0xC[0x4];
};
struct PatchInfo {
u64 indirect_offset;
u64 indirect_size;
BucketTreeHeader indirect_header;
u64 aes_ctr_offset;
u64 aes_ctr_size;
BucketTreeHeader aes_ctr_header;
};
static_assert(sizeof(PatchInfo) == 0x40);
struct CompressionInfo {
u64 table_offset;
u64 table_size;
BucketTreeHeader table_header;
u8 _0x20[0x8];
};
static_assert(sizeof(CompressionInfo) == 0x28);
struct BktrEntry {
u8 _0x0[0x4];
u32 count;
u64 size;
u64 offsets[0x3FF0 / sizeof(u64)];
};
static_assert(sizeof(BktrEntry) == 0x4000);
struct NX_PACKED BktrRelocationEntry {
u64 patched_addr;
u64 source_addr;
u32 flag;
};
static_assert(sizeof(BktrRelocationEntry) == 0x14);
struct BktrRelocationBucket {
u8 _0x0[0x4];
u32 count;
u64 end_offset;
BktrRelocationEntry entries[0x3FF0 / sizeof(BktrRelocationEntry)];
u8 _[0x3FF0 % sizeof(BktrRelocationEntry)];
};
static_assert(sizeof(BktrRelocationBucket) == 0x4000);
struct FsHeader {
u16 version; // always 2.
u8 fs_type; // see FileSystemType.
@@ -152,12 +220,16 @@ struct FsHeader {
IntegrityMetaInfo integrity_meta_info; // used for romfs
} hash_data;
u8 patch_info[0x40];
PatchInfo patch_info;
u64 section_ctr;
u8 spares_info[0x30];
u8 compression_info[0x28];
CompressionInfo compression_info;
u8 meta_data_hash_data_info[0x30];
u8 reserved[0x30];
auto IsValid() const -> bool {
return version == 2;
}
};
static_assert(sizeof(FsHeader) == 0x200);
static_assert(sizeof(FsHeader::hash_data) == 0xF8);
@@ -203,6 +275,10 @@ struct Header {
FsHeader fs_header[NCA_SECTION_TOTAL];
auto IsValid() const -> bool {
return magic == NCA3_MAGIC;
}
auto GetKeyGeneration() const -> u8 {
if (old_key_gen < key_gen) {
return key_gen;
@@ -220,12 +296,25 @@ struct Header {
key_gen = key_generation;
}
}
auto GetSectionCount() const -> u8 {
u8 count = 0;
for (u32 i = 0; i < NCA_SECTION_TOTAL; i++) {
if (!fs_header[i].IsValid() || !fs_table[i].IsValid()) {
break;
}
count++;
}
return count;
}
};
static_assert(sizeof(Header) == 0xC00);
auto GetContentTypeStr(u8 content_type) -> const char*;
auto GetDistributionTypeStr(u8 distribution_type) -> const char*;
Result DecryptHeader(const void* in, const keys::Keys& keys, Header& out);
Result DecryptKeak(const keys::Keys& keys, Header& header);
Result EncryptKeak(const keys::Keys& keys, Header& header, u8 key_generation);
Result VerifyFixedKey(const Header& header);
@@ -236,4 +325,52 @@ Result ParseControl(const fs::FsPath& path, u64 program_id, void* nacp_out = nul
auto GetKeyGenStr(u8 key_gen) -> const char*;
// finds and decrypts the title key, also decrypts header key area if needed.
Result GetDecryptedTitleKey(Header& header, const keys::Keys& keys, keys::KeyEntry& out);
// same as above but also checks the path for ticket.
Result GetDecryptedTitleKey(fs::Fs* fs, const fs::FsPath& path, Header& header, const keys::Keys& keys, keys::KeyEntry& out);
// helpers.
struct DecyptedData : yati::source::Base {
DecyptedData(u64 align, const std::shared_ptr<yati::source::Base>& source);
Result Read(void *_buf, s64 _off, s64 _size, u64* _bytes_read) override;
virtual Result SetCtr(u64 ctr) = 0;
private:
virtual Result Decrypt(void* buf, s64 off, s64 size) = 0;
private:
std::shared_ptr<yati::source::Base> m_source;
const u64 m_align;
};
// todo: add support for xts sections.
struct DecyptedDataCtr final : DecyptedData {
DecyptedDataCtr(const void* key, u64 ctr, const std::shared_ptr<yati::source::Base>& source);
Result SetCtr(u64 ctr) override;
private:
Result Decrypt(void* buf, s64 off, s64 size) override;
private:
Aes128CtrContext m_ctx{};
u8 m_ctr[AES_BLOCK_SIZE]{};
};
struct NcaReader final : yati::source::Base {
NcaReader(const nca::Header& decrypted_header, const void* key, u64 size, const std::shared_ptr<yati::source::Base>& source);
Result Read(void *_buf, s64 off, s64 size, u64* bytes_read) override;
Result ReadEncrypted(void *_buf, s64 off, s64 size, u64* bytes_read);
private:
Result ReadInternal(void *_buf, s64 off, s64 size, u64* bytes_read, bool decrypt);
private:
const nca::Header m_header;
const u64 m_capacity;
std::shared_ptr<yati::source::Base> m_source;
std::unique_ptr<DecyptedData> m_decryptor{};
u8 m_key[0x10]{};
};
} // namespace sphaira::nca

View File

@@ -1,6 +1,7 @@
#pragma once
#include "fs.hpp"
#include "yati/source/base.hpp"
#include <switch.h>
#include <vector>
@@ -82,4 +83,16 @@ static constexpr inline bool HasRequiredSystemVersion(const NcmContentMetaKey *k
// fills program id and out path of the control nca.
Result GetFsPathFromContentId(NcmContentStorage* cs, const NcmContentMetaKey& key, const NcmContentId& id, u64* out_program_id, fs::FsPath* out_path);
// helper for reading nca from ncm.
struct NcmSource final : yati::source::Base {
NcmSource(NcmContentStorage* cs, const NcmContentId* id);
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override;
Result GetSize(s64* size);
private:
NcmContentStorage m_cs;
NcmContentId m_id;
s64 m_size{};
};
} // namespace sphaira::ncm

View File

@@ -1,6 +1,13 @@
#pragma once
#include "yati/source/base.hpp"
#include "utils/lru.hpp"
#include "defines.hpp"
#include <switch.h>
#include <vector>
#include <memory>
#include <zstd.h>
namespace sphaira::ncz {
@@ -8,7 +15,11 @@ namespace sphaira::ncz {
// todo: byteswap this
#define NCZ_BLOCK_MAGIC std::byteswap(0x4E435A424C4F434BUL)
#define NCZ_SECTION_OFFSET (0x4000 + sizeof(ncz::Header))
#define NCZ_BLOCK_VERSION (2)
#define NCZ_BLOCK_TYPE (1)
#define NCZ_NORMAL_SIZE (0x4000)
#define NCZ_SECTION_OFFSET (NCZ_NORMAL_SIZE + sizeof(ncz::Header))
struct Header {
u64 magic; // NCZ_SECTION_MAGIC
@@ -23,11 +34,21 @@ struct BlockHeader {
u8 block_size_exponent;
u32 total_blocks;
u64 decompressed_size;
Result IsValid() const {
R_UNLESS(magic == NCZ_BLOCK_MAGIC, 9);
R_UNLESS(version == NCZ_BLOCK_VERSION, Result_YatiInvalidNczBlockVersion);
R_UNLESS(type == NCZ_BLOCK_TYPE, Result_YatiInvalidNczBlockType);
R_UNLESS(total_blocks, Result_YatiInvalidNczBlockTotal);
R_UNLESS(block_size_exponent >= 14 && block_size_exponent <= 32, Result_YatiInvalidNczBlockSizeExponent);
R_SUCCEED();
}
};
struct Block {
u32 size;
};
using Blocks = std::vector<Block>;
struct BlockInfo {
u64 offset; // compressed offset.
@@ -50,5 +71,39 @@ struct Section {
return off < offset + size && off >= offset;
}
};
using Sections = std::vector<Section>;
struct NczBlockReader final : yati::source::Base {
explicit NczBlockReader(const Header& header, const Sections& sections, const BlockHeader& block_header, const Blocks& blocks, u64 offset, const std::shared_ptr<yati::source::Base>& source);
Result Read(void *_buf, s64 off, s64 size, u64* bytes_read) override;
private:
struct LruData {
s64 offset{};
std::vector<u8> data{};
auto InRange(u64 off) const -> bool {
return off < offset + data.size() && off >= offset;
}
};
private:
Result ReadInternal(void *_buf, s64 off, s64 size, u64* bytes_read, bool decrypt);
private:
const Header m_header;
const Sections m_sections;
const BlockHeader m_block_header;
const Blocks m_blocks;
const u64 m_block_offset;
std::shared_ptr<yati::source::Base> m_source;
u32 m_block_size{};
std::vector<BlockInfo> m_block_infos{};
// lru cache of blocks
std::vector<LruData> m_lru_data{};
utils::Lru<LruData> m_lru{};
};
} // namespace sphaira::ncz

View File

@@ -10,6 +10,11 @@ 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;
Result Read2(void* buf, s64 off, s64 size) {
u64 bytes_read;
return Read(buf, off, size, &bytes_read);
}
virtual bool IsStream() const {
return false;
}

View File

@@ -1,42 +1,50 @@
#pragma once
#include "base.hpp"
#include "fs.hpp"
#include "usb/usbds.hpp"
#include "usb/usb_installer.hpp"
#include <string>
#include <vector>
#include <memory>
#include <switch.h>
namespace sphaira::yati::source {
struct Usb final : Base {
Usb(u64 transfer_timeout);
~Usb();
Usb(u64 transfer_timeout) {
m_usb = std::make_unique<usb::install::Usb>(transfer_timeout);
}
bool IsStream() const override;
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override;
Result Finished(u64 timeout);
void SignalCancel() override {
m_usb->SignalCancel();
}
bool IsStream() const override {
return m_usb->GetFlags() & usb::api::FLAG_STREAM;
}
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override {
return m_usb->Read(buf, off, size, bytes_read);
}
Result IsUsbConnected(u64 timeout) {
return m_usb->IsUsbConnected(timeout);
}
Result WaitForConnection(u64 timeout, std::vector<std::string>& out_names);
void SetFileNameForTranfser(const std::string& name);
Result WaitForConnection(u64 timeout, std::vector<std::string>& names) {
return m_usb->WaitForConnection(timeout, names);
}
void SignalCancel() override {
m_usb->Cancel();
Result OpenFile(u32 index, s64& file_size) {
return m_usb->OpenFile(index, file_size);
}
Result CloseFile() {
return m_usb->CloseFile();
}
private:
Result SendCmdHeader(u32 cmdId, size_t dataSize, u64 timeout);
Result SendFileRangeCmd(u64 offset, u64 size, u64 timeout);
private:
std::unique_ptr<usb::UsbDs> m_usb;
std::string m_transfer_file_name{};
u8 m_flags{};
std::unique_ptr<usb::install::Usb> m_usb{};
};
} // namespace sphaira::yati::source