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

2
.gitignore vendored
View File

@@ -25,3 +25,5 @@ compile_commands.json
out
usb_test/
__pycache__
usb_*.spec

View File

@@ -18,17 +18,6 @@
"inherits":["core"],
"cacheVariables": {
"CMAKE_BUILD_TYPE": "MinSizeRel",
"ENABLE_NETWORK_INSTALL": false,
"LTO": true
}
},
{
"name": "ReleaseWithInstall",
"displayName": "ReleaseWithInstall",
"inherits":["core"],
"cacheVariables": {
"CMAKE_BUILD_TYPE": "MinSizeRel",
"ENABLE_NETWORK_INSTALL": true,
"LTO": true
}
},
@@ -38,8 +27,8 @@
"inherits":["core"],
"cacheVariables": {
"CMAKE_BUILD_TYPE": "MinSizeRel",
"ENABLE_NETWORK_INSTALL": true,
"LTO": false
"LTO": false,
"DEV_BUILD": true
}
}
],
@@ -49,11 +38,6 @@
"configurePreset": "Release",
"jobs": 16
},
{
"name": "ReleaseWithInstall",
"configurePreset": "ReleaseWithInstall",
"jobs": 16
},
{
"name": "Dev",
"configurePreset": "Dev",

View File

@@ -1,8 +0,0 @@
{
"url": "https://github.com/ITotalJustice/ftpsrv",
"assets": [
{
"name": "switch"
}
]
}

View File

@@ -1,3 +0,0 @@
{
"url": "https://github.com/ITotalJustice/untitled"
}

View File

@@ -35,6 +35,7 @@ set(sphaira_VERSION_HASH "${sphaira_VERSION} [${GIT_COMMIT}]")
add_executable(sphaira
source/ui/menus/appstore.cpp
source/ui/menus/file_viewer.cpp
source/ui/menus/image_viewer.cpp
source/ui/menus/filebrowser.cpp
source/ui/menus/file_picker.cpp
source/ui/menus/homebrew.cpp
@@ -65,6 +66,7 @@ add_executable(sphaira
source/ui/widget.cpp
source/ui/list.cpp
source/ui/scrolling_text.cpp
source/ui/music_player.cpp
source/app.cpp
source/download.cpp
@@ -89,29 +91,39 @@ add_executable(sphaira
source/title_info.cpp
source/minizip_helper.cpp
source/fatfs.cpp
source/usbdvd.cpp
source/utils/utils.cpp
source/utils/audio.cpp
source/utils/nsz_dumper.cpp
source/utils/devoptab_common.cpp
source/utils/devoptab_romfs.cpp
source/utils/devoptab_save.cpp
source/utils/devoptab_nro.cpp
source/utils/devoptab_nca.cpp
source/utils/devoptab_nsp.cpp
source/utils/devoptab_xci.cpp
source/utils/devoptab_zip.cpp
source/utils/devoptab_bfsar.cpp
source/usb/base.cpp
source/usb/usbds.cpp
source/usb/usbhs.cpp
source/usb/usb_uploader.cpp
source/usb/usb_installer.cpp
source/usb/usb_dumper.cpp
source/yati/yati.cpp
source/yati/container/nsp.cpp
source/yati/container/xci.cpp
source/yati/source/file.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
source/yati/nx/nca.cpp
source/yati/nx/ncz.cpp
source/yati/nx/ncm.cpp
source/yati/nx/ns.cpp
@@ -123,7 +135,8 @@ target_compile_definitions(sphaira PRIVATE
-DAPP_VERSION="${sphaira_VERSION}"
-DAPP_VERSION_HASH="${sphaira_VERSION_HASH}"
-DCURL_NO_OLDIES=1
-DENABLE_NETWORK_INSTALL=$<BOOL:${ENABLE_NETWORK_INSTALL}>
-DDEV_BUILD=$<BOOL:${DEV_BUILD}>
-DZSTD_STATIC_LINKING_ONLY=1
)
target_compile_options(sphaira PRIVATE
@@ -177,12 +190,12 @@ FetchContent_Declare(ftpsrv
FetchContent_Declare(libhaze
GIT_REPOSITORY https://github.com/ITotalJustice/libhaze.git
GIT_TAG 0be1523
GIT_TAG f0b2a14
)
FetchContent_Declare(libpulsar
GIT_REPOSITORY https://github.com/ITotalJustice/switch-libpulsar.git
GIT_TAG de656e4
GIT_TAG ac7bc97
)
FetchContent_Declare(nanovg
@@ -197,7 +210,7 @@ FetchContent_Declare(stb
FetchContent_Declare(yyjson
GIT_REPOSITORY https://github.com/ibireme/yyjson.git
GIT_TAG 0.11.1
GIT_TAG 0.12.0
)
FetchContent_Declare(minIni
@@ -213,7 +226,7 @@ FetchContent_Declare(zstd
FetchContent_Declare(libusbhsfs
GIT_REPOSITORY https://github.com/ITotalJustice/libusbhsfs.git
GIT_TAG d0a973e
GIT_TAG 625269b
)
FetchContent_Declare(libnxtc
@@ -226,31 +239,47 @@ FetchContent_Declare(nvjpg
GIT_TAG 45680e7
)
FetchContent_Declare(dr_libs
GIT_REPOSITORY https://github.com/mackron/dr_libs.git
GIT_TAG b962384
SOURCE_SUBDIR NONE
)
FetchContent_Declare(id3v2lib
GIT_REPOSITORY https://github.com/larsbs/id3v2lib.git
GIT_TAG 141ffb8
)
FetchContent_Declare(libusbdvd
GIT_REPOSITORY https://github.com/proconsule/libusbdvd.git
GIT_TAG 3cb0613
)
set(USE_NEW_ZSTD ON)
# has issues with some homebrew and game icons (oxenfree, overwatch2).
set(USE_NVJPG ON)
set(USE_NVJPG OFF)
set(ZSTD_BUILD_STATIC ON)
set(ZSTD_BUILD_SHARED OFF)
set(ZSTD_BUILD_COMPRESSION OFF)
set(ZSTD_BUILD_COMPRESSION ON)
set(ZSTD_MULTITHREAD_SUPPORT ON)
set(ZSTD_BUILD_DECOMPRESSION ON)
set(ZSTD_BUILD_DICTBUILDER OFF)
set(ZSTD_LEGACY_SUPPORT OFF)
set(ZSTD_MULTITHREAD_SUPPORT OFF)
set(ZSTD_BUILD_PROGRAMS OFF)
set(ZSTD_BUILD_TESTS OFF)
set(MININI_LIB_NAME minIni)
set(MININI_USE_STDIO ON)
set(MININI_USE_NX OFF)
set(MININI_USE_FLOAT OFF)
set(MININI_USE_FLOAT ON)
if (CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")
set(NANOVG_DEBUG ON)
endif()
set(NANOVG_NO_JPEG OFF)
set(NANOVG_NO_PNG OFF)
set(NANOVG_NO_BMP ON)
set(NANOVG_NO_BMP OFF)
set(NANOVG_NO_PSD ON)
set(NANOVG_NO_TGA ON)
set(NANOVG_NO_GIF ON)
@@ -283,6 +312,9 @@ FetchContent_MakeAvailable(
libusbhsfs
libnxtc
nvjpg
dr_libs
id3v2lib
libusbdvd
)
set(FTPSRV_LIB_BUILD TRUE)
@@ -324,6 +356,9 @@ target_include_directories(ftpsrv_helper PUBLIC ${ftpsrv_SOURCE_DIR}/src/platfor
add_library(stb INTERFACE)
target_include_directories(stb INTERFACE ${stb_SOURCE_DIR})
add_library(dr_libs INTERFACE)
target_include_directories(dr_libs INTERFACE ${dr_libs_SOURCE_DIR})
add_library(libnxtc
${libnxtc_SOURCE_DIR}/source/nxtc.c
${libnxtc_SOURCE_DIR}/source/nxtc_log.c
@@ -331,6 +366,28 @@ add_library(libnxtc
)
target_include_directories(libnxtc PUBLIC ${libnxtc_SOURCE_DIR}/include)
add_library(libusbdvd
${libusbdvd_SOURCE_DIR}/source/usbdvd.cpp
${libusbdvd_SOURCE_DIR}/source/usbdvd_scsi.cpp
${libusbdvd_SOURCE_DIR}/source/usbdvd_utils.cpp
${libusbdvd_SOURCE_DIR}/source/fs/usbdvd_datadisc.cpp
${libusbdvd_SOURCE_DIR}/source/fs/audiocdfs/audiocdfs.cpp
${libusbdvd_SOURCE_DIR}/source/fs/audiocdfs/cdaudio_devoptab.cpp
${libusbdvd_SOURCE_DIR}/source/fs/iso9660/usbdvd_iso9660.cpp
${libusbdvd_SOURCE_DIR}/source/fs/iso9660/iso9660_devoptab.cpp
${libusbdvd_SOURCE_DIR}/source/fs/udf/usbdvd_udf.cpp
${libusbdvd_SOURCE_DIR}/source/fs/udf/udf_devoptab.cpp
${libusbdvd_SOURCE_DIR}/source/os/switch/switch_usb.cpp
)
target_include_directories(libusbdvd PRIVATE ${libusbdvd_SOURCE_DIR}/source/)
target_include_directories(libusbdvd PRIVATE ${libusbdvd_SOURCE_DIR}/source/os/switch)
target_include_directories(libusbdvd PRIVATE ${libusbdvd_SOURCE_DIR}/source/fs)
target_include_directories(libusbdvd PRIVATE ${libusbdvd_SOURCE_DIR}/source/fs/audiocdfs)
target_include_directories(libusbdvd PRIVATE ${libusbdvd_SOURCE_DIR}/source/fs/iso9660)
target_include_directories(libusbdvd PRIVATE ${libusbdvd_SOURCE_DIR}/source/fs/udf)
target_include_directories(libusbdvd PUBLIC ${libusbdvd_SOURCE_DIR}/include)
if (USE_NVJPG)
add_library(nvjpg
${nvjpg_SOURCE_DIR}/lib/decoder.cpp
@@ -357,7 +414,6 @@ endif()
add_library(fatfs
source/ff16/diskio.c
source/ff16/ff.c
source/ff16/ffunicode.c
)
target_include_directories(fatfs PUBLIC source/ff16)
@@ -380,6 +436,9 @@ target_link_libraries(sphaira PRIVATE
# libusbhsfs
libnxtc
fatfs
dr_libs
id3v2lib
libusbdvd
${minizip_lib}
ZLIB::ZLIB

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

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,7 @@
#include "evman.hpp"
#include "fs.hpp"
#include "app.hpp"
#include "utils/thread.hpp"
#include <switch.h>
#include <cstring>
@@ -32,8 +33,8 @@ namespace {
constexpr auto API_AGENT = "TotalJustice";
constexpr u64 CHUNK_SIZE = 1024*1024;
constexpr auto MAX_THREADS = 4;
constexpr int THREAD_PRIO = PRIO_PREEMPTIVE;
constexpr int THREAD_CORE = 1;
constexpr int THREAD_PRIO = 0x2F;
constexpr int THREAD_CORE = 2;
std::atomic_bool g_running{};
CURLSH* g_curl_share{};
@@ -262,14 +263,17 @@ struct ThreadEntry {
R_UNLESS(m_curl != nullptr, Result_CurlFailedEasyInit);
ueventCreate(&m_uevent, true);
R_TRY(threadCreate(&m_thread, ThreadFunc, this, nullptr, 1024*32, THREAD_PRIO, THREAD_CORE));
R_TRY(svcSetThreadCoreMask(m_thread.handle, THREAD_CORE, THREAD_AFFINITY_DEFAULT(THREAD_CORE)));
R_TRY(utils::CreateThread(&m_thread, ThreadFunc, this, 1024*32));
R_TRY(threadStart(&m_thread));
R_SUCCEED();
}
void Close() {
void SignalClose() {
ueventSignal(&m_uevent);
}
void Close() {
SignalClose();
threadWaitForExit(&m_thread);
threadClose(&m_thread);
if (m_curl) {
@@ -320,13 +324,17 @@ struct ThreadQueue {
auto Create() -> Result {
ueventCreate(&m_uevent, true);
R_TRY(threadCreate(&m_thread, ThreadFunc, this, nullptr, 1024*32, THREAD_PRIO, THREAD_CORE));
R_TRY(utils::CreateThread(&m_thread, ThreadFunc, this, 1024*32));
R_TRY(threadStart(&m_thread));
R_SUCCEED();
}
void Close() {
void SignalClose() {
ueventSignal(&m_uevent);
}
void Close() {
SignalClose();
threadWaitForExit(&m_thread);
threadClose(&m_thread);
}
@@ -1049,6 +1057,12 @@ void ThreadEntry::ThreadFunc(void* p) {
void ThreadQueue::ThreadFunc(void* p) {
auto data = static_cast<ThreadQueue*>(p);
if (!g_cache.init()) {
log_write("failed to init json cache\n");
}
ON_SCOPE_EXIT(g_cache.exit());
while (g_running) {
auto rc = waitSingle(waiterForUEvent(&data->m_uevent), UINT64_MAX);
log_write("[thread queue] woke up\n");
@@ -1141,16 +1155,22 @@ auto Init() -> bool {
log_write("finished creating threads\n");
if (!g_cache.init()) {
log_write("failed to init json cache\n");
}
return true;
}
void Exit() {
void ExitSignal() {
g_running = false;
g_thread_queue.SignalClose();
for (auto& entry : g_threads) {
entry.SignalClose();
}
}
void Exit() {
ExitSignal();
g_thread_queue.Close();
if (g_curl_single) {
@@ -1168,7 +1188,6 @@ void Exit() {
}
curl_global_cleanup();
g_cache.exit();
}
auto ToMemory(const Api& e) -> ApiResult {

View File

@@ -7,6 +7,7 @@
#include "i18n.hpp"
#include "location.hpp"
#include "threaded_file_transfer.hpp"
#include "haze_helper.hpp"
#include "ui/sidebar.hpp"
#include "ui/error_box.hpp"
@@ -18,29 +19,179 @@
#include "yati/source/stream.hpp"
#include "usb/usb_uploader.hpp"
#include "usb/tinfoil.hpp"
#include "usb/usb_dumper.hpp"
#include "usb/usbds.hpp"
namespace sphaira::dump {
namespace {
struct ZipInternal {
WriteSource* writer;
s64 offset;
s64 size;
Result rc;
};
voidpf zopen64_file(voidpf opaque, const void* filename, int mode)
{
ZipInternal* fs = (ZipInternal*)calloc(1, sizeof(*fs));
fs->writer = (WriteSource*)opaque;
return fs;
}
ZPOS64_T ztell64_file(voidpf opaque, voidpf stream)
{
auto fs = (ZipInternal*)stream;
return fs->offset;
}
long zseek64_file(voidpf opaque, voidpf stream, ZPOS64_T offset, int origin)
{
auto fs = (ZipInternal*)stream;
switch (origin) {
case SEEK_SET: {
fs->offset = offset;
} break;
case SEEK_CUR: {
fs->offset += offset;
} break;
case SEEK_END: {
fs->offset = fs->size + offset;
} break;
}
return 0;
}
uLong zwrite_file(voidpf opaque, voidpf stream, const void* buf, uLong size) {
auto fs = (ZipInternal*)stream;
if (R_FAILED(fs->rc = fs->writer->Write(buf, fs->offset, size))) {
return 0;
}
fs->offset += size;
fs->size = std::max(fs->size, fs->offset);
return size;
}
int zclose_file(voidpf opaque, voidpf stream) {
if (stream) {
auto fs = (ZipInternal*)stream;
std::memset(fs, 0, sizeof(*fs));
std::free(fs);
}
return 0;
}
int zerror_file(voidpf opaque, voidpf stream) {
auto fs = (ZipInternal*)stream;
if (R_FAILED(fs->rc)) {
return -1;
}
return 0;
}
constexpr zlib_filefunc64_def zlib_filefunc = {
.zopen64_file = zopen64_file,
.zwrite_file = zwrite_file,
.ztell64_file = ztell64_file,
.zseek64_file = zseek64_file,
.zclose_file = zclose_file,
.zerror_file = zerror_file,
};
struct DumpLocationEntry {
const DumpLocationType type;
const char* name;
};
struct WriteFileSource final : WriteSource {
WriteFileSource(fs::File* file) : m_file{file} {
}
Result Write(const void* buf, s64 off, s64 size) override {
return m_file->Write(off, buf, size, FsWriteOption_None);
}
Result SetSize(s64 size) override {
return m_file->SetSize(size);
}
private:
fs::File* m_file;
};
struct WriteNullSource final : WriteSource {
Result Write(const void* buf, s64 off, s64 size) override {
R_SUCCEED();
}
Result SetSize(s64 size) override {
R_SUCCEED();
}
};
struct WriteUsbSource final : WriteSource {
WriteUsbSource(u64 transfer_timeout) {
// disable mtp if enabled.
m_was_mtp_enabled = haze::IsInit();
if (m_was_mtp_enabled) {
haze::Exit();
}
m_usb = std::make_unique<usb::dump::Usb>(transfer_timeout);
}
~WriteUsbSource() {
m_usb.reset();
if (m_was_mtp_enabled) {
haze::Init();
}
}
Result WaitForConnection(std::string_view path, u64 timeout) {
return m_usb->WaitForConnection(path, timeout);
}
Result CloseFile() {
return m_usb->CloseFile();
}
Result Write(const void* buf, s64 off, s64 size) override {
return m_usb->Write(buf, off, size);
}
Result SetSize(s64 size) override {
R_SUCCEED();
}
private:
std::unique_ptr<usb::dump::Usb> m_usb{};
bool m_was_mtp_enabled{};
};
constexpr DumpLocationEntry DUMP_LOCATIONS[]{
{ DumpLocationType_SdCard, "microSD card (/dumps/)" },
#if ENABLE_NETWORK_INSTALL
{ DumpLocationType_Usb, "USB export to PC (usb_export.py)" },
{ DumpLocationType_UsbS2S, "USB transfer (Switch 2 Switch)" },
#endif
{ DumpLocationType_DevNull, "/dev/null (Speed Test)" },
};
#if ENABLE_NETWORK_INSTALL
struct UsbTest final : usb::upload::Usb, yati::source::Stream {
UsbTest(ui::ProgressBox* pbox, BaseSource* source) : Usb{UINT64_MAX} {
m_pbox = pbox;
m_source = source;
UsbTest(ui::ProgressBox* pbox, BaseSource* source, std::span<const fs::FsPath> paths)
: Usb{UINT64_MAX}
, m_pbox{pbox}
, m_source{source}
, m_paths{paths} {
}
auto& GetPath(u32 index) const {
return m_paths[index];
}
auto& GetPath() const {
return GetPath(m_current_file_index);
}
Result ReadChunk(void* buf, s64 size, u64* bytes_read) override {
@@ -49,26 +200,33 @@ struct UsbTest final : usb::upload::Usb, yati::source::Stream {
R_SUCCEED();
}
Result Read(const std::string& path, void* buf, s64 off, s64 size, u64* bytes_read) override {
Result Read(void* buf, u64 off, u32 size, u64* bytes_read) override {
if (m_pull) {
return Stream::Read(buf, off, size, bytes_read);
} else {
return ReadInternal(path, buf, off, size, bytes_read);
return ReadInternal(GetPath(), buf, off, size, bytes_read);
}
}
Result ReadInternal(const std::string& path, void* buf, s64 off, s64 size, u64* bytes_read) {
if (m_path != path) {
m_path = path;
m_progress = 0;
m_pull_offset = 0;
Stream::Reset();
m_size = m_source->GetSize(path);
m_pbox->SetImage(m_source->GetIcon(path));
m_pbox->SetTitle(m_source->GetName(path));
m_pbox->NewTransfer(m_path);
}
Result Open(u32 index, s64& out_size, u16& out_flags) override {
const auto path = m_paths[index];
const auto size = m_source->GetSize(path);
m_progress = 0;
m_pull_offset = 0;
Stream::Reset();
m_size = size;
m_pbox->SetImage(m_source->GetIcon(path));
m_pbox->SetTitle(m_source->GetName(path));
m_pbox->NewTransfer(path);
m_current_file_index = index;
out_size = size;
out_flags = usb::api::FLAG_STREAM;
R_SUCCEED();
}
Result ReadInternal(const std::string& path, void* buf, s64 off, s64 size, u64* bytes_read) {
R_TRY(m_source->Read(path, buf, off, size, bytes_read));
m_offset += *bytes_read;
@@ -93,16 +251,55 @@ struct UsbTest final : usb::upload::Usb, yati::source::Stream {
private:
ui::ProgressBox* m_pbox{};
BaseSource* m_source{};
std::string m_path{};
std::span<const fs::FsPath> m_paths{};
thread::PullCallback m_pull{};
s64 m_offset{};
s64 m_size{};
s64 m_progress{};
s64 m_pull_offset{};
u32 m_current_file_index{};
};
#endif
Result DumpToFile(ui::ProgressBox* pbox, fs::Fs* fs, const fs::FsPath& root, BaseSource* source, std::span<const fs::FsPath> paths) {
Result DumpToUsb(ui::ProgressBox* pbox, BaseSource* source, std::span<const fs::FsPath> paths, const CustomTransfer& custom_transfer) {
auto write_source = std::make_unique<WriteUsbSource>(3e+9);
for (const auto& path : paths) {
const auto file_size = source->GetSize(path);
pbox->SetImage(source->GetIcon(path));
pbox->SetTitle(source->GetName(path));
pbox->NewTransfer("Waiting for USB connection...");
// wait until usb is ready.
while (true) {
R_TRY(pbox->ShouldExitResult());
const auto rc = write_source->WaitForConnection(path, 3e+9);
if (R_SUCCEEDED(rc)) {
break;
}
}
pbox->NewTransfer(path);
ON_SCOPE_EXIT(write_source->CloseFile());
if (custom_transfer) {
R_TRY(custom_transfer(pbox, source, write_source.get(), path));
} else {
R_TRY(thread::Transfer(pbox, file_size,
[&](void* data, s64 off, s64 size, u64* bytes_read) -> Result {
return source->Read(path, data, off, size, bytes_read);
},
[&](const void* data, s64 off, s64 size) -> Result {
return write_source->Write(data, off, size);
}
));
}
}
R_SUCCEED();
}
Result DumpToFile(ui::ProgressBox* pbox, fs::Fs* fs, const fs::FsPath& root, BaseSource* source, std::span<const fs::FsPath> paths, const CustomTransfer& custom_transfer) {
const auto is_file_based_emummc = App::IsFileBaseEmummc();
for (const auto& path : paths) {
@@ -122,19 +319,24 @@ Result DumpToFile(ui::ProgressBox* pbox, fs::Fs* fs, const fs::FsPath& root, Bas
{
fs::File file;
R_TRY(fs->OpenFile(temp_path, FsOpenMode_Write, &file));
auto write_source = std::make_unique<WriteFileSource>(&file);
R_TRY(thread::Transfer(pbox, file_size,
[&](void* data, s64 off, s64 size, u64* bytes_read) -> Result {
return source->Read(path, data, off, size, bytes_read);
},
[&](const void* data, s64 off, s64 size) -> Result {
const auto rc = file.Write(off, data, size, FsWriteOption_None);
if (is_file_based_emummc) {
svcSleepThread(2e+6); // 2ms
if (custom_transfer) {
R_TRY(custom_transfer(pbox, source, write_source.get(), path));
} else {
R_TRY(thread::Transfer(pbox, file_size,
[&](void* data, s64 off, s64 size, u64* bytes_read) -> Result {
return source->Read(path, data, off, size, bytes_read);
},
[&](const void* data, s64 off, s64 size) -> Result {
const auto rc = write_source->Write(data, off, size);
if (is_file_based_emummc) {
svcSleepThread(2e+6); // 2ms
}
return rc;
}
return rc;
}
));
));
}
}
fs->DeleteFile(base_path);
@@ -144,53 +346,50 @@ Result DumpToFile(ui::ProgressBox* pbox, fs::Fs* fs, const fs::FsPath& root, Bas
R_SUCCEED();
}
Result DumpToFileNative(ui::ProgressBox* pbox, BaseSource* source, std::span<const fs::FsPath> paths) {
Result DumpToFileNative(ui::ProgressBox* pbox, BaseSource* source, std::span<const fs::FsPath> paths, const CustomTransfer& custom_transfer) {
fs::FsNativeSd fs{};
return DumpToFile(pbox, &fs, "/", source, paths);
return DumpToFile(pbox, &fs, "/", source, paths, custom_transfer);
}
Result DumpToStdio(ui::ProgressBox* pbox, const location::StdioEntry& loc, BaseSource* source, std::span<const fs::FsPath> paths) {
Result DumpToStdio(ui::ProgressBox* pbox, const location::StdioEntry& loc, BaseSource* source, std::span<const fs::FsPath> paths, const CustomTransfer& custom_transfer) {
fs::FsStdio fs{};
return DumpToFile(pbox, &fs, loc.mount, source, paths);
return DumpToFile(pbox, &fs, loc.mount, source, paths, custom_transfer);
}
#if ENABLE_NETWORK_INSTALL
Result DumpToUsbS2SStream(ui::ProgressBox* pbox, UsbTest* usb, std::span<const fs::FsPath> paths) {
Result DumpToUsbS2SInternal(ui::ProgressBox* pbox, UsbTest* usb) {
auto source = usb->GetSource();
for (auto& path : paths) {
while (!pbox->ShouldExit()) {
R_TRY(usb->PollCommands());
const auto path = usb->GetPath();
const auto file_size = source->GetSize(path);
R_TRY(thread::TransferPull(pbox, file_size,
[&](void* data, s64 off, s64 size, u64* bytes_read) -> Result {
return usb->ReadInternal(path, data, off, size, bytes_read);
},
[&](thread::StartThreadCallback start, thread::PullCallback pull) -> Result {
[&](const thread::StartThreadCallback& start, const thread::PullCallback& pull) -> Result {
usb->SetPullCallback(pull);
R_TRY(start());
while (!pbox->ShouldExit()) {
R_TRY(usb->PollCommands());
if (usb->GetPullOffset() >= file_size) {
R_SUCCEED();
const auto rc = usb->file_transfer_loop();
if (R_FAILED(rc)) {
if (rc == Result_UsbUploadExit) {
break;
} else {
R_THROW(rc);
}
}
}
R_THROW(0xFFFF);
return pbox->ShouldExitResult();
}
));
}
R_SUCCEED();
}
Result DumpToUsbS2SRandom(ui::ProgressBox* pbox, UsbTest* usb) {
while (!pbox->ShouldExit()) {
R_TRY(usb->PollCommands());
}
R_THROW(0xFFFF);
return pbox->ShouldExitResult();
}
Result DumpToUsbS2S(ui::ProgressBox* pbox, BaseSource* source, std::span<const fs::FsPath> paths) {
@@ -199,27 +398,16 @@ Result DumpToUsbS2S(ui::ProgressBox* pbox, BaseSource* source, std::span<const f
file_list.emplace_back(path);
}
// auto usb = std::make_unique<UsbTest>(pbox, entries);
auto usb = std::make_unique<UsbTest>(pbox, source);
constexpr u64 timeout = 1e+9;
auto usb = std::make_unique<UsbTest>(pbox, source, paths);
constexpr u64 timeout = 3e+9;
while (!pbox->ShouldExit()) {
if (R_SUCCEEDED(usb->IsUsbConnected(timeout))) {
pbox->NewTransfer("USB connected, sending file list"_i18n);
u8 flags = usb::tinfoil::USBFlag_NONE;
if (App::GetApp()->m_dump_usb_transfer_stream.Get()) {
flags |= usb::tinfoil::USBFlag_STREAM;
}
if (R_SUCCEEDED(usb->WaitForConnection(timeout, flags, file_list))) {
if (R_SUCCEEDED(usb->WaitForConnection(timeout, file_list))) {
pbox->NewTransfer("Sent file list, waiting for command..."_i18n);
Result rc;
if (flags & usb::tinfoil::USBFlag_STREAM) {
rc = DumpToUsbS2SStream(pbox, usb.get(), paths);
} else {
rc = DumpToUsbS2SRandom(pbox, usb.get());
}
Result rc = DumpToUsbS2SInternal(pbox, usb.get());
// wait for exit command.
if (R_SUCCEEDED(rc)) {
@@ -242,11 +430,10 @@ Result DumpToUsbS2S(ui::ProgressBox* pbox, BaseSource* source, std::span<const f
}
}
R_THROW(0xFFFF);
return pbox->ShouldExitResult();
}
#endif
Result DumpToDevNull(ui::ProgressBox* pbox, BaseSource* source, std::span<const fs::FsPath> paths) {
Result DumpToDevNull(ui::ProgressBox* pbox, BaseSource* source, std::span<const fs::FsPath> paths, const CustomTransfer& custom_transfer) {
for (auto path : paths) {
R_TRY(pbox->ShouldExitResult());
@@ -255,14 +442,20 @@ Result DumpToDevNull(ui::ProgressBox* pbox, BaseSource* source, std::span<const
pbox->SetTitle(source->GetName(path));
pbox->NewTransfer(path);
R_TRY(thread::Transfer(pbox, file_size,
[&](void* data, s64 off, s64 size, u64* bytes_read) -> Result {
return source->Read(path, data, off, size, bytes_read);
},
[&](const void* data, s64 off, s64 size) -> Result {
R_SUCCEED();
}
));
auto write_source = std::make_unique<WriteNullSource>();
if (custom_transfer) {
R_TRY(custom_transfer(pbox, source, write_source.get(), path));
} else {
R_TRY(thread::Transfer(pbox, file_size,
[&](void* data, s64 off, s64 size, u64* bytes_read) -> Result {
return source->Read(path, data, off, size, bytes_read);
},
[&](const void* data, s64 off, s64 size) -> Result {
return write_source->Write(data, off, size);
}
));
}
}
R_SUCCEED();
@@ -318,13 +511,13 @@ Result DumpToNetwork(ui::ProgressBox* pbox, const location::Entry& loc, BaseSour
} // namespace
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) {
DumpLocation out;
ui::PopupList::Items items;
std::vector<DumpEntry> dump_entries;
out.network = location::Load();
if (location_flags & (1 << DumpLocationType_Network)) {
if (!custom_transfer && location_flags & (1 << DumpLocationType_Network)) {
for (s32 i = 0; i < std::size(out.network); i++) {
dump_entries.emplace_back(DumpLocationType_Network, i);
items.emplace_back(out.network[i].name);
@@ -341,36 +534,45 @@ void DumpGetLocation(const std::string& title, u32 location_flags, const OnLocat
for (s32 i = 0; i < std::size(DUMP_LOCATIONS); i++) {
if (location_flags & (1 << DUMP_LOCATIONS[i].type)) {
dump_entries.emplace_back(DUMP_LOCATIONS[i].type, i);
items.emplace_back(i18n::get(DUMP_LOCATIONS[i].name));
log_write("[dump] got name: %s\n", DUMP_LOCATIONS[i].name);
if (!custom_transfer || DUMP_LOCATIONS[i].type != DumpLocationType_UsbS2S) {
log_write("[dump] got name 2: %s\n", DUMP_LOCATIONS[i].name);
dump_entries.emplace_back(DUMP_LOCATIONS[i].type, i);
items.emplace_back(i18n::get(DUMP_LOCATIONS[i].name));
}
}
}
App::Push<ui::PopupList>(
title, items, [dump_entries, out, on_loc](auto op_index) mutable {
out.entry = dump_entries[*op_index];
log_write("got entry: %u index: %zu\n", out.entry.type, *op_index);
on_loc(out);
}
);
}
void Dump(const std::shared_ptr<BaseSource>& source, const DumpLocation& location, const std::vector<fs::FsPath>& paths, const OnExit& on_exit) {
App::Push<ui::ProgressBox>(0, "Exporting"_i18n, "", [source, paths, location](auto pbox) -> Result {
if (location.entry.type == DumpLocationType_Network) {
R_TRY(DumpToNetwork(pbox, location.network[location.entry.index], source.get(), paths));
} else if (location.entry.type == DumpLocationType_Stdio) {
R_TRY(DumpToStdio(pbox, location.stdio[location.entry.index], source.get(), paths));
} else if (location.entry.type == DumpLocationType_SdCard) {
R_TRY(DumpToFileNative(pbox, source.get(), paths));
} else if (location.entry.type == DumpLocationType_UsbS2S) {
#if ENABLE_NETWORK_INSTALL
R_TRY(DumpToUsbS2S(pbox, source.get(), paths));
#endif
} else if (location.entry.type == DumpLocationType_DevNull) {
R_TRY(DumpToDevNull(pbox, source.get(), paths));
}
Result Dump(ui::ProgressBox* pbox, const std::shared_ptr<BaseSource>& source, const DumpLocation& location, const std::vector<fs::FsPath>& paths, const CustomTransfer& custom_transfer) {
if (location.entry.type == DumpLocationType_Network) {
R_TRY(DumpToNetwork(pbox, location.network[location.entry.index], source.get(), paths));
} else if (location.entry.type == DumpLocationType_Stdio) {
R_TRY(DumpToStdio(pbox, location.stdio[location.entry.index], source.get(), paths, custom_transfer));
} else if (location.entry.type == DumpLocationType_SdCard) {
R_TRY(DumpToFileNative(pbox, source.get(), paths, custom_transfer));
} else if (location.entry.type == DumpLocationType_Usb) {
R_TRY(DumpToUsb(pbox, source.get(), paths, custom_transfer));
} else if (location.entry.type == DumpLocationType_UsbS2S) {
R_TRY(DumpToUsbS2S(pbox, source.get(), paths));
} else if (location.entry.type == DumpLocationType_DevNull) {
R_TRY(DumpToDevNull(pbox, source.get(), paths, custom_transfer));
}
R_SUCCEED();
R_SUCCEED();
}
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) {
App::Push<ui::ProgressBox>(0, "Exporting"_i18n, "", [source, paths, location, custom_transfer](auto pbox) -> Result {
return Dump(pbox, source, location, paths, custom_transfer);
}, [on_exit](Result rc){
App::PushErrorBox(rc, "Export failed!"_i18n);
@@ -379,7 +581,9 @@ void Dump(const std::shared_ptr<BaseSource>& source, const DumpLocation& locatio
log_write("dump successfull!!!\n");
}
on_exit(rc);
if (on_exit) {
on_exit(rc);
}
});
}
@@ -389,4 +593,15 @@ void Dump(const std::shared_ptr<BaseSource>& source, const std::vector<fs::FsPat
});
}
void Dump(const std::shared_ptr<BaseSource>& source, const std::vector<fs::FsPath>& paths, const CustomTransfer& custom_transfer, const OnExit& on_exit, u32 location_flags) {
DumpGetLocation("Select export location"_i18n, location_flags, [source, paths, on_exit, custom_transfer](const DumpLocation& loc) {
Dump(source, loc, paths, on_exit, custom_transfer);
}, custom_transfer);
}
void FileFuncWriter(WriteSource* writer, zlib_filefunc64_def* funcs) {
*funcs = zlib_filefunc;
funcs->opaque = writer;
}
} // namespace sphaira::dump

View File

@@ -1,3 +1,5 @@
#include "utils/devoptab.hpp"
#include "utils/devoptab_common.hpp"
#include "fatfs.hpp"
#include "defines.hpp"
#include "log.hpp"
@@ -6,6 +8,7 @@
#include <array>
#include <algorithm>
#include <span>
#include <memory>
#include <cstring>
#include <cstdio>
@@ -15,98 +18,77 @@
namespace sphaira::fatfs {
namespace {
auto is_archive(BYTE attr) -> bool {
const auto archive_attr = AM_DIR | AM_ARC;
return (attr & archive_attr) == archive_attr;
}
// todo: replace with off+size and have the data be in another struct
// in order to be more lcache efficient.
struct BufferedFileData {
u8* data{};
u64 off{};
u64 size{};
struct FsStorageSource final : yati::source::Base {
FsStorageSource(FsStorage* s) : m_s{*s} {
~BufferedFileData() {
if (data) {
free(data);
}
}
void Allocate(u64 new_size) {
data = (u8*)realloc(data, new_size * sizeof(*data));
off = 0;
size = 0;
}
};
template<typename T>
struct LinkedList {
T* data;
LinkedList* next;
LinkedList* prev;
};
constexpr u64 CACHE_LARGE_ALLOC_SIZE = 1024 * 512;
constexpr u64 CACHE_LARGE_SIZE = 1024 * 16;
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;
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override {
R_TRY(fsStorageRead(&m_s, off, buf, size));
*bytes_read = size;
R_SUCCEED();
}
// 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;
}
Result GetSize(s64* size) {
return fsStorageGetSize(&m_s, size);
}
// 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{};
FsStorage m_s;
};
using LruBufferedData = Lru<BufferedFileData>;
struct File {
FIL* files;
u32 file_count;
size_t off;
char path[256];
};
struct Dir {
FDIR dir;
char path[256];
};
u64 get_size_from_files(const File* file) {
u64 size = 0;
for (u32 i = 0; i < file->file_count; i++) {
size += f_size(&file->files[i]);
}
return size;
}
FIL* get_current_file(File* file) {
auto off = file->off;
for (u32 i = 0; i < file->file_count; i++) {
auto fil = &file->files[i];
if (off <= f_size(fil)) {
return fil;
}
off -= f_size(fil);
}
return NULL;
}
// adjusts current file pos and sets the rest of files to 0.
void set_current_file_pos(File* file) {
s64 off = file->off;
for (u32 i = 0; i < file->file_count; i++) {
auto fil = &file->files[i];
if (off >= 0 && off < f_size(fil)) {
f_lseek(fil, off);
} else {
f_rewind(fil);
}
off -= f_size(fil);
}
}
enum BisMountType {
BisMountType_PRODINFOF,
@@ -117,10 +99,7 @@ enum BisMountType {
struct FatStorageEntry {
FsStorage storage;
s64 storage_size;
LruBufferedData lru_cache[2];
BufferedFileData buffered_small[1024]; // 1MiB (usually).
BufferedFileData buffered_large[2]; // 1MiB
std::unique_ptr<devoptab::common::LruBufferedData> buffered;
FATFS fs;
devoptab_t devoptab;
};
@@ -141,93 +120,39 @@ static_assert(std::size(BIS_MOUNT_ENTRIES) == FF_VOLUMES);
FatStorageEntry g_fat_storage[FF_VOLUMES];
Result ReadStorage(FsStorage* storage, std::span<LruBufferedData> lru_cache, void *_buffer, u64 file_off, u64 read_size, u64 capacity) {
// log_write("[FATFS] read offset: %zu size: %zu\n", file_off, read_size);
auto dst = static_cast<u8*>(_buffer);
size_t amount = 0;
R_UNLESS(file_off < capacity, FsError_UnsupportedOperateRangeForFileStorage);
read_size = std::min(read_size, capacity - file_off);
// fatfs reads in max 16k chunks.
// knowing this, it's possible to detect large file reads by simply checking if
// the read size is 16k (or more, maybe in the furter).
// however this would destroy random access performance, such as fetching 512 bytes.
// the fix was to have 2 LRU caches, one for large data and the other for small (anything below 16k).
// the results in file reads 32MB -> 184MB and directory listing is instant.
const auto large_read = read_size >= 1024 * 16;
auto& lru = large_read ? lru_cache[1] : lru_cache[0];
for (auto list = lru.begin(); list; list = list->next) {
const auto& m_buffered = list->data;
if (m_buffered->size) {
// check if we can read this data into the beginning of dst.
if (file_off < m_buffered->off + m_buffered->size && file_off >= m_buffered->off) {
const auto off = file_off - m_buffered->off;
const auto size = std::min<s64>(read_size, m_buffered->size - off);
if (size) {
// log_write("[FAT] cache HIT at: %zu\n", file_off);
std::memcpy(dst, m_buffered->data + off, size);
read_size -= size;
file_off += size;
amount += size;
dst += size;
lru.Update(list);
break;
}
}
}
}
if (read_size) {
// log_write("[FAT] cache miss at: %zu %zu\n", file_off, read_size);
auto alloc_size = large_read ? CACHE_LARGE_ALLOC_SIZE : std::max<u64>(read_size, 512);
alloc_size = std::min(alloc_size, capacity - file_off);
auto m_buffered = lru.GetNextFree();
m_buffered->Allocate(alloc_size);
// if the dst is big enough, read data in place.
if (read_size > alloc_size) {
R_TRY(fsStorageRead(storage, file_off, dst, read_size));
const auto bytes_read = read_size;
read_size -= bytes_read;
file_off += bytes_read;
amount += bytes_read;
dst += bytes_read;
// save the last chunk of data to the m_buffered io.
const auto max_advance = std::min<u64>(amount, alloc_size);
m_buffered->off = file_off - max_advance;
m_buffered->size = max_advance;
std::memcpy(m_buffered->data, dst - max_advance, max_advance);
} else {
R_TRY(fsStorageRead(storage, file_off, m_buffered->data, alloc_size));
const auto bytes_read = alloc_size;
const auto max_advance = std::min<u64>(read_size, bytes_read);
std::memcpy(dst, m_buffered->data, max_advance);
m_buffered->off = file_off;
m_buffered->size = bytes_read;
read_size -= max_advance;
file_off += max_advance;
amount += max_advance;
dst += max_advance;
}
}
R_SUCCEED();
}
void fill_stat(const FILINFO* fno, struct stat *st) {
void fill_stat(const char* path, const FILINFO* fno, struct stat *st) {
memset(st, 0, sizeof(*st));
st->st_nlink = 1;
struct tm tm{};
tm.tm_sec = (fno->ftime & 0x1F) << 1;
tm.tm_min = (fno->ftime >> 5) & 0x3F;
tm.tm_hour = (fno->ftime >> 11);
tm.tm_mday = (fno->fdate & 0x1F);
tm.tm_mon = ((fno->fdate >> 5) & 0xF) - 1;
tm.tm_year = (fno->fdate >> 9) + 80;
st->st_atime = mktime(&tm);
st->st_mtime = st->st_atime;
st->st_ctime = st->st_atime;
// fake file.
if (path && is_archive(fno->fattrib)) {
st->st_size = 0;
char file_path[256];
for (u16 i = 0; i < 256; i++) {
std::snprintf(file_path, sizeof(file_path), "%s/%02u", path, i);
FILINFO file_info;
if (FR_OK != f_stat(file_path, &file_info)) {
break;
}
st->st_size += file_info.fsize;
}
st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
} else
if (fno->fattrib & AM_DIR) {
st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
} else {
@@ -242,64 +167,126 @@ static int set_errno(struct _reent *r, int err) {
}
int fat_open(struct _reent *r, void *fileStruct, const char *path, int flags, int mode) {
memset(fileStruct, 0, sizeof(FIL));
auto file = static_cast<File*>(fileStruct);
std::memset(file, 0, sizeof(*file));
if (FR_OK != f_open((FIL*)fileStruct, path, FA_READ)) {
// todo: init array
// todo: handle dir.
FIL fil{};
if (FR_OK == f_open(&fil, path, FA_READ)) {
file->file_count = 1;
file->files = (FIL*)std::malloc(sizeof(*file->files));
std::memcpy(file->files, &fil, sizeof(*file->files));
// todo: check what error code is returned here.
} else {
FILINFO info{};
if (FR_OK != f_stat(path, &info)) {
return set_errno(r, ENOENT);
}
if (!(info.fattrib & AM_ARC)) {
return set_errno(r, ENOENT);
}
char file_path[256];
for (u16 i = 0; i < 256; i++) {
std::memset(&fil, 0, sizeof(fil));
std::snprintf(file_path, sizeof(file_path), "%s/%02u", path, i);
if (FR_OK != f_open(&fil, file_path, FA_READ)) {
break;
}
file->files = (FIL*)std::realloc(file->files, (i + 1) * sizeof(*file->files));
std::memcpy(&file->files[i], &fil, sizeof(fil));
file->file_count++;
}
}
if (!file->files) {
return set_errno(r, ENOENT);
}
std::snprintf(file->path, sizeof(file->path), "%s", path);
return r->_errno = 0;
}
int fat_close(struct _reent *r, void *fd) {
if (FR_OK != f_close((FIL*)fd)) {
return set_errno(r, ENOENT);
auto file = static_cast<File*>(fd);
if (file->files) {
for (u32 i = 0; i < file->file_count; i++) {
f_close(&file->files[i]);
}
free(file->files);
}
return r->_errno = 0;
}
ssize_t fat_read(struct _reent *r, void *fd, char *ptr, size_t len) {
UINT bytes_read;
if (FR_OK != f_read((FIL*)fd, ptr, len, &bytes_read)) {
return set_errno(r, ENOENT);
auto file = static_cast<File*>(fd);
UINT total_bytes_read = 0;
while (len) {
UINT bytes_read;
auto fil = get_current_file(file);
if (!fil) {
log_write("[FATFS] failed to get fil\n");
return set_errno(r, ENOENT);
}
if (FR_OK != f_read(fil, ptr, len, &bytes_read)) {
return set_errno(r, ENOENT);
}
if (!bytes_read) {
break;
}
len -= bytes_read;
file->off += bytes_read;
total_bytes_read += bytes_read;
}
return bytes_read;
return total_bytes_read;
}
off_t fat_seek(struct _reent *r, void *fd, off_t pos, int dir) {
auto file = static_cast<File*>(fd);
const auto size = get_size_from_files(file);
if (dir == SEEK_CUR) {
pos += f_tell((FIL*)fd);
pos += file->off;
} else if (dir == SEEK_END) {
pos = f_size((FIL*)fd);
pos = size;
}
if (FR_OK != f_lseek((FIL*)fd, pos)) {
set_errno(r, ENOENT);
return 0;
}
file->off = std::clamp<u64>(pos, 0, size);
set_current_file_pos(file);
r->_errno = 0;
return f_tell((FIL*)fd);
return file->off;
}
int fat_fstat(struct _reent *r, void *fd, struct stat *st) {
const FIL* file = (FIL*)fd;
auto file = static_cast<File*>(fd);
/* Only fill the attr and size field, leaving the timestamp blank. */
FILINFO info = {0};
info.fattrib = file->obj.attr;
info.fsize = file->obj.objsize;
FILINFO info{};
info.fsize = get_size_from_files(file);
/* Fill stat info. */
fill_stat(&info, st);
fill_stat(nullptr, &info, st);
return r->_errno = 0;
}
DIR_ITER* fat_diropen(struct _reent *r, DIR_ITER *dirState, const char *path) {
memset(dirState->dirStruct, 0, sizeof(FDIR));
auto dir = static_cast<Dir*>(dirState->dirStruct);
std::memset(dir, 0, sizeof(*dir));
if (FR_OK != f_opendir((FDIR*)dirState->dirStruct, path)) {
if (FR_OK != f_opendir(&dir->dir, path)) {
set_errno(r, ENOENT);
return NULL;
}
@@ -309,15 +296,20 @@ DIR_ITER* fat_diropen(struct _reent *r, DIR_ITER *dirState, const char *path) {
}
int fat_dirreset(struct _reent *r, DIR_ITER *dirState) {
if (FR_OK != f_rewinddir((FDIR*)dirState->dirStruct)) {
auto dir = static_cast<Dir*>(dirState->dirStruct);
if (FR_OK != f_rewinddir(&dir->dir)) {
return set_errno(r, ENOENT);
}
return r->_errno = 0;
}
int fat_dirnext(struct _reent *r, DIR_ITER *dirState, char *filename, struct stat *filestat) {
auto dir = static_cast<Dir*>(dirState->dirStruct);
FILINFO fno{};
if (FR_OK != f_readdir((FDIR*)dirState->dirStruct, &fno)) {
if (FR_OK != f_readdir(&dir->dir, &fno)) {
return set_errno(r, ENOENT);
}
@@ -326,15 +318,18 @@ int fat_dirnext(struct _reent *r, DIR_ITER *dirState, char *filename, struct sta
}
strcpy(filename, fno.fname);
fill_stat(&fno, filestat);
fill_stat(dir->path, &fno, filestat);
return r->_errno = 0;
}
int fat_dirclose(struct _reent *r, DIR_ITER *dirState) {
if (FR_OK != f_closedir((FDIR*)dirState->dirStruct)) {
auto dir = static_cast<Dir*>(dirState->dirStruct);
if (FR_OK != f_closedir(&dir->dir)) {
return set_errno(r, ENOENT);
}
return r->_errno = 0;
}
@@ -357,19 +352,19 @@ int fat_lstat(struct _reent *r, const char *file, struct stat *st) {
return set_errno(r, ENOENT);
}
fill_stat(&fno, st);
fill_stat(file, &fno, st);
return r->_errno = 0;
}
constexpr devoptab_t DEVOPTAB = {
.structSize = sizeof(FIL),
.structSize = sizeof(File),
.open_r = fat_open,
.close_r = fat_close,
.read_r = fat_read,
.seek_r = fat_seek,
.fstat_r = fat_fstat,
.stat_r = fat_lstat,
.dirStateSize = sizeof(FDIR),
.dirStateSize = sizeof(Dir),
.diropen_r = fat_diropen,
.dirreset_r = fat_dirreset,
.dirnext_r = fat_dirnext,
@@ -378,37 +373,55 @@ constexpr devoptab_t DEVOPTAB = {
.lstat_r = fat_lstat,
};
Mutex g_mutex{};
bool g_is_init{};
} // namespace
Result MountAll() {
SCOPED_MUTEX(&g_mutex);
if (g_is_init) {
R_SUCCEED();
}
for (u32 i = 0; i < FF_VOLUMES; i++) {
auto& fat = g_fat_storage[i];
const auto& bis = BIS_MOUNT_ENTRIES[i];
log_write("[FAT] %s\n", bis.volume_name);
fat.lru_cache[0].Init(fat.buffered_small);
fat.lru_cache[1].Init(fat.buffered_large);
// log_write("[FAT] %s\n", bis.volume_name);
fat.devoptab = DEVOPTAB;
fat.devoptab.name = bis.volume_name;
fat.devoptab.deviceData = &fat;
R_TRY(fsOpenBisStorage(&fat.storage, bis.id));
R_TRY(fsStorageGetSize(&fat.storage, &fat.storage_size));
log_write("[FAT] BIS SUCCESS %s\n", bis.volume_name);
auto source = std::make_shared<FsStorageSource>(&fat.storage);
s64 size;
R_TRY(source->GetSize(&size));
// log_write("[FAT] BIS SUCCESS %s\n", bis.volume_name);
fat.buffered = std::make_unique<devoptab::common::LruBufferedData>(source, size);
R_UNLESS(FR_OK == f_mount(&fat.fs, bis.mount_name, 1), 0x1);
log_write("[FAT] MOUNT SUCCESS %s\n", bis.volume_name);
// log_write("[FAT] MOUNT SUCCESS %s\n", bis.volume_name);
R_UNLESS(AddDevice(&fat.devoptab) >= 0, 0x1);
log_write("[FAT] DEVICE SUCCESS %s\n", bis.volume_name);
// log_write("[FAT] DEVICE SUCCESS %s\n", bis.volume_name);
}
g_is_init = true;
R_SUCCEED();
}
void UnmountAll() {
SCOPED_MUTEX(&g_mutex);
if (!g_is_init) {
return;
}
for (u32 i = 0; i < FF_VOLUMES; i++) {
auto& fat = g_fat_storage[i];
const auto& bis = BIS_MOUNT_ENTRIES[i];
@@ -433,7 +446,7 @@ const char* VolumeStr[] {
Result fatfs_read(u8 num, void* dst, u64 offset, u64 size) {
// log_write("[FAT] num: %u\n", num);
auto& fat = sphaira::fatfs::g_fat_storage[num];
return sphaira::fatfs::ReadStorage(&fat.storage, fat.lru_cache, dst, offset, size, fat.storage_size);
return fat.buffered->Read2(dst, offset, size);
}
} // extern "C"

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,7 @@
#include "app.hpp"
#include "fs.hpp"
#include "log.hpp"
#include "utils/thread.hpp"
#include <algorithm>
#include <minIni.h>
@@ -14,7 +15,6 @@
namespace sphaira::ftpsrv {
namespace {
#if ENABLE_NETWORK_INSTALL
struct InstallSharedData {
Mutex mutex;
std::deque<std::string> queued_files;
@@ -27,11 +27,8 @@ struct InstallSharedData {
bool in_progress;
bool enabled;
};
#endif
const char* INI_PATH = "/config/ftpsrv/config.ini";
constexpr int THREAD_PRIO = PRIO_PREEMPTIVE;
constexpr int THREAD_CORE = 2;
FtpSrvConfig g_ftpsrv_config = {0};
std::atomic_bool g_should_exit = false;
bool g_is_running{false};
@@ -46,7 +43,6 @@ void ftp_progress_callback(void) {
sphaira::App::NotifyFlashLed();
}
#if ENABLE_NETWORK_INSTALL
InstallSharedData g_shared_data{};
const char* SUPPORTED_EXT[] = {
@@ -277,11 +273,73 @@ FtpVfs g_vfs_install = {
.rmdir = vfs_install_rmdir,
.rename = vfs_install_rename,
};
#endif
void loop(void* arg) {
log_write("[FTP] loop entered\n");
// load config.
{
SCOPED_MUTEX(&g_mutex);
g_ftpsrv_config.log_callback = ftp_log_callback;
g_ftpsrv_config.progress_callback = ftp_progress_callback;
g_ftpsrv_config.anon = ini_getbool("Login", "anon", 0, INI_PATH);
int user_len = ini_gets("Login", "user", "", g_ftpsrv_config.user, sizeof(g_ftpsrv_config.user), INI_PATH);
int pass_len = ini_gets("Login", "pass", "", g_ftpsrv_config.pass, sizeof(g_ftpsrv_config.pass), INI_PATH);
g_ftpsrv_config.port = ini_getl("Network", "port", 5000, INI_PATH); // 5000 to keep compat with older sphaira
g_ftpsrv_config.timeout = ini_getl("Network", "timeout", 0, INI_PATH);
g_ftpsrv_config.use_localtime = ini_getbool("Misc", "use_localtime", 0, INI_PATH);
bool log_enabled = ini_getbool("Log", "log", 0, INI_PATH);
// get nx config
bool mount_devices = ini_getbool("Nx", "mount_devices", 1, INI_PATH);
bool mount_bis = ini_getbool("Nx", "mount_bis", 0, INI_PATH);
bool save_writable = ini_getbool("Nx", "save_writable", 0, INI_PATH);
g_ftpsrv_config.port = ini_getl("Nx", "app_port", g_ftpsrv_config.port, INI_PATH); // compat
// get Nx-App overrides
g_ftpsrv_config.anon = ini_getbool("Nx-App", "anon", g_ftpsrv_config.anon, INI_PATH);
user_len = ini_gets("Nx-App", "user", g_ftpsrv_config.user, g_ftpsrv_config.user, sizeof(g_ftpsrv_config.user), INI_PATH);
pass_len = ini_gets("Nx-App", "pass", g_ftpsrv_config.pass, g_ftpsrv_config.pass, sizeof(g_ftpsrv_config.pass), INI_PATH);
g_ftpsrv_config.port = ini_getl("Nx-App", "port", g_ftpsrv_config.port, INI_PATH);
g_ftpsrv_config.timeout = ini_getl("Nx-App", "timeout", g_ftpsrv_config.timeout, INI_PATH);
g_ftpsrv_config.use_localtime = ini_getbool("Nx-App", "use_localtime", g_ftpsrv_config.use_localtime, INI_PATH);
log_enabled = ini_getbool("Nx-App", "log", log_enabled, INI_PATH);
mount_devices = ini_getbool("Nx-App", "mount_devices", mount_devices, INI_PATH);
mount_bis = ini_getbool("Nx-App", "mount_bis", mount_bis, INI_PATH);
save_writable = ini_getbool("Nx-App", "save_writable", save_writable, INI_PATH);
g_should_exit = false;
mount_devices = true;
g_ftpsrv_config.timeout = 0;
if (!g_ftpsrv_config.port) {
g_ftpsrv_config.port = 5000;
log_write("[FTP] no port config, defaulting to 5000\n");
}
// keep compat with older sphaira
if (!user_len && !pass_len) {
g_ftpsrv_config.anon = true;
log_write("[FTP] no user pass, defaulting to anon\n");
}
fsdev_wrapMountSdmc();
const VfsNxCustomPath custom = {
.name = "install",
.user = NULL,
.func = &g_vfs_install,
};
vfs_nx_init(&custom, mount_devices, save_writable, mount_bis, false);
}
ON_SCOPE_EXIT(
vfs_nx_exit();
fsdev_wrapUnmountAll();
);
while (!g_should_exit) {
ftpsrv_init(&g_ftpsrv_config);
while (!g_should_exit) {
@@ -305,77 +363,20 @@ bool Init() {
return false;
}
if (R_FAILED(fsdev_wrapMountSdmc())) {
log_write("[FTP] cannot mount sdmc\n");
return false;
}
// if (R_FAILED(fsdev_wrapMountSdmc())) {
// log_write("[FTP] cannot mount sdmc\n");
// return false;
// }
g_ftpsrv_config.log_callback = ftp_log_callback;
g_ftpsrv_config.progress_callback = ftp_progress_callback;
g_ftpsrv_config.anon = ini_getbool("Login", "anon", 0, INI_PATH);
int user_len = ini_gets("Login", "user", "", g_ftpsrv_config.user, sizeof(g_ftpsrv_config.user), INI_PATH);
int pass_len = ini_gets("Login", "pass", "", g_ftpsrv_config.pass, sizeof(g_ftpsrv_config.pass), INI_PATH);
g_ftpsrv_config.port = ini_getl("Network", "port", 5000, INI_PATH); // 5000 to keep compat with older sphaira
g_ftpsrv_config.timeout = ini_getl("Network", "timeout", 0, INI_PATH);
g_ftpsrv_config.use_localtime = ini_getbool("Misc", "use_localtime", 0, INI_PATH);
bool log_enabled = ini_getbool("Log", "log", 0, INI_PATH);
// get nx config
bool mount_devices = ini_getbool("Nx", "mount_devices", 1, INI_PATH);
bool mount_bis = ini_getbool("Nx", "mount_bis", 0, INI_PATH);
bool save_writable = ini_getbool("Nx", "save_writable", 0, INI_PATH);
g_ftpsrv_config.port = ini_getl("Nx", "app_port", g_ftpsrv_config.port, INI_PATH); // compat
// get Nx-App overrides
g_ftpsrv_config.anon = ini_getbool("Nx-App", "anon", g_ftpsrv_config.anon, INI_PATH);
user_len = ini_gets("Nx-App", "user", g_ftpsrv_config.user, g_ftpsrv_config.user, sizeof(g_ftpsrv_config.user), INI_PATH);
pass_len = ini_gets("Nx-App", "pass", g_ftpsrv_config.pass, g_ftpsrv_config.pass, sizeof(g_ftpsrv_config.pass), INI_PATH);
g_ftpsrv_config.port = ini_getl("Nx-App", "port", g_ftpsrv_config.port, INI_PATH);
g_ftpsrv_config.timeout = ini_getl("Nx-App", "timeout", g_ftpsrv_config.timeout, INI_PATH);
g_ftpsrv_config.use_localtime = ini_getbool("Nx-App", "use_localtime", g_ftpsrv_config.use_localtime, INI_PATH);
log_enabled = ini_getbool("Nx-App", "log", log_enabled, INI_PATH);
mount_devices = ini_getbool("Nx-App", "mount_devices", mount_devices, INI_PATH);
mount_bis = ini_getbool("Nx-App", "mount_bis", mount_bis, INI_PATH);
save_writable = ini_getbool("Nx-App", "save_writable", save_writable, INI_PATH);
g_should_exit = false;
mount_devices = true;
g_ftpsrv_config.timeout = 0;
if (!g_ftpsrv_config.port) {
log_write("[FTP] no port config\n");
return false;
}
// keep compat with older sphaira
if (!user_len && !pass_len) {
log_write("[FTP] no user pass\n");
g_ftpsrv_config.anon = true;
}
#if ENABLE_NETWORK_INSTALL
const VfsNxCustomPath custom = {
.name = "install",
.user = NULL,
.func = &g_vfs_install,
};
vfs_nx_init(&custom, mount_devices, save_writable, mount_bis, false);
#else
vfs_nx_init(NULL, mount_devices, save_writable, mount_bis, false);
#endif
// todo: replace everything with ini_browse for faster loading.
// or load everything in the init thread.
Result rc;
if (R_FAILED(rc = threadCreate(&g_thread, loop, nullptr, nullptr, 1024*16, THREAD_PRIO, THREAD_CORE))) {
if (R_FAILED(rc = utils::CreateThread(&g_thread, loop, nullptr, 1024*16))) {
log_write("[FTP] failed to create nxlink thread: 0x%X\n", rc);
return false;
}
if (R_FAILED(rc = svcSetThreadCoreMask(g_thread.handle, THREAD_CORE, THREAD_AFFINITY_DEFAULT(THREAD_CORE)))) {
log_write("[FTP] failed to set core mask: 0x%X\n", rc);
return false;
}
if (R_FAILED(rc = threadStart(&g_thread))) {
log_write("[FTP] failed to start nxlink thread: 0x%X\n", rc);
threadClose(&g_thread);
@@ -398,8 +399,6 @@ void Exit() {
threadWaitForExit(&g_thread);
threadClose(&g_thread);
vfs_nx_exit();
fsdev_wrapUnmountAll();
memset(&g_ftpsrv_config, 0, sizeof(g_ftpsrv_config));
log_write("[FTP] exitied\n");
@@ -410,7 +409,6 @@ void ExitSignal() {
g_should_exit = true;
}
#if ENABLE_NETWORK_INSTALL
void InitInstallMode(const OnInstallStart& on_start, const OnInstallWrite& on_write, const OnInstallClose& on_close) {
SCOPED_MUTEX(&g_shared_data.mutex);
g_shared_data.on_start = on_start;
@@ -423,7 +421,6 @@ void DisableInstallMode() {
SCOPED_MUTEX(&g_shared_data.mutex);
g_shared_data.enabled = false;
}
#endif
unsigned GetPort() {
SCOPED_MUTEX(&g_mutex);

View File

@@ -4,6 +4,9 @@
#include <mbedtls/md5.h>
#include <utility>
#include <zlib.h>
#include <zstd.h>
namespace sphaira::hash {
namespace {
@@ -59,12 +62,152 @@ private:
struct HashSource {
virtual ~HashSource() = default;
virtual void Update(const void* buf, s64 size) = 0;
virtual void Update(const void* buf, s64 size, s64 file_size) = 0;
virtual void Get(std::string& out) = 0;
};
struct HashNull final : HashSource {
void Update(const void* buf, s64 size, s64 file_size) override {
m_in_size += size;
}
void Get(std::string& out) override {
char str[64];
std::snprintf(str, sizeof(str), "%zu bytes", m_in_size);
out = str;
}
private:
size_t m_in_size{};
};
// this currently crashes when freeing the pool :/
#define USE_THREAD_POOL 0
struct HashZstd final : HashSource {
HashZstd() {
const auto num_threads = 3;
const auto level = ZSTD_CLEVEL_DEFAULT;
m_ctx = ZSTD_createCCtx();
if (!m_ctx) {
log_write("[ZSTD] failed to create ctx\n");
}
#if USE_THREAD_POOL
m_pool = ZSTD_createThreadPool(num_threads);
if (!m_pool) {
log_write("[ZSTD] failed to create pool\n");
}
if (ZSTD_isError(ZSTD_CCtx_refThreadPool(m_ctx, m_pool))) {
log_write("[ZSTD] failed ZSTD_CCtx_refThreadPool(m_pool)\n");
}
#endif
if (ZSTD_isError(ZSTD_CCtx_setParameter(m_ctx, ZSTD_c_compressionLevel, level))) {
log_write("[ZSTD] failed ZSTD_CCtx_setParameter(ZSTD_c_compressionLevel)\n");
}
if (ZSTD_isError(ZSTD_CCtx_setParameter(m_ctx, ZSTD_c_nbWorkers, num_threads))) {
log_write("[ZSTD] failed ZSTD_CCtx_setParameter(ZSTD_c_nbWorkers)\n");
}
m_out_buf.resize(ZSTD_CStreamOutSize());
}
~HashZstd() {
ZSTD_freeCCtx(m_ctx);
#if USE_THREAD_POOL
// crashes here during ZSTD_pthread_join()
// ZSTD_freeThreadPool(m_pool);
#endif
}
void Update(const void* buf, s64 size, s64 file_size) override {
ZSTD_inBuffer input = { buf, (u64)size, 0 };
const auto last_chunk = m_in_size + size >= file_size;
const auto mode = last_chunk ? ZSTD_e_end : ZSTD_e_continue;
while (input.pos < input.size) {
ZSTD_outBuffer output = { m_out_buf.data(), m_out_buf.size(), 0 };
const size_t remaining = ZSTD_compressStream2(m_ctx, &output , &input, mode);
if (ZSTD_isError(remaining)) {
log_write("[ZSTD] error: %zu\n", remaining);
break;
}
m_out_size += output.pos;
};
m_in_size += size;
}
void Get(std::string& out) override {
log_write("getting size: %zu vs %zu\n", m_out_size, m_in_size);
char str[64];
const u32 percentage = ((double)m_out_size / (double)m_in_size) * 100.0;
std::snprintf(str, sizeof(str), "%u%%", percentage);
out = str;
log_write("got size: %zu vs %zu\n", m_out_size, m_in_size);
}
private:
ZSTD_CCtx* m_ctx{};
ZSTD_threadPool* m_pool{};
std::vector<u8> m_out_buf{};
size_t m_in_size{};
size_t m_out_size{};
};
struct HashDeflate final : HashSource {
HashDeflate() {
deflateInit(&m_ctx, Z_DEFAULT_COMPRESSION);
m_out_buf.resize(deflateBound(&m_ctx, 1024*1024*16)); // max chunk size.
}
~HashDeflate() {
deflateEnd(&m_ctx);
}
void Update(const void* buf, s64 size, s64 file_size) override {
m_ctx.avail_in = size;
m_ctx.next_in = const_cast<Bytef*>((const Bytef*)buf);
const auto last_chunk = m_in_size + size >= file_size;
const auto mode = last_chunk ? Z_FINISH : Z_NO_FLUSH;
while (m_ctx.avail_in != 0) {
m_ctx.next_out = m_out_buf.data();
m_ctx.avail_out = m_out_buf.size();
const auto rc = deflate(&m_ctx, mode);
if (Z_OK != rc) {
if (Z_STREAM_END != rc) {
log_write("[ZLIB] deflate error: %d\n", rc);
}
break;
}
}
m_in_size += size;
}
void Get(std::string& out) override {
char str[64];
const u32 percentage = ((double)m_ctx.total_out / (double)m_in_size) * 100.0;
std::snprintf(str, sizeof(str), "%u%%", percentage);
out = str;
}
private:
z_stream m_ctx{};
std::vector<u8> m_out_buf{};
size_t m_in_size{};
};
struct HashCrc32 final : HashSource {
void Update(const void* buf, s64 size) override {
void Update(const void* buf, s64 size, s64 file_size) override {
m_seed = crc32CalculateWithSeed(m_seed, buf, size);
}
@@ -88,7 +231,7 @@ struct HashMd5 final : HashSource {
mbedtls_md5_free(&m_ctx);
}
void Update(const void* buf, s64 size) override {
void Update(const void* buf, s64 size, s64 file_size) override {
mbedtls_md5_update_ret(&m_ctx, (const u8*)buf, size);
}
@@ -113,7 +256,7 @@ struct HashSha1 final : HashSource {
sha1ContextCreate(&m_ctx);
}
void Update(const void* buf, s64 size) override {
void Update(const void* buf, s64 size, s64 file_size) override {
sha1ContextUpdate(&m_ctx, buf, size);
}
@@ -138,7 +281,7 @@ struct HashSha256 final : HashSource {
sha256ContextCreate(&m_ctx);
}
void Update(const void* buf, s64 size) override {
void Update(const void* buf, s64 size, s64 file_size) override {
sha256ContextUpdate(&m_ctx, buf, size);
}
@@ -167,7 +310,7 @@ Result Hash(ui::ProgressBox* pbox, std::unique_ptr<HashSource> hash, BaseSource*
return source->Read(data, off, size, bytes_read);
},
[&](const void* data, s64 off, s64 size) -> Result {
hash->Update(data, size);
hash->Update(data, size, file_size);
R_SUCCEED();
}
));
@@ -184,6 +327,9 @@ auto GetTypeStr(Type type) -> const char* {
case Type::Md5: return "MD5";
case Type::Sha1: return "SHA1";
case Type::Sha256: return "SHA256";
case Type::Null: return "/dev/null (Speed Test)";
case Type::Deflate: return "Deflate (Speed Test)";
case Type::Zstd: return "ZSTD (Speed Test)";
}
return "";
}
@@ -194,6 +340,9 @@ Result Hash(ui::ProgressBox* pbox, Type type, BaseSource* source, std::string& o
case Type::Md5: return Hash(pbox, std::make_unique<HashMd5>(), source, out);
case Type::Sha1: return Hash(pbox, std::make_unique<HashSha1>(), source, out);
case Type::Sha256: return Hash(pbox, std::make_unique<HashSha256>(), source, out);
case Type::Null: return Hash(pbox, std::make_unique<HashNull>(), source, out);
case Type::Deflate: return Hash(pbox, std::make_unique<HashDeflate>(), source, out);
case Type::Zstd: return Hash(pbox, std::make_unique<HashZstd>(), source, out);
}
std::unreachable();
}

View File

@@ -12,7 +12,6 @@
namespace sphaira::haze {
namespace {
#if ENABLE_NETWORK_INSTALL
struct InstallSharedData {
Mutex mutex;
std::string current_file;
@@ -25,7 +24,6 @@ struct InstallSharedData {
bool in_progress;
bool enabled;
};
#endif
constexpr int THREAD_PRIO = 0x20;
constexpr int THREAD_CORE = 2;
@@ -33,7 +31,6 @@ std::atomic_bool g_should_exit = false;
bool g_is_running{false};
Mutex g_mutex{};
#if ENABLE_NETWORK_INSTALL
InstallSharedData g_shared_data{};
const char* SUPPORTED_EXT[] = {
@@ -58,7 +55,6 @@ void on_thing() {
}
}
}
#endif
struct FsProxyBase : ::haze::FileSystemProxyImpl {
FsProxyBase(const char* name, const char* display_name) : m_name{name}, m_display_name{display_name} {
@@ -411,7 +407,6 @@ struct FsDevNullProxy final : FsProxyVfs {
}
};
#if ENABLE_NETWORK_INSTALL
struct FsInstallProxy final : FsProxyVfs {
using FsProxyVfs::FsProxyVfs;
@@ -537,7 +532,6 @@ struct FsInstallProxy final : FsProxyVfs {
return false;
}
};
#endif
::haze::FsEntries g_fs_entries{};
@@ -582,9 +576,7 @@ bool Init() {
g_fs_entries.emplace_back(std::make_shared<FsProxy>(std::make_unique<fs::FsNativeImage>(FsImageDirectoryId_Nand), "image_nand", "Image nand"));
g_fs_entries.emplace_back(std::make_shared<FsProxy>(std::make_unique<fs::FsNativeImage>(FsImageDirectoryId_Sd), "image_sd", "Image sd"));
g_fs_entries.emplace_back(std::make_shared<FsDevNullProxy>("DevNull", "DevNull (Speed Test)"));
#if ENABLE_NETWORK_INSTALL
g_fs_entries.emplace_back(std::make_shared<FsInstallProxy>("install", "Install (NSP, XCI, NSZ, XCZ)"));
#endif
g_should_exit = false;
if (!::haze::Initialize(haze_callback, THREAD_PRIO, THREAD_CORE, g_fs_entries)) {
@@ -595,6 +587,11 @@ bool Init() {
return g_is_running = true;
}
bool IsInit() {
SCOPED_MUTEX(&g_mutex);
return g_is_running;
}
void Exit() {
SCOPED_MUTEX(&g_mutex);
if (!g_is_running) {
@@ -609,7 +606,6 @@ void Exit() {
log_write("[MTP] exitied\n");
}
#if ENABLE_NETWORK_INSTALL
void InitInstallMode(const OnInstallStart& on_start, const OnInstallWrite& on_write, const OnInstallClose& on_close) {
SCOPED_MUTEX(&g_shared_data.mutex);
g_shared_data.on_start = on_start;
@@ -622,6 +618,5 @@ void DisableInstallMode() {
SCOPED_MUTEX(&g_shared_data.mutex);
g_shared_data.enabled = false;
}
#endif
} // namespace sphaira::haze

View File

@@ -1,6 +1,7 @@
#include "location.hpp"
#include "fs.hpp"
#include "app.hpp"
#include "usbdvd.hpp"
#include <ff.h>
#include <cstring>
@@ -76,9 +77,22 @@ auto Load() -> Entries {
}
auto GetStdio(bool write) -> StdioEntries {
StdioEntries out{};
// try and load usbdvd entry.
// todo: check if more than 1 entry is supported.
// todo: only call if usbdvd is init.
if (!write) {
StdioEntry entry;
if (usbdvd::GetMountPoint(entry)) {
out.emplace_back(entry);
}
}
// bail out early if usbhdd is disabled.
if (!App::GetHddEnable()) {
log_write("[USBHSFS] not enabled\n");
return {};
return out;
}
static UsbHsFsDevice devices[0x20];
@@ -86,8 +100,6 @@ auto GetStdio(bool write) -> StdioEntries {
log_write("[USBHSFS] got connected: %u\n", usbHsFsGetPhysicalDeviceCount());
log_write("[USBHSFS] got count: %u\n", count);
StdioEntries out{};
for (s32 i = 0; i < count; i++) {
const auto& e = devices[i];
@@ -109,9 +121,6 @@ auto GetStdio(bool write) -> StdioEntries {
auto GetFat() -> StdioEntries {
StdioEntries out{};
// todo: move this somewhere else.
out.emplace_back("Qlaunch_romfs:/", "Qlaunch RomFS (Read Only)", true);
for (auto& e : VolumeStr) {
char path[64];
std::snprintf(path, sizeof(path), "%s:/", e);

View File

@@ -1,8 +1,10 @@
#include "log.hpp"
#include "defines.hpp"
#include <cstdio>
#include <cstdarg>
#include <ctime>
#include <atomic>
#include <unistd.h>
#include <mutex>
#include <switch.h>
#if sphaira_USE_LOG
@@ -10,18 +12,19 @@ namespace {
constexpr const char* logpath = "/config/sphaira/log.txt";
int nxlink_socket{};
bool g_file_open{};
std::mutex mutex{};
std::atomic_int32_t nxlink_socket{};
std::atomic_bool g_file_open{};
Mutex g_mutex;
void log_write_arg_internal(const char* s, std::va_list* v) {
const auto t = std::time(nullptr);
const auto tm = std::localtime(&t);
static char buf[512];
char buf[512];
const auto len = std::snprintf(buf, sizeof(buf), "[%02u:%02u:%02u] -> ", tm->tm_hour, tm->tm_min, tm->tm_sec);
std::vsnprintf(buf + len, sizeof(buf) - len, s, *v);
SCOPED_MUTEX(&g_mutex);
if (g_file_open) {
auto file = std::fopen(logpath, "a");
if (file) {
@@ -39,7 +42,7 @@ void log_write_arg_internal(const char* s, std::va_list* v) {
extern "C" {
auto log_file_init() -> bool {
std::scoped_lock lock{mutex};
SCOPED_MUTEX(&g_mutex);
if (g_file_open) {
return false;
}
@@ -55,7 +58,7 @@ auto log_file_init() -> bool {
}
auto log_nxlink_init() -> bool {
std::scoped_lock lock{mutex};
SCOPED_MUTEX(&g_mutex);
if (nxlink_socket) {
return false;
}
@@ -65,14 +68,14 @@ auto log_nxlink_init() -> bool {
}
void log_file_exit() {
std::scoped_lock lock{mutex};
SCOPED_MUTEX(&g_mutex);
if (g_file_open) {
g_file_open = false;
}
}
void log_nxlink_exit() {
std::scoped_lock lock{mutex};
SCOPED_MUTEX(&g_mutex);
if (nxlink_socket) {
close(nxlink_socket);
nxlink_socket = 0;
@@ -80,7 +83,6 @@ void log_nxlink_exit() {
}
bool log_is_init() {
std::scoped_lock lock{mutex};
return g_file_open || nxlink_socket;
}
@@ -89,7 +91,6 @@ void log_write(const char* s, ...) {
return;
}
std::scoped_lock lock{mutex};
std::va_list v{};
va_start(v, s);
log_write_arg_internal(s, &v);
@@ -101,7 +102,6 @@ void log_write_arg(const char* s, va_list* v) {
return;
}
std::scoped_lock lock{mutex};
log_write_arg_internal(s, v);
}

View File

@@ -2,6 +2,7 @@
#include <memory>
#include "app.hpp"
#include "log.hpp"
#include "ui/menus/main_menu.hpp"
int main(int argc, char** argv) {
if (!argc || !argv) {
@@ -9,6 +10,7 @@ int main(int argc, char** argv) {
}
auto app = std::make_unique<sphaira::App>(argv[0]);
app->Push<sphaira::ui::menu::main::MainMenu>();
app->Loop();
return 0;
}

View File

@@ -4,6 +4,8 @@
#include <cstring>
#include <cstdio>
#include "log.hpp"
namespace sphaira::mz {
namespace {
@@ -193,8 +195,10 @@ voidpf minizip_open_file_func_stdio(voidpf opaque, const void* filename, int mod
if ((mode & ZLIB_FILEFUNC_MODE_READWRITEFILTER) == ZLIB_FILEFUNC_MODE_READ) {
mode_fopen = "rb";
} else if (mode & ZLIB_FILEFUNC_MODE_EXISTING) {
log_write("[ZIP] opening r/w\n");
mode_fopen = "r+b";
} else if (mode & ZLIB_FILEFUNC_MODE_CREATE) {
log_write("[ZIP] opening r/w +\n");
mode_fopen = "wb";
} else {
return NULL;
@@ -219,6 +223,7 @@ long minizip_seek_file_func_stdio(voidpf opaque, voidpf stream, ZPOS64_T offset,
uLong minizip_read_file_func_stdio(voidpf opaque, voidpf stream, void* buf, uLong size) {
auto file = static_cast<std::FILE*>(stream);
log_write("[ZIP] doing read\n");
return std::fread(buf, 1, size, file);
}

View File

@@ -13,11 +13,6 @@
namespace sphaira {
namespace {
struct NroData {
NroStart start;
NroHeader header;
};
auto nro_parse_internal(fs::Fs* fs, const fs::FsPath& path, NroEntry& entry) -> Result {
entry.path = path;
@@ -60,7 +55,7 @@ auto nro_parse_internal(fs::Fs* fs, const fs::FsPath& path, NroEntry& entry) ->
std::strcpy(nacp.lang.author, "Unknown");
std::strcpy(nacp.display_version, "Unknown");
entry.romfs_offset = entry.romfs_size = entry.icon_offset = entry.icon_size = 0;
entry.icon_offset = entry.icon_size = 0;
entry.is_nacp_valid = false;
} else {
entry.size += sizeof(asset) + asset.icon.size + asset.nacp.size + asset.romfs.size;
@@ -70,8 +65,6 @@ auto nro_parse_internal(fs::Fs* fs, const fs::FsPath& path, NroEntry& entry) ->
// lazy load the icons
entry.icon_size = asset.icon.size;
entry.icon_offset = data.header.size + asset.icon.offset;
entry.romfs_offset = data.header.size + asset.romfs.offset;
entry.romfs_size = asset.romfs.size;
entry.is_nacp_valid = true;
}

View File

@@ -4,6 +4,7 @@
#include "nro.hpp"
#include "log.hpp"
#include "fs.hpp"
#include "utils/thread.hpp"
#include <cstring>
#include <vector>
@@ -24,7 +25,7 @@ namespace {
using Socket = int;
constexpr s32 SERVER_PORT = NXLINK_SERVER_PORT;
constexpr s32 CLIENT_PORT = NXLINK_CLIENT_PORT;
constexpr s32 ZLIB_CHUNK = 0x4000;
constexpr s32 ZLIB_CHUNK = 1024*64;
constexpr s32 ERR_OK = 0;
constexpr s32 ERR_FILE = -1;
@@ -35,7 +36,7 @@ constexpr const char UDP_MAGIC_SERVER[] = {"nxboot"};
constexpr const char UDP_MAGIC_CLIENT[] = {"bootnx"};
Thread g_thread{};
std::mutex g_mutex{};
Mutex g_mutex{};
std::atomic_bool g_quit{false};
bool g_is_running{false};
NxlinkCallback g_callback{};
@@ -55,7 +56,9 @@ struct SocketWrapper {
}
}
void nonBlocking() {
fcntl(sock, F_SETFL, fcntl(sock, F_GETFL) | O_NONBLOCK);
if (this->sock > 0) {
fcntl(sock, F_SETFL, fcntl(sock, F_GETFL) | O_NONBLOCK);
}
}
Socket operator=(Socket s) { return this->sock = s; }
operator int() { return this->sock; }
@@ -106,11 +109,11 @@ void WriteCallbackProgress(NxlinkCallbackType type, s64 offset, s64 size) {
g_callback(&data);
}
auto recvall(int sock, void* buf, int size) -> bool {
auto recvall(int sock, void* buf, int size, sockaddr* src_addr = nullptr, socklen_t* addrlen = nullptr) -> bool {
auto p = static_cast<u8*>(buf);
int got{}, left{size};
while (!g_quit && got < size) {
const auto len = recv(sock, p + got, left, 0);
const auto len = recvfrom(sock, p + got, left, 0, src_addr, addrlen);
if (len == -1) {
if (errno != EWOULDBLOCK && errno != EAGAIN) {
return false;
@@ -124,11 +127,11 @@ auto recvall(int sock, void* buf, int size) -> bool {
return !g_quit;
}
auto sendall(Socket sock, const void* buf, int size) -> bool {
auto sendall(Socket sock, const void* buf, int size, const sockaddr* dest_addr = nullptr, socklen_t addrlen = 0) -> bool {
auto p = static_cast<const u8*>(buf);
int sent{}, left{size};
while (!g_quit && sent < size) {
const auto len = send(sock, p + sent, left, 0);
const auto len = sendto(sock, p + sent, left, 0, dest_addr, addrlen);
if (len == -1) {
if (errno != EWOULDBLOCK && errno != EAGAIN) {
return false;
@@ -141,6 +144,23 @@ auto sendall(Socket sock, const void* buf, int size) -> bool {
return !g_quit;
}
auto acceptall(Socket sock, sockaddr* addr, socklen_t* addrlen) -> Socket {
while (!g_quit) {
Socket connfd = accept(sock, addr, addrlen);
if (connfd < 0) {
if (errno != EWOULDBLOCK && errno != EAGAIN) {
return -1;
}
log_write("[NXLINK] blocking socket in accept, trying again\n");
svcSleepThread(1e+6);
} else {
return connfd;
}
}
return -1;
}
auto get_file_data(Socket sock, int max) -> std::vector<u8> {
std::vector<u8> buf(max);
std::vector<u8> chunk(ZLIB_CHUNK);
@@ -173,7 +193,7 @@ auto get_file_data(Socket sock, int max) -> std::vector<u8> {
}
void loop(void* args) {
log_write("in nxlink thread func\n");
log_write("[NXLINK] in nxlink thread func\n");
const sockaddr_in servaddr{
.sin_family = AF_INET,
.sin_port = htons(SERVER_PORT),
@@ -213,33 +233,33 @@ void loop(void* args) {
SocketWrapper sock_udp(AF_INET, SOCK_DGRAM, 0);
if (sock < 0 || sock_udp < 0) {
log_write("failed to get sock/sock_udp: 0x%X\n", socketGetLastResult());
log_write("[NXLINK] failed to get sock/sock_udp: 0x%X %s\n", socketGetLastResult(), strerror(errno));
continue;
}
u32 tmpval = 1;
if (0 > setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &tmpval, sizeof(tmpval))) {
log_write("set sockopt(): 0x%X\n", socketGetLastResult());
log_write("[NXLINK] set sockopt(): 0x%X %s\n", socketGetLastResult(), strerror(errno));
continue;
}
if (0 > setsockopt(sock_udp, SOL_SOCKET, SO_REUSEADDR, &tmpval, sizeof(tmpval))) {
log_write("set sockopt(): 0x%X\n", socketGetLastResult());
log_write("[NXLINK] set sockopt(): 0x%X %s\n", socketGetLastResult(), strerror(errno));
continue;
}
if (0 > bind(sock, (const sockaddr*)&servaddr, sizeof(servaddr))) {
log_write("failed to get bind(sock): 0x%X\n", socketGetLastResult());
log_write("[NXLINK] failed to get bind(sock): 0x%X %s\n", socketGetLastResult(), strerror(errno));
continue;
}
if (0 > bind(sock_udp, (const sockaddr*)&servaddr, sizeof(servaddr))) {
log_write("failed to get bind(sock_udp): 0x%X\n", socketGetLastResult());
log_write("[NXLINK] failed to get bind(sock_udp): 0x%X %s\n", socketGetLastResult(), strerror(errno));
continue;
}
if (0 > listen(sock, 10)) {
log_write("failed to get listen: 0x%X\n", socketGetLastResult());
log_write("[NXLINK] failed to get listen: 0x%X %s\n", socketGetLastResult(), strerror(errno));
continue;
}
@@ -252,7 +272,7 @@ void loop(void* args) {
pfds[1].events = POLLIN;
while (!g_quit) {
auto poll_rc = poll(pfds, std::size(pfds), 1000/60);
auto poll_rc = poll(pfds, std::size(pfds), 100);
if (poll_rc < 0) {
break;
} else if (poll_rc == 0) {
@@ -264,48 +284,52 @@ void loop(void* args) {
if (pfds[1].revents & POLLIN) {
char recvbuf[6];
socklen_t from_len = sizeof(sa_remote);
auto udp_len = recvfrom(sock_udp, recvbuf, sizeof(recvbuf), 0, (sockaddr*)&sa_remote, &from_len);
if (udp_len == sizeof(recvbuf) && !std::strncmp(recvbuf, UDP_MAGIC_SERVER, std::strlen(UDP_MAGIC_SERVER))) {
// log_write("got udp len: %d - %.*s\n", udp_len, udp_len, recvbuf);
if (!recvall(sock_udp, recvbuf, sizeof(recvbuf), (sockaddr*)&sa_remote, &from_len)) {
log_write("[NXLINK] failed to get udp socket: 0x%X %s\n", socketGetLastResult(), strerror(errno));
continue;
}
if (!std::strncmp(recvbuf, UDP_MAGIC_SERVER, std::strlen(UDP_MAGIC_SERVER))) {
sa_remote.sin_family = AF_INET;
sa_remote.sin_port = htons(NXLINK_CLIENT_PORT);
udp_len = sendto(sock_udp, UDP_MAGIC_CLIENT, std::strlen(UDP_MAGIC_CLIENT), 0, (const sockaddr*)&sa_remote, sizeof(sa_remote));
if (udp_len != std::strlen(UDP_MAGIC_CLIENT)) {
log_write("nxlink failed to send udp packet\n");
if (!sendall(sock_udp, UDP_MAGIC_CLIENT, std::strlen(UDP_MAGIC_CLIENT), (const sockaddr*)&sa_remote, sizeof(sa_remote))) {
log_write("[NXLINK] failed to send udp socket: 0x%X %s\n", socketGetLastResult(), strerror(errno));
continue;
}
}
}
socklen_t accept_len = sizeof(sa_remote);
SocketWrapper connfd = accept(sock, (sockaddr*)&sa_remote, &accept_len);
SocketWrapper connfd = acceptall(sock, (sockaddr*)&sa_remote, &accept_len);
if (connfd < 0) {
log_write("[NXLINK] failed to accept socket: 0x%X %s\n", socketGetLastResult(), strerror(errno));
continue;
}
WriteCallbackNone(NxlinkCallbackType_Connected);
u32 namelen{};
if (!recvall(connfd, &namelen, sizeof(namelen))) {
log_write("failed to get name: 0x%X\n", socketGetLastResult());
log_write("[NXLINK] failed to get name: 0x%X %s\n", socketGetLastResult(), strerror(errno));
continue;
}
fs::FsPath name{};
if (namelen >= sizeof(name)) {
log_write("namelen is bigger than name: 0x%X\n", socketGetLastResult());
log_write("[NXLINK] namelen is bigger than name: 0x%X %s\n", socketGetLastResult(), strerror(errno));
continue;
}
if (!recvall(connfd, name, namelen)) {
log_write("failed to get name: 0x%X\n", socketGetLastResult());
log_write("[NXLINK] failed to get name: 0x%X %s\n", socketGetLastResult(), strerror(errno));
continue;
}
log_write("got name: %s\n", name.s);
log_write("[NXLINK] got name: %s\n", name.s);
u32 filesize{};
if (!recvall(connfd, &filesize, sizeof(filesize))) {
log_write("failed to get filesize: 0x%X\n", socketGetLastResult());
log_write("[NXLINK] failed to get filesize: 0x%X %s\n", socketGetLastResult(), strerror(errno));
continue;
}
@@ -319,7 +343,7 @@ void loop(void* args) {
// tell nxlink that we want this file
if (!sendall(connfd, &ERR_OK, sizeof(ERR_OK))) {
log_write("failed to tell nxlink that we want the file: 0x%X\n", socketGetLastResult());
log_write("[NXLINK] failed to tell nxlink that we want the file: 0x%X %s\n", socketGetLastResult(), strerror(errno));
continue;
}
@@ -329,6 +353,7 @@ void loop(void* args) {
WriteCallbackFile(NxlinkCallbackType_WriteEnd, name);
if (file_data.empty()) {
log_write("[NXLINK] failed to get file data: 0x%X %s\n", socketGetLastResult(), strerror(errno));
continue;
}
@@ -343,7 +368,7 @@ void loop(void* args) {
// if (R_FAILED(rc = create_directories(fs, path))) {
if (R_FAILED(rc = fs.CreateDirectoryRecursivelyWithPath(path))) {
sendall(connfd, &ERR_FILE, sizeof(ERR_FILE));
log_write("failed to create directories: %X\n", rc);
log_write("[NXLINK] failed to create directories: %X\n", rc);
continue;
}
@@ -351,7 +376,7 @@ void loop(void* args) {
const auto temp_path = path + "~";
if (R_FAILED(rc = fs.CreateFile(temp_path, file_data.size(), 0)) && rc != FsError_PathAlreadyExists) {
sendall(connfd, &ERR_FILE, sizeof(ERR_FILE));
log_write("failed to create file: %X\n", rc);
log_write("[NXLINK] failed to create file: %X\n", rc);
continue;
}
ON_SCOPE_EXIT(fs.DeleteFile(temp_path));
@@ -360,13 +385,13 @@ void loop(void* args) {
fs::File f;
if (R_FAILED(rc = fs.OpenFile(temp_path, FsOpenMode_Write, &f))) {
sendall(connfd, &ERR_FILE, sizeof(ERR_FILE));
log_write("failed to open file %X\n", rc);
log_write("[NXLINK] failed to open file %X\n", rc);
continue;
}
if (R_FAILED(rc = f.SetSize(file_data.size()))) {
sendall(connfd, &ERR_FILE, sizeof(ERR_FILE));
log_write("failed to set file size: 0x%X\n", socketGetLastResult());
log_write("[NXLINK] failed to set file size: 0x%X\n", socketGetLastResult());
continue;
}
@@ -374,7 +399,7 @@ void loop(void* args) {
while (offset < file_data.size()) {
svcSleepThread(YieldType_WithoutCoreMigration);
u64 chunk_size = ZLIB_CHUNK;
u64 chunk_size = 1024*1024;
if (offset + chunk_size > file_data.size()) {
chunk_size = file_data.size() - offset;
}
@@ -387,23 +412,25 @@ void loop(void* args) {
// if (R_FAILED(rc = fsFileWrite(&f, 0, file_data.data(), file_data.size(), FsWriteOption_None))) {
if (R_FAILED(rc)) {
sendall(connfd, &ERR_FILE, sizeof(ERR_FILE));
log_write("failed to write: 0x%X\n", socketGetLastResult());
log_write("[NXLINK] failed to write: 0x%X\n", socketGetLastResult());
continue;
}
}
if (R_FAILED(rc = fs.DeleteFile(path)) && rc != FsError_PathNotFound) {
log_write("failed to delete %X\n", rc);
log_write("[NXLINK] failed to delete %X\n", rc);
continue;
}
if (R_FAILED(rc = fs.RenameFile(temp_path, path))) {
log_write("failed to rename %X\n", rc);
log_write("[NXLINK] failed to rename %X\n", rc);
continue;
}
// log error here, but don't fail as we already have the nro
// so this just means that nxlink server won't start.
if (!sendall(connfd, &ERR_OK, sizeof(ERR_OK))) {
log_write("failed to send ok message: 0x%X\n", socketGetLastResult());
log_write("[NXLINK] failed to send ok message: 0x%X %s\n", socketGetLastResult(), strerror(errno));
continue;
}
@@ -435,7 +462,7 @@ void loop(void* args) {
}
args += nxlinked;
// log_write("launching with: %s %s\n", path.c_str(), args.c_str());
// log_write("[NXLINK] launching with: %s %s\n", path.c_str(), args.c_str());
if (R_SUCCEEDED(sphaira::nro_launch(path, args))) {
g_quit = true;
}
@@ -449,7 +476,7 @@ void loop(void* args) {
extern "C" {
bool nxlinkInitialize(NxlinkCallback callback) {
std::scoped_lock lock{g_mutex};
SCOPED_MUTEX(&g_mutex);
if (g_is_running) {
return false;
}
@@ -458,13 +485,13 @@ bool nxlinkInitialize(NxlinkCallback callback) {
g_quit = false;
Result rc;
if (R_FAILED(rc = threadCreate(&g_thread, loop, nullptr, nullptr, 1024*64, PRIO_PREEMPTIVE, 2))) {
log_write("failed to create nxlink thread: 0x%X\n", rc);
if (R_FAILED(rc = sphaira::utils::CreateThread(&g_thread, loop, nullptr, 1024*64))) {
log_write("[NXLINK] failed to create nxlink thread: 0x%X\n", rc);
return false;
}
if (R_FAILED(rc = threadStart(&g_thread))) {
log_write("failed to start nxlink thread: 0x%X\n", rc);
log_write("[NXLINK] failed to start nxlink thread: 0x%X\n", rc);
threadClose(&g_thread);
return false;
}
@@ -473,17 +500,15 @@ bool nxlinkInitialize(NxlinkCallback callback) {
}
void nxlinkExit() {
std::scoped_lock lock{g_mutex};
if (g_is_running) {
g_is_running = false;
}
SCOPED_MUTEX(&g_mutex);
g_is_running = false;
g_quit = true;
threadWaitForExit(&g_thread);
threadClose(&g_thread);
}
void nxlinkSignalExit() {
std::scoped_lock lock{g_mutex};
SCOPED_MUTEX(&g_mutex);
g_quit = true;
}

View File

@@ -17,6 +17,8 @@ auto OptionBase<T>::GetInternal(const char* name) -> T {
m_value = ini_getbool(m_section.c_str(), name, m_default_value, App::CONFIG_PATH);
} else if constexpr(std::is_same_v<T, long>) {
m_value = ini_getl(m_section.c_str(), name, m_default_value, App::CONFIG_PATH);
} else if constexpr(std::is_same_v<T, float>) {
m_value = ini_getf(m_section.c_str(), name, m_default_value, App::CONFIG_PATH);
} else if constexpr(std::is_same_v<T, std::string>) {
char buf[FS_MAX_PATH];
ini_gets(m_section.c_str(), name, m_default_value.c_str(), buf, sizeof(buf), App::CONFIG_PATH);
@@ -52,6 +54,8 @@ void OptionBase<T>::Set(T value) {
ini_putl(m_section.c_str(), m_name.c_str(), value, App::CONFIG_PATH);
} else if constexpr(std::is_same_v<T, long>) {
ini_putl(m_section.c_str(), m_name.c_str(), value, App::CONFIG_PATH);
} else if constexpr(std::is_same_v<T, float>) {
ini_putf(m_section.c_str(), m_name.c_str(), value, App::CONFIG_PATH);
} else if constexpr(std::is_same_v<T, std::string>) {
ini_puts(m_section.c_str(), m_name.c_str(), value.c_str(), App::CONFIG_PATH);
}
@@ -71,6 +75,8 @@ auto OptionBase<T>::LoadFrom(const char* name, const char* value) -> bool {
m_value = ini_parse_getbool(value, m_default_value);
} else if constexpr(std::is_same_v<T, long>) {
m_value = ini_parse_getl(value, m_default_value);
} else if constexpr(std::is_same_v<T, float>) {
m_value = ini_atof(value);
} else if constexpr(std::is_same_v<T, std::string>) {
m_value = value;
}
@@ -84,6 +90,7 @@ auto OptionBase<T>::LoadFrom(const char* name, const char* value) -> bool {
template struct OptionBase<bool>;
template struct OptionBase<long>;
template struct OptionBase<float>;
template struct OptionBase<std::string>;
} // namespace sphaira::option

View File

@@ -3,6 +3,7 @@
#include "defines.hpp"
#include "app.hpp"
#include "minizip_helper.hpp"
#include "utils/thread.hpp"
#include <vector>
#include <algorithm>
@@ -72,13 +73,13 @@ public:
};
struct ThreadData {
ThreadData(ui::ProgressBox* _pbox, s64 size, const ReadCallback& _rfunc, const WriteCallback& _wfunc, u64 buffer_size);
ThreadData(ui::ProgressBox* _pbox, s64 size, const ReadCallback& _rfunc, const DecompressCallback& _dfunc, const WriteCallback& _wfunc, u64 buffer_size);
auto GetResults() volatile -> Result;
void WakeAllThreads();
auto IsAnyRunning() volatile const -> bool {
return read_running || write_running;
return read_running || decompress_running || write_running;
}
auto GetWriteOffset() volatile const -> s64 {
@@ -100,6 +101,17 @@ struct ThreadData {
void SetReadResult(Result result) {
read_result = result;
// wake up decompress thread as it may be waiting on data that never comes.
condvarWakeOne(std::addressof(can_decompress));
if (R_FAILED(result)) {
ueventSignal(GetDoneEvent());
}
}
void SetDecompressResult(Result result) {
decompress_result = result;
// wake up write thread as it may be waiting on data that never comes.
condvarWakeOne(std::addressof(can_write));
@@ -110,6 +122,10 @@ struct ThreadData {
void SetWriteResult(Result result) {
write_result = result;
// wake up decompress thread as it may be waiting on data that never comes.
condvarWakeOne(std::addressof(can_decompress_write));
ueventSignal(GetDoneEvent());
}
@@ -122,9 +138,12 @@ struct ThreadData {
Result Pull(void* data, s64 size, u64* bytes_read);
Result readFuncInternal();
Result decompressFuncInternal();
Result writeFuncInternal();
private:
Result SetDecompressBuf(std::vector<u8>& buf, s64 off, s64 size);
Result GetDecompressBuf(std::vector<u8>& buf_out, s64& off_out);
Result SetWriteBuf(std::vector<u8>& buf, s64 size);
Result GetWriteBuf(std::vector<u8>& buf_out, s64& off_out);
Result SetPullBuf(std::vector<u8>& buf, s64 size);
@@ -136,21 +155,30 @@ private:
// these need to be copied
ui::ProgressBox* const pbox;
const ReadCallback& rfunc;
const DecompressCallback& dfunc;
const WriteCallback& wfunc;
// these need to be created
Mutex mutex{};
Mutex read_mutex{};
Mutex write_mutex{};
Mutex pull_mutex{};
CondVar can_read{};
CondVar can_write{};
CondVar can_decompress{};
CondVar can_decompress_write{};
// only used when pull is active.
CondVar can_pull{};
CondVar can_pull_write{};
UEvent m_uevent_done{};
UEvent m_uevent_progres{};
RingBuf<2> read_buffers{};
RingBuf<2> write_buffers{};
std::vector<u8> pull_buffer{};
s64 pull_buffer_offset{};
@@ -159,27 +187,35 @@ private:
// these are shared between threads
std::atomic<s64> read_offset{};
std::atomic<s64> decompress_offset{};
std::atomic<s64> write_offset{};
std::atomic<Result> read_result{};
std::atomic<Result> decompress_result{};
std::atomic<Result> write_result{};
std::atomic<Result> pull_result{};
std::atomic_bool read_running{true};
std::atomic_bool decompress_running{true};
std::atomic_bool write_running{true};
};
ThreadData::ThreadData(ui::ProgressBox* _pbox, s64 size, const ReadCallback& _rfunc, const WriteCallback& _wfunc, u64 buffer_size)
ThreadData::ThreadData(ui::ProgressBox* _pbox, s64 size, const ReadCallback& _rfunc, const DecompressCallback& _dfunc, const WriteCallback& _wfunc, u64 buffer_size)
: pbox{_pbox}
, rfunc{_rfunc}
, dfunc{_dfunc}
, wfunc{_wfunc}
, read_buffer_size{buffer_size}
, write_size{size} {
mutexInit(std::addressof(mutex));
mutexInit(std::addressof(read_mutex));
mutexInit(std::addressof(write_mutex));
mutexInit(std::addressof(pull_mutex));
condvarInit(std::addressof(can_read));
condvarInit(std::addressof(can_decompress));
condvarInit(std::addressof(can_decompress_write));
condvarInit(std::addressof(can_write));
condvarInit(std::addressof(can_pull));
condvarInit(std::addressof(can_pull_write));
@@ -190,6 +226,7 @@ ThreadData::ThreadData(ui::ProgressBox* _pbox, s64 size, const ReadCallback& _rf
auto ThreadData::GetResults() volatile -> Result {
R_TRY(pbox->ShouldExitResult());
R_TRY(read_result.load());
R_TRY(decompress_result.load());
R_TRY(write_result.load());
R_TRY(pull_result.load());
R_SUCCEED();
@@ -198,44 +235,80 @@ auto ThreadData::GetResults() volatile -> Result {
void ThreadData::WakeAllThreads() {
condvarWakeAll(std::addressof(can_read));
condvarWakeAll(std::addressof(can_write));
condvarWakeAll(std::addressof(can_decompress));
condvarWakeAll(std::addressof(can_decompress_write));
condvarWakeAll(std::addressof(can_pull));
condvarWakeAll(std::addressof(can_pull_write));
mutexUnlock(std::addressof(mutex));
mutexUnlock(std::addressof(read_mutex));
mutexUnlock(std::addressof(write_mutex));
mutexUnlock(std::addressof(pull_mutex));
}
Result ThreadData::SetDecompressBuf(std::vector<u8>& buf, s64 off, s64 size) {
buf.resize(size);
mutexLock(std::addressof(read_mutex));
if (!read_buffers.ringbuf_free()) {
if (!write_running) {
R_SUCCEED();
}
R_TRY(condvarWait(std::addressof(can_read), std::addressof(read_mutex)));
}
ON_SCOPE_EXIT(mutexUnlock(std::addressof(read_mutex)));
R_TRY(GetResults());
read_buffers.ringbuf_push(buf, off);
return condvarWakeOne(std::addressof(can_decompress));
}
Result ThreadData::GetDecompressBuf(std::vector<u8>& buf_out, s64& off_out) {
mutexLock(std::addressof(read_mutex));
if (!read_buffers.ringbuf_size()) {
if (!read_running) {
buf_out.resize(0);
R_SUCCEED();
}
R_TRY(condvarWait(std::addressof(can_decompress), std::addressof(read_mutex)));
}
ON_SCOPE_EXIT(mutexUnlock(std::addressof(read_mutex)));
R_TRY(GetResults());
read_buffers.ringbuf_pop(buf_out, off_out);
return condvarWakeOne(std::addressof(can_read));
}
Result ThreadData::SetWriteBuf(std::vector<u8>& buf, s64 size) {
buf.resize(size);
mutexLock(std::addressof(mutex));
mutexLock(std::addressof(write_mutex));
if (!write_buffers.ringbuf_free()) {
if (!write_running) {
if (!decompress_running) {
R_SUCCEED();
}
R_TRY(condvarWait(std::addressof(can_read), std::addressof(mutex)));
R_TRY(condvarWait(std::addressof(can_decompress_write), std::addressof(write_mutex)));
}
ON_SCOPE_EXIT(mutexUnlock(std::addressof(mutex)));
ON_SCOPE_EXIT(mutexUnlock(std::addressof(write_mutex)));
R_TRY(GetResults());
write_buffers.ringbuf_push(buf, 0);
return condvarWakeOne(std::addressof(can_write));
}
Result ThreadData::GetWriteBuf(std::vector<u8>& buf_out, s64& off_out) {
mutexLock(std::addressof(mutex));
mutexLock(std::addressof(write_mutex));
if (!write_buffers.ringbuf_size()) {
if (!read_running) {
if (!decompress_running) {
buf_out.resize(0);
R_SUCCEED();
}
R_TRY(condvarWait(std::addressof(can_write), std::addressof(mutex)));
R_TRY(condvarWait(std::addressof(can_write), std::addressof(write_mutex)));
}
ON_SCOPE_EXIT(mutexUnlock(std::addressof(mutex)));
ON_SCOPE_EXIT(mutexUnlock(std::addressof(write_mutex)));
R_TRY(GetResults());
write_buffers.ringbuf_pop(buf_out, off_out);
return condvarWakeOne(std::addressof(can_read));
return condvarWakeOne(std::addressof(can_decompress_write));
}
Result ThreadData::SetPullBuf(std::vector<u8>& buf, s64 size) {
@@ -296,6 +369,7 @@ Result ThreadData::readFuncInternal() {
while (this->read_offset < this->write_size && R_SUCCEEDED(this->GetResults())) {
// read more data
const auto buffer_offset = this->read_offset.load();
s64 read_size = this->read_buffer_size;
u64 bytes_read{};
@@ -306,13 +380,82 @@ Result ThreadData::readFuncInternal() {
}
auto buf_size = bytes_read;
R_TRY(this->SetWriteBuf(buf, buf_size));
R_TRY(this->SetDecompressBuf(buf, buffer_offset, buf_size));
}
log_write("finished read thread success!\n");
R_SUCCEED();
}
// read thread reads all data from the source
Result ThreadData::decompressFuncInternal() {
ON_SCOPE_EXIT( decompress_running = false; );
std::vector<u8> buf{};
std::vector<u8> temp_buf{};
buf.reserve(this->read_buffer_size);
temp_buf.reserve(this->read_buffer_size);
const auto temp_buf_flush_max = this->read_buffer_size / 2;
while (this->decompress_offset < this->write_size && R_SUCCEEDED(this->GetResults())) {
s64 decompress_buf_off{};
R_TRY(this->GetDecompressBuf(buf, decompress_buf_off));
if (buf.empty()) {
log_write("exiting decompress func early because no data was received\n");
break;
}
if (this->dfunc) {
R_TRY(this->dfunc(buf.data(), decompress_buf_off, buf.size(), [&](const void* _data, s64 size) -> Result {
auto data = (const u8*)_data;
while (size) {
const auto block_off = temp_buf.size();
const auto rsize = std::min<s64>(size, temp_buf_flush_max - block_off);
temp_buf.resize(block_off + rsize);
std::memcpy(temp_buf.data() + block_off, data, rsize);
if (temp_buf.size() == temp_buf_flush_max) {
// log_write("flushing data: %zu %.2f MiB\n", temp_buf.size(), temp_buf.size() / 1024.0 / 1024.0);
R_TRY(this->SetWriteBuf(temp_buf, temp_buf.size()));
temp_buf.resize(0);
}
size -= rsize;
this->decompress_offset += rsize;
data += rsize;
// const auto buf_off = temp_buf.size();
// temp_buf.resize(buf_off + size);
// std::memcpy(temp_buf.data() + buf_off, data, size);
// this->decompress_offset += size;
// if (temp_buf.size() >= temp_buf_flush_max) {
// // log_write("flushing data: %zu %.2f MiB\n", temp_buf.size(), temp_buf.size() / 1024.0 / 1024.0);
// R_TRY(this->SetWriteBuf(temp_buf, temp_buf.size()));
// temp_buf.resize(0);
// }
}
R_SUCCEED();
}));
} else {
this->decompress_offset += buf.size();
R_TRY(this->SetWriteBuf(buf, buf.size()));
}
}
// flush buffer.
if (!temp_buf.empty()) {
log_write("flushing data: %zu\n", temp_buf.size());
R_TRY(this->SetWriteBuf(temp_buf, temp_buf.size()));
}
log_write("finished decompress thread success!\n");
R_SUCCEED();
}
// write thread writes data to wfunc.
Result ThreadData::writeFuncInternal() {
ON_SCOPE_EXIT( write_running = false; );
@@ -349,17 +492,20 @@ void readFunc(void* d) {
log_write("read thread returned now\n");
}
void decompressFunc(void* d) {
log_write("hello decomp thread func\n");
auto t = static_cast<ThreadData*>(d);
t->SetDecompressResult(t->decompressFuncInternal());
log_write("decompress thread returned now\n");
}
void writeFunc(void* d) {
auto t = static_cast<ThreadData*>(d);
t->SetWriteResult(t->writeFuncInternal());
log_write("write thread returned now\n");
}
auto GetAlternateCore(int id) {
return id == 1 ? 2 : 1;
}
Result TransferInternal(ui::ProgressBox* pbox, s64 size, const ReadCallback& rfunc, const WriteCallback& wfunc, const StartCallback2& sfunc, Mode mode, u64 buffer_size = NORMAL_BUFFER_SIZE) {
Result TransferInternal(ui::ProgressBox* pbox, s64 size, const ReadCallback& rfunc, const DecompressCallback& dfunc, const WriteCallback& wfunc, const StartCallback2& sfunc, Mode mode, u64 buffer_size = NORMAL_BUFFER_SIZE) {
const auto is_file_based_emummc = App::IsFileBaseEmummc();
if (is_file_based_emummc) {
@@ -403,27 +549,30 @@ Result TransferInternal(ui::ProgressBox* pbox, s64 size, const ReadCallback& rfu
R_SUCCEED();
}
else {
const auto WRITE_THREAD_CORE = sfunc ? pbox->GetCpuId() : GetAlternateCore(pbox->GetCpuId());
const auto READ_THREAD_CORE = GetAlternateCore(WRITE_THREAD_CORE);
ThreadData t_data{pbox, size, rfunc, wfunc, buffer_size};
ThreadData t_data{pbox, size, rfunc, dfunc, wfunc, buffer_size};
Thread t_read{};
R_TRY(threadCreate(&t_read, readFunc, std::addressof(t_data), nullptr, 1024*256, 0x3B, READ_THREAD_CORE));
R_TRY(utils::CreateThread(&t_read, readFunc, std::addressof(t_data)));
ON_SCOPE_EXIT(threadClose(&t_read));
Thread t_decompress{};
R_TRY(utils::CreateThread(&t_decompress, decompressFunc, std::addressof(t_data)));
ON_SCOPE_EXIT(threadClose(&t_decompress));
Thread t_write{};
R_TRY(threadCreate(&t_write, writeFunc, std::addressof(t_data), nullptr, 1024*256, 0x3B, WRITE_THREAD_CORE));
R_TRY(utils::CreateThread(&t_write, writeFunc, std::addressof(t_data)));
ON_SCOPE_EXIT(threadClose(&t_write));
const auto start_threads = [&]() -> Result {
log_write("starting threads\n");
R_TRY(threadStart(std::addressof(t_read)));
R_TRY(threadStart(std::addressof(t_decompress)));
R_TRY(threadStart(std::addressof(t_write)));
R_SUCCEED();
};
ON_SCOPE_EXIT(threadWaitForExit(std::addressof(t_read)));
ON_SCOPE_EXIT(threadWaitForExit(std::addressof(t_decompress)));
ON_SCOPE_EXIT(threadWaitForExit(std::addressof(t_write)));
if (sfunc) {
@@ -463,6 +612,8 @@ Result TransferInternal(ui::ProgressBox* pbox, s64 size, const ReadCallback& rfu
if (R_FAILED(waitSingleHandle(t_read.handle, 1000))) {
continue;
} else if (R_FAILED(waitSingleHandle(t_decompress.handle, 1000))) {
continue;
} else if (R_FAILED(waitSingleHandle(t_write.handle, 1000))) {
continue;
}
@@ -485,18 +636,22 @@ Result TransferInternal(ui::ProgressBox* pbox, s64 size, const ReadCallback& rfu
} // namespace
Result Transfer(ui::ProgressBox* pbox, s64 size, const ReadCallback& rfunc, const WriteCallback& wfunc, Mode mode) {
return TransferInternal(pbox, size, rfunc, wfunc, nullptr, mode);
return TransferInternal(pbox, size, rfunc, nullptr, wfunc, nullptr, mode);
}
Result Transfer(ui::ProgressBox* pbox, s64 size, const ReadCallback& rfunc, const DecompressCallback& dfunc, const WriteCallback& wfunc, Mode mode) {
return TransferInternal(pbox, size, rfunc, dfunc, wfunc, nullptr, mode);
}
Result TransferPull(ui::ProgressBox* pbox, s64 size, const ReadCallback& rfunc, const StartCallback& sfunc, Mode mode) {
return TransferInternal(pbox, size, rfunc, nullptr, [sfunc](StartThreadCallback start, PullCallback pull) -> Result {
return TransferInternal(pbox, size, rfunc, nullptr, nullptr, [sfunc](StartThreadCallback start, PullCallback pull) -> Result {
R_TRY(start());
return sfunc(pull);
}, mode);
}
Result TransferPull(ui::ProgressBox* pbox, s64 size, const ReadCallback& rfunc, const StartCallback2& sfunc, Mode mode) {
return TransferInternal(pbox, size, rfunc, nullptr, sfunc, mode);
return TransferInternal(pbox, size, rfunc, nullptr, nullptr, sfunc, mode);
}
Result TransferUnzip(ui::ProgressBox* pbox, void* zfile, fs::Fs* fs, const fs::FsPath& path, s64 size, u32 crc32, Mode mode) {
@@ -537,6 +692,7 @@ Result TransferUnzip(ui::ProgressBox* pbox, void* zfile, fs::Fs* fs, const fs::F
*bytes_read = result;
R_SUCCEED();
},
nullptr,
[&](const void* data, s64 off, s64 size) -> Result {
return f.Write(off, data, size, FsWriteOption_None);
},
@@ -568,6 +724,7 @@ Result TransferZip(ui::ProgressBox* pbox, void* zfile, fs::Fs* fs, const fs::FsP
}
return rc;
},
nullptr,
[&](const void* data, s64 off, s64 size) -> Result {
if (ZIP_OK != zipWriteInFileInZip(zfile, data, size)) {
log_write("failed to write zip file: %s\n", path.s);

View File

@@ -7,6 +7,8 @@
#include "yati/nx/nca.hpp"
#include "yati/nx/ncm.hpp"
#include "utils/thread.hpp"
#include <cstring>
#include <atomic>
#include <ranges>
@@ -18,9 +20,6 @@
namespace sphaira::title {
namespace {
constexpr int THREAD_PRIO = PRIO_PREEMPTIVE;
constexpr int THREAD_CORE = 1;
struct ThreadData {
ThreadData(bool title_cache);
@@ -383,8 +382,7 @@ Result Init() {
}
g_thread_data = std::make_unique<ThreadData>(true);
R_TRY(threadCreate(&g_thread, ThreadFunc, g_thread_data.get(), nullptr, 1024*32, THREAD_PRIO, THREAD_CORE));
svcSetThreadCoreMask(g_thread.handle, THREAD_CORE, THREAD_AFFINITY_DEFAULT(THREAD_CORE));
R_TRY(utils::CreateThread(&g_thread, ThreadFunc, g_thread_data.get(), 1024*32));
R_TRY(threadStart(&g_thread));
}

View File

@@ -100,6 +100,7 @@ auto GetCodeMessage(Result rc) -> const char* {
case Result_ThemezerFailedToDownloadTheme: return "SphairaError_ThemezerFailedToDownloadTheme";
case Result_MainFailedToDownloadUpdate: return "SphairaError_MainFailedToDownloadUpdate";
case Result_UsbDsBadDeviceSpeed: return "SphairaError_UsbDsBadDeviceSpeed";
case Result_NcaBadMagic: return "SphairaError_NcaBadMagic";
case Result_NspBadMagic: return "SphairaError_NspBadMagic";
case Result_XciBadMagic: return "SphairaError_XciBadMagic";
case Result_XciSecurePartitionNotFound: return "SphairaError_XciSecurePartitionNotFound";
@@ -147,6 +148,16 @@ auto GetCodeMessage(Result rc) -> const char* {
case Result_YatiCertNotFound: return "SphairaError_YatiCertNotFound";
case Result_YatiNcmDbCorruptHeader: return "SphairaError_YatiNcmDbCorruptHeader";
case Result_YatiNcmDbCorruptInfos: return "SphairaError_YatiNcmDbCorruptInfos";
case Result_NszFailedCreateCctx: return "SphairaError_NszFailedCreateCctx";
case Result_NszFailedSetCompressionLevel: return "SphairaError_NszFailedSetCompressionLevel";
case Result_NszFailedSetThreadCount: return "SphairaError_NszFailedSetThreadCount";
case Result_NszFailedSetLongDistanceMode: return "SphairaError_NszFailedSetLongDistanceMode";
case Result_NszFailedResetCctx: return "SphairaError_NszFailedResetCctx";
case Result_NszFailedCompress2: return "SphairaError_NszFailedCompress2";
case Result_NszFailedCompressStream2: return "SphairaError_NszFailedCompressStream2";
case Result_NszTooManyBlocks: return "SphairaError_NszTooManyBlocks";
case Result_NszMissingBlocks: return "SphairaError_NszMissingBlocks";
}
return "";
@@ -166,7 +177,7 @@ ErrorBox::ErrorBox(const std::string& message) : m_message{message} {
SetPop();
}});
App::PlaySoundEffect(SoundEffect::SoundEffect_Error);
App::PlaySoundEffect(SoundEffect::Error);
}
ErrorBox::ErrorBox(Result code, const std::string& message) : ErrorBox{message} {

View File

@@ -72,7 +72,7 @@ auto List::ScrollDown(s64& index, s64 step, s64 count) -> bool {
}
if (index != old_index) {
App::PlaySoundEffect(SoundEffect_Scroll);
App::PlaySoundEffect(SoundEffect::Scroll);
s64 delta = index - old_index;
s64 start = m_yoff / max * m_row;
@@ -110,7 +110,7 @@ auto List::ScrollUp(s64& index, s64 step, s64 count) -> bool {
}
if (index != old_index) {
App::PlaySoundEffect(SoundEffect_Scroll);
App::PlaySoundEffect(SoundEffect::Scroll);
s64 start = m_yoff / max * m_row;
while (index < start) {

View File

@@ -577,13 +577,13 @@ EntryMenu::EntryMenu(Entry& entry, const LazyImage& default_icon, Menu& menu)
std::make_pair(Button::DPAD_DOWN | Button::RS_DOWN, Action{[this](){
if (m_index < (m_options.size() - 1)) {
SetIndex(m_index + 1);
App::PlaySoundEffect(SoundEffect_Focus);
App::PlaySoundEffect(SoundEffect::Focus);
}
}}),
std::make_pair(Button::DPAD_UP | Button::RS_UP, Action{[this](){
if (m_index != 0) {
SetIndex(m_index - 1);
App::PlaySoundEffect(SoundEffect_Focus);
App::PlaySoundEffect(SoundEffect::Focus);
}
}}),
std::make_pair(Button::X, Action{"Options"_i18n, [this](){
@@ -1007,7 +1007,7 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
if (touch && m_index == i) {
FireAction(Button::A);
} else {
App::PlaySoundEffect(SoundEffect_Focus);
App::PlaySoundEffect(SoundEffect::Focus);
SetIndex(i);
}
});

View File

@@ -6,13 +6,16 @@ namespace {
} // namespace
Menu::Menu(const fs::FsPath& path) : MenuBase{path, MenuFlag_None}, m_path{path} {
Menu::Menu(fs::Fs* fs, const fs::FsPath& path)
: MenuBase{path, MenuFlag_None}
, m_fs{fs}
, m_path{path} {
SetAction(Button::B, Action{"Back"_i18n, [this](){
SetPop();
}});
std::string buf;
if (R_SUCCEEDED(m_fs.OpenFile(m_path, FsOpenMode_Read, &m_file))) {
if (R_SUCCEEDED(m_fs->OpenFile(m_path, FsOpenMode_Read, &m_file))) {
m_file.GetSize(&m_file_size);
buf.resize(m_file_size + 1);

View File

@@ -1,11 +1,14 @@
#include "ui/menus/filebrowser.hpp"
#include "ui/menus/homebrew.hpp"
#include "ui/menus/file_viewer.hpp"
#include "ui/menus/image_viewer.hpp"
#include "ui/sidebar.hpp"
#include "ui/option_box.hpp"
#include "ui/popup_list.hpp"
#include "ui/progress_box.hpp"
#include "ui/error_box.hpp"
#include "ui/menus/file_viewer.hpp"
#include "ui/music_player.hpp"
#include "utils/devoptab.hpp"
@@ -28,6 +31,7 @@
#include "yati/yati.hpp"
#include "yati/source/file.hpp"
#include <usbdvd.h>
#include <minIni.h>
#include <minizip/zip.h>
#include <minizip/unzip.h>
@@ -80,10 +84,10 @@ constexpr FsEntry FS_ENTRIES[]{
constexpr std::string_view AUDIO_EXTENSIONS[] = {
"mp3", "ogg", "flac", "wav", "aac" "ac3", "aif", "asf", "bfwav",
"bfsar", "bfstm",
"bfsar", "bfstm", "bwav",
};
constexpr std::string_view VIDEO_EXTENSIONS[] = {
"mp4", "mkv", "m3u", "m3u8", "hls", "vob", "avi", "dv", "flv", "m2ts",
"mp4", "mkv", "m3u", "m3u8", "hls", "vob", "avi", "dv", "flv", "m2ts", "webm",
"m2v", "m4a", "mov", "mpeg", "mpg", "mts", "swf", "ts", "vob", "wma", "wmv",
};
constexpr std::string_view IMAGE_EXTENSIONS[] = {
@@ -98,6 +102,9 @@ constexpr std::string_view NSP_EXTENSIONS[] = {
constexpr std::string_view XCI_EXTENSIONS[] = {
"xci", "xcz",
};
constexpr std::string_view NCA_EXTENSIONS[] = {
"nca", "ncz",
};
// these are files that are already compressed or encrypted and should
// be stored raw in a zip file.
constexpr std::string_view COMPRESSED_EXTENSIONS[] = {
@@ -106,6 +113,16 @@ constexpr std::string_view COMPRESSED_EXTENSIONS[] = {
constexpr std::string_view ZIP_EXTENSIONS[] = {
"zip",
};
// supported music playback extensions.
constexpr std::string_view MUSIC_EXTENSIONS[] = {
"bfstm", "bfwav", "wav", "mp3", "ogg", "adf",
};
// supported theme music playback extensions.
constexpr std::span THEME_MUSIC_EXTENSIONS = MUSIC_EXTENSIONS;
constexpr std::string_view CDDVD_EXTENSIONS[] = {
"iso", "cue",
};
struct RomDatabaseEntry {
// uses the naming scheme from retropie.
@@ -473,8 +490,10 @@ FsView::FsView(Base* menu, const std::shared_ptr<fs::Fs>& fs, const fs::FsPath&
}})
);
log_write("setting side\n");
SetSide(m_side);
log_write("getting path\n");
auto buf = path;
if (path.empty() && entry.IsSd()) {
ini_gets("paths", "last_path", entry.root, buf, sizeof(buf), App::CONFIG_PATH);
@@ -485,7 +504,9 @@ FsView::FsView(Base* menu, const std::shared_ptr<fs::Fs>& fs, const fs::FsPath&
buf = entry.root;
}
log_write("setting fs\n");
SetFs(fs, buf, entry);
log_write("set fs\n");
}
FsView::FsView(FsView* view, ViewSide side) : FsView{view->m_menu, view->m_fs, view->m_path, view->m_fs_entry, side} {
@@ -509,7 +530,7 @@ void FsView::Update(Controller* controller, TouchInfo* touch) {
if (touch && m_index == i) {
FireAction(Button::A);
} else {
App::PlaySoundEffect(SoundEffect_Focus);
App::PlaySoundEffect(SoundEffect::Focus);
SetIndex(i);
}
});
@@ -574,8 +595,9 @@ void FsView::Draw(NVGcontext* vg, Theme* theme) {
if (e.IsDir()) {
// NOTE: this takes longer than 16ms when opening a new folder due to it
// checking all 9 folders at once.
if (!got_dir_count && e.file_count == -1 && e.dir_count == -1) {
if (!got_dir_count && !e.done_stat && e.file_count == -1 && e.dir_count == -1) {
got_dir_count = true;
e.done_stat = true;
m_fs->DirGetEntryCount(GetNewPath(e), &e.file_count, &e.dir_count);
}
@@ -586,7 +608,8 @@ void FsView::Draw(NVGcontext* vg, Theme* theme) {
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) + 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_TOP, theme->GetColour(text_id), "%zd dirs"_i18n.c_str(), e.dir_count);
}
} else if (e.IsFile()) {
if (!e.time_stamp.is_valid) {
if (!e.time_stamp.is_valid && !e.done_stat) {
e.done_stat = true;
const auto path = GetNewPath(e);
if (m_fs->IsNative()) {
m_fs->GetFileTimeStampRaw(path, &e.time_stamp);
@@ -686,12 +709,44 @@ void FsView::OnClick() {
nro_launch(GetNewPathCurrent());
}
});
} else if (IsExtension(entry.GetExtension(), NCA_EXTENSIONS)) {
MountFileFs(devoptab::MountNca, devoptab::UmountNca);
} else if (IsExtension(entry.GetExtension(), NSP_EXTENSIONS)) {
MountNspFs();
MountFileFs(devoptab::MountNsp, devoptab::UmountNsp);
} else if (IsExtension(entry.GetExtension(), XCI_EXTENSIONS)) {
MountXciFs();
MountFileFs(devoptab::MountXci, devoptab::UmountXci);
} else if (IsExtension(entry.GetExtension(), "zip")) {
MountZipFs();
MountFileFs(devoptab::MountZip, devoptab::UmountZip);
} else if (IsExtension(entry.GetExtension(), "bfsar")) {
MountFileFs(devoptab::MountBfsar, devoptab::UmountBfsar);
} else if (IsExtension(entry.GetExtension(), MUSIC_EXTENSIONS)) {
App::Push<music::Menu>(GetFs(), GetNewPathCurrent());
} else if (IsExtension(entry.GetExtension(), IMAGE_EXTENSIONS)) {
App::Push<imageview::Menu>(GetFs(), GetNewPathCurrent());
} else if (IsExtension(entry.GetExtension(), CDDVD_EXTENSIONS)) {
std::shared_ptr<CUSBDVD> usbdvd;
if (entry.GetExtension() == "cue") {
const auto cue_path = GetNewPathCurrent();
fs::FsPath bin_path = cue_path;
std::strcpy(std::strstr(bin_path, ".cue"), ".bin");
if (m_fs->FileExists(bin_path)) {
usbdvd = std::make_shared<CUSBDVD>(cue_path, bin_path);
}
} else {
usbdvd = std::make_shared<CUSBDVD>(GetNewPathCurrent());
}
if (usbdvd && usbdvd->usbdvd_drive_ctx.fs.mounted) {
auto fs = std::make_shared<FsStdioWrapper>(usbdvd->usbdvd_drive_ctx.fs.mountpoint, [usbdvd](){
// dummy func to keep shared_ptr alive until fs is closed.
});
MountFsHelper(fs, usbdvd->usbdvd_drive_ctx.fs.disc_fstype);
log_write("[USBDVD] mounted\n");
} else {
log_write("[USBDVD] failed to mount\n");
}
} else if (IsExtension(entry.GetExtension(), INSTALL_EXTENSIONS)) {
InstallFiles();
} else if (IsSd()) {
@@ -1926,9 +1981,9 @@ void FsView::DisplayAdvancedOptions() {
});
}
if (IsSd() && m_entries_current.size() && !m_selected_count && GetEntry().IsFile() && GetEntry().file_size < 1024*64) {
if (m_entries_current.size() && !m_selected_count && GetEntry().IsFile() && GetEntry().file_size < 1024*64) {
options->Add<SidebarEntryCallback>("View as text (unfinished)"_i18n, [this](){
App::Push<fileview::Menu>(GetNewPathCurrent());
App::Push<fileview::Menu>(GetFs(), GetNewPathCurrent());
});
}
@@ -1938,6 +1993,13 @@ void FsView::DisplayAdvancedOptions() {
});
}
if (m_entries_current.size() && !m_selected_count && IsExtension(GetEntry().GetExtension(), THEME_MUSIC_EXTENSIONS)) {
options->Add<SidebarEntryCallback>("Set as background music"_i18n, [this](){
const auto rc = App::SetDefaultBackgroundMusic(GetFs(), GetNewPathCurrent());
App::PushErrorBox(rc, "Failed to set default music path"_i18n);
});
}
if (m_entries_current.size() && !m_selected_count && GetEntry().IsFile()) {
options->Add<SidebarEntryCallback>("Hash"_i18n, [this](){
auto options = std::make_unique<Sidebar>("Hash Options"_i18n, Sidebar::Side::RIGHT);
@@ -1955,6 +2017,15 @@ void FsView::DisplayAdvancedOptions() {
options->Add<SidebarEntryCallback>("SHA256"_i18n, [this](){
DisplayHash(hash::Type::Sha256);
});
options->Add<SidebarEntryCallback>("/dev/null (Speed Test)"_i18n, [this](){
DisplayHash(hash::Type::Null);
});
options->Add<SidebarEntryCallback>("Deflate (Speed Test)"_i18n, [this](){
DisplayHash(hash::Type::Deflate);
});
options->Add<SidebarEntryCallback>("ZSTD (Speed Test)"_i18n, [this](){
DisplayHash(hash::Type::Zstd);
});
});
}
@@ -1964,42 +2035,14 @@ void FsView::DisplayAdvancedOptions() {
});
}
void FsView::MountNspFs() {
void FsView::MountFileFs(const MountFsFunc& mount_func, const UmountFsFunc& umount_func) {
fs::FsPath mount;
const auto rc = devoptab::MountNsp(GetFs(), GetNewPathCurrent(), mount);
App::PushErrorBox(rc, "Failed to mount NSP."_i18n);
const auto rc = mount_func(GetFs(), GetNewPathCurrent(), mount);
App::PushErrorBox(rc, "Failed to mount FS."_i18n);
if (R_SUCCEEDED(rc)) {
auto fs = std::make_shared<FsStdioWrapper>(mount, [mount](){
devoptab::UmountNsp(mount);
});
MountFsHelper(fs, GetEntry().GetName());
}
}
void FsView::MountXciFs() {
fs::FsPath mount;
const auto rc = devoptab::MountXci(GetFs(), GetNewPathCurrent(), mount);
App::PushErrorBox(rc, "Failed to mount XCI."_i18n);
if (R_SUCCEEDED(rc)) {
auto fs = std::make_shared<FsStdioWrapper>(mount, [mount](){
devoptab::UmountXci(mount);
});
MountFsHelper(fs, GetEntry().GetName());
}
}
void FsView::MountZipFs() {
fs::FsPath mount;
const auto rc = devoptab::MountZip(GetFs(), GetNewPathCurrent(), mount);
App::PushErrorBox(rc, "Failed to mount zip."_i18n);
if (R_SUCCEEDED(rc)) {
auto fs = std::make_shared<FsStdioWrapper>(mount, [mount](){
devoptab::UmountZip(mount);
auto fs = std::make_shared<FsStdioWrapper>(mount, [mount, umount_func](){
umount_func(mount);
});
MountFsHelper(fs, GetEntry().GetName());
@@ -2007,32 +2050,15 @@ void FsView::MountZipFs() {
}
Base::Base(u32 flags, u32 options)
: Base{CreateFs(FS_ENTRY_DEFAULT), FS_ENTRY_DEFAULT, {}, false, flags, options} {
: MenuBase{"FileBrowser"_i18n, flags}
, m_options{options} {
Init(CreateFs(FS_ENTRY_DEFAULT), FS_ENTRY_DEFAULT, {}, false);
}
Base::Base(const std::shared_ptr<fs::Fs>& fs, const FsEntry& fs_entry, const fs::FsPath& path, bool is_custom, u32 flags, u32 options)
: MenuBase{"FileBrowser"_i18n, flags}
, m_options{options} {
if (m_options & FsOption_CanSplit) {
SetAction(Button::L3, Action{"Split"_i18n, [this](){
SetSplitScreen(IsSplitScreen() ^ 1);
}});
}
if (!IsTab()) {
SetAction(Button::SELECT, Action{"Close"_i18n, [this](){
PromptIfShouldExit();
}});
}
if (is_custom) {
m_custom_fs = fs;
m_custom_fs_entry = fs_entry;
}
view_left = std::make_unique<FsView>(this, fs, path, fs_entry, ViewSide::Left);
view = view_left.get();
ueventCreate(&g_change_uevent, true);
Init(fs, fs_entry, path, is_custom);
}
void Base::Update(Controller* controller, TouchInfo* touch) {
@@ -2355,6 +2381,31 @@ auto Base::CreateFs(const FsEntry& fs_entry) -> std::shared_ptr<fs::Fs> {
std::unreachable();
}
void Base::Init(const std::shared_ptr<fs::Fs>& fs, const FsEntry& fs_entry, const fs::FsPath& path, bool is_custom) {
if (m_options & FsOption_CanSplit) {
SetAction(Button::L3, Action{"Split"_i18n, [this](){
SetSplitScreen(IsSplitScreen() ^ 1);
}});
}
if (!IsTab()) {
SetAction(Button::SELECT, Action{"Close"_i18n, [this](){
PromptIfShouldExit();
}});
}
if (is_custom) {
m_custom_fs = fs;
m_custom_fs_entry = fs_entry;
}
log_write("creating view\n");
view_left = std::make_unique<FsView>(this, fs, path, fs_entry, ViewSide::Left);
view = view_left.get();
ueventCreate(&g_change_uevent, true);
}
void MountFsHelper(const std::shared_ptr<fs::Fs>& fs, const fs::FsPath& name) {
const filebrowser::FsEntry fs_entry{
.name = name,

View File

@@ -1,5 +1,3 @@
#if ENABLE_NETWORK_INSTALL
#include "ui/menus/ftp_menu.hpp"
#include "app.hpp"
#include "defines.hpp"
@@ -106,5 +104,3 @@ void Menu::OnDisableInstallMode() {
}
} // namespace sphaira::ui::menu::ftp
#endif

View File

@@ -7,9 +7,13 @@
#include "image.hpp"
#include "swkbd.hpp"
#include "utils/utils.hpp"
#include "utils/nsz_dumper.hpp"
#include "ui/menus/game_menu.hpp"
#include "ui/menus/game_meta_menu.hpp"
#include "ui/menus/save_menu.hpp"
#include "ui/menus/gc_menu.hpp" // remove when gc event pr is merged.
#include "ui/sidebar.hpp"
#include "ui/error_box.hpp"
#include "ui/option_box.hpp"
@@ -50,6 +54,11 @@ struct NspSource final : dump::BaseSource {
return rc;
}
Result Read(const std::string& path, void* buf, s64 off, s64 size) {
u64 bytes_read = 0;
return Read(path, buf, off, size, &bytes_read);
}
auto GetName(const std::string& path) const -> std::string {
const auto it = std::ranges::find_if(m_entries, [&path](auto& e){
return path.find(e.path.s) != path.npos;
@@ -86,11 +95,57 @@ struct NspSource final : dump::BaseSource {
return App::GetDefaultImage();
}
Result GetEntryFromPath(const std::string& path, NspEntry& out) const {
const auto it = std::ranges::find_if(m_entries, [&path](auto& e){
return path.find(e.path.s) != path.npos;
});
R_UNLESS(it != m_entries.end(), Result_GameBadReadForDump);
out = *it;
R_SUCCEED();
}
private:
std::vector<NspEntry> m_entries{};
bool m_is_file_based_emummc{};
};
Result NszExport(ProgressBox* pbox, const keys::Keys& keys, dump::BaseSource* _source, dump::WriteSource* writer, const fs::FsPath& path) {
auto source = (NspSource*)_source;
NspEntry entry;
R_TRY(source->GetEntryFromPath(path, entry));
const auto nca_creator = [&entry](const nca::Header& header, const keys::KeyEntry& title_key, const utils::nsz::Collection& collection) {
const auto content_id = ncm::GetContentIdFromStr(collection.name.c_str());
return std::make_unique<nca::NcaReader>(
header, &title_key, collection.size,
std::make_shared<ncm::NcmSource>(&entry.cs, &content_id)
);
};
auto& collections = entry.collections;
s64 read_offset = entry.nsp_data.size();
s64 write_offset = entry.nsp_data.size();
R_TRY(utils::nsz::NszExport(pbox, nca_creator, read_offset, write_offset, collections, keys, source, writer, path));
// zero base the offsets.
for (auto& collection : collections) {
collection.offset -= entry.nsp_data.size();
}
// build new nsp collection with the updated offsets and sizes.
s64 nsp_size = 0;
const auto nsp_data = yati::container::Nsp::Build(collections, nsp_size);
R_TRY(writer->Write(nsp_data.data(), 0, nsp_data.size()));
// update with actual size.
R_TRY(writer->SetSize(nsp_size));
R_SUCCEED();
}
Result Notify(Result rc, const std::string& error_message) {
if (R_FAILED(rc)) {
App::Push<ui::ErrorBox>(rc,
@@ -135,18 +190,6 @@ void LoadControlEntry(Entry& e, bool force_image_load = false) {
}
}
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;
}
void FreeEntry(NVGcontext* vg, Entry& e) {
nvgDeleteImage(vg, e.image);
e.image = 0;
@@ -324,26 +367,15 @@ Menu::Menu(u32 flags) : grid::Menu{"Games"_i18n, flags} {
);
});
options->Add<SidebarEntryCallback>("Export NSP"_i18n, [this](){
auto options = std::make_unique<Sidebar>("Select content to export"_i18n, Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(std::move(options)));
auto export_nsp = options->Add<SidebarEntryCallback>("Export NSP"_i18n, [this](){
ExportOptions(false);
});
export_nsp->Depends(App::IsApplication, "Not supported in Applet Mode"_i18n);
options->Add<SidebarEntryCallback>("Export All"_i18n, [this](){
DumpGames(title::ContentFlag_All);
}, true);
options->Add<SidebarEntryCallback>("Export Application"_i18n, [this](){
DumpGames(title::ContentFlag_Application);
}, true);
options->Add<SidebarEntryCallback>("Export Patch"_i18n, [this](){
DumpGames(title::ContentFlag_Patch);
}, true);
options->Add<SidebarEntryCallback>("Export AddOnContent"_i18n, [this](){
DumpGames(title::ContentFlag_AddOnContent);
}, true);
options->Add<SidebarEntryCallback>("Export DataPatch"_i18n, [this](){
DumpGames(title::ContentFlag_DataPatch);
}, true);
}, true);
auto export_nsz = options->Add<SidebarEntryCallback>("Export NSZ"_i18n, [this](){
ExportOptions(true);
}, "Exports to NSZ (compressed NSP)"_i18n);
export_nsz->Depends(App::IsApplication, "Not supported in Applet Mode"_i18n);
options->Add<SidebarEntryCallback>("Export options"_i18n, [this](){
App::DisplayDumpOptions(false);
@@ -418,6 +450,9 @@ Menu::Menu(u32 flags) : grid::Menu{"Games"_i18n, flags} {
ns::Initialize();
es::Initialize();
title::Init();
fsOpenGameCardDetectionEventNotifier(std::addressof(m_gc_event_notifier));
fsEventNotifierGetEventHandle(std::addressof(m_gc_event_notifier), std::addressof(m_gc_event), true);
}
Menu::~Menu() {
@@ -426,9 +461,15 @@ Menu::~Menu() {
FreeEntries();
ns::Exit();
es::Exit();
eventClose(std::addressof(m_gc_event));
fsEventNotifierClose(std::addressof(m_gc_event_notifier));
}
void Menu::Update(Controller* controller, TouchInfo* touch) {
// force update if gamecard state changed.
m_dirty |= R_SUCCEEDED(eventWait(&m_gc_event, 0));
if (m_dirty) {
App::Notify("Updating application record list"_i18n);
SortAndFindLastFile(true);
@@ -439,7 +480,7 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
if (touch && m_index == i) {
FireAction(Button::A);
} else {
App::PlaySoundEffect(SoundEffect_Focus);
App::PlaySoundEffect(SoundEffect::Focus);
SetIndex(i);
}
});
@@ -538,7 +579,7 @@ void Menu::ScanHomebrew() {
continue;
}
m_entries.emplace_back(e.application_id, e.type);
m_entries.emplace_back(e.application_id, e.last_event);
}
offset += record_count;
@@ -640,16 +681,37 @@ void Menu::DeleteGames() {
});
}
void Menu::DumpGames(u32 flags) {
void Menu::ExportOptions(bool to_nsz) {
auto options = std::make_unique<Sidebar>("Select content to export"_i18n, Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(std::move(options)));
options->Add<SidebarEntryCallback>("Export All"_i18n, [this, to_nsz](){
DumpGames(title::ContentFlag_All, to_nsz);
}, true);
options->Add<SidebarEntryCallback>("Export Application"_i18n, [this, to_nsz](){
DumpGames(title::ContentFlag_Application, to_nsz);
}, true);
options->Add<SidebarEntryCallback>("Export Patch"_i18n, [this, to_nsz](){
DumpGames(title::ContentFlag_Patch, to_nsz);
}, true);
options->Add<SidebarEntryCallback>("Export AddOnContent"_i18n, [this, to_nsz](){
DumpGames(title::ContentFlag_AddOnContent, to_nsz);
}, true);
options->Add<SidebarEntryCallback>("Export DataPatch"_i18n, [this, to_nsz](){
DumpGames(title::ContentFlag_DataPatch, to_nsz);
}, true);
}
void Menu::DumpGames(u32 flags, bool to_nsz) {
auto targets = GetSelectedEntries();
ClearSelection();
std::vector<NspEntry> nsp_entries;
for (auto& e : targets) {
BuildNspEntries(e, flags, nsp_entries);
BuildNspEntries(e, flags, nsp_entries, to_nsz);
}
DumpNsp(nsp_entries);
DumpNsp(nsp_entries, to_nsz);
}
void Menu::CreateSaves(AccountUid uid) {
@@ -762,7 +824,7 @@ void DeleteMetaEntries(u64 app_id, int image, const std::string& name, const tit
});
}
auto BuildNspPath(const Entry& e, const NsApplicationContentMetaStatus& status) -> fs::FsPath {
auto BuildNspPath(const Entry& e, const NsApplicationContentMetaStatus& status, bool to_nsz) -> fs::FsPath {
fs::FsPath name_buf = e.GetName();
title::utilsReplaceIllegalCharacters(name_buf, true);
@@ -778,17 +840,19 @@ auto BuildNspPath(const Entry& e, const NsApplicationContentMetaStatus& status)
}
}
const auto ext = to_nsz ? "nsz" : "nsp";
fs::FsPath path;
if (App::GetApp()->m_dump_app_folder.Get()) {
std::snprintf(path, sizeof(path), "%s/%s %s[%016lX][v%u][%s].nsp", name_buf.s, name_buf.s, version, status.application_id, status.version, ncm::GetMetaTypeShortStr(status.meta_type));
std::snprintf(path, sizeof(path), "%s/%s %s[%016lX][v%u][%s].%s", name_buf.s, name_buf.s, version, status.application_id, status.version, ncm::GetMetaTypeShortStr(status.meta_type), ext);
} else {
std::snprintf(path, sizeof(path), "%s %s[%016lX][v%u][%s].nsp", name_buf.s, version, status.application_id, status.version, ncm::GetMetaTypeShortStr(status.meta_type));
std::snprintf(path, sizeof(path), "%s %s[%016lX][v%u][%s].%s", name_buf.s, version, status.application_id, status.version, ncm::GetMetaTypeShortStr(status.meta_type), ext);
}
return path;
}
Result BuildContentEntry(const NsApplicationContentMetaStatus& status, ContentInfoEntry& out) {
Result BuildContentEntry(const NsApplicationContentMetaStatus& status, ContentInfoEntry& out, bool to_nsz) {
NcmMetaData meta;
R_TRY(GetNcmMetaFromMetaStatus(status, meta));
@@ -824,14 +888,14 @@ Result BuildContentEntry(const NsApplicationContentMetaStatus& status, ContentIn
R_SUCCEED();
}
Result BuildNspEntry(const Entry& e, const ContentInfoEntry& info, const keys::Keys& keys, NspEntry& out) {
Result BuildNspEntry(const Entry& e, const ContentInfoEntry& info, const keys::Keys& keys, NspEntry& out, bool to_nsz) {
out.application_name = e.GetName();
out.path = BuildNspPath(e, info.status);
out.path = BuildNspPath(e, info.status, to_nsz);
s64 offset{};
for (auto& e : info.content_infos) {
char nca_name[64];
std::snprintf(nca_name, sizeof(nca_name), "%s%s", hexIdToStr(e.content_id).str, e.content_type == NcmContentType_Meta ? ".cnmt.nca" : ".nca");
std::snprintf(nca_name, sizeof(nca_name), "%s%s", utils::hexIdToStr(e.content_id).str, e.content_type == NcmContentType_Meta ? ".cnmt.nca" : ".nca");
u64 size;
ncmContentInfoSizeToU64(std::addressof(e), std::addressof(size));
@@ -856,10 +920,10 @@ Result BuildNspEntry(const Entry& e, const ContentInfoEntry& info, const keys::K
R_TRY(es::PatchTicket(entry.tik_data, entry.cert_data, key_gen, keys, App::GetApp()->m_dump_convert_to_common_ticket.Get()));
char tik_name[64];
std::snprintf(tik_name, sizeof(tik_name), "%s%s", hexIdToStr(rights_id).str, ".tik");
std::snprintf(tik_name, sizeof(tik_name), "%s%s", utils::hexIdToStr(rights_id).str, ".tik");
char cert_name[64];
std::snprintf(cert_name, sizeof(cert_name), "%s%s", hexIdToStr(rights_id).str, ".cert");
std::snprintf(cert_name, sizeof(cert_name), "%s%s", utils::hexIdToStr(rights_id).str, ".cert");
out.collections.emplace_back(tik_name, offset, entry.tik_data.size());
offset += entry.tik_data.size();
@@ -876,7 +940,7 @@ Result BuildNspEntry(const Entry& e, const ContentInfoEntry& info, const keys::K
R_SUCCEED();
}
Result BuildNspEntries(Entry& e, const title::MetaEntries& meta_entries, std::vector<NspEntry>& out) {
Result BuildNspEntries(Entry& e, const title::MetaEntries& meta_entries, std::vector<NspEntry>& out, bool to_nsz) {
LoadControlEntry(e);
keys::Keys keys;
@@ -887,7 +951,7 @@ Result BuildNspEntries(Entry& e, const title::MetaEntries& meta_entries, std::ve
R_TRY(BuildContentEntry(status, info));
NspEntry nsp;
R_TRY(BuildNspEntry(e, info, keys, nsp));
R_TRY(BuildNspEntry(e, info, keys, nsp, to_nsz));
out.emplace_back(nsp).icon = e.image;
}
@@ -895,21 +959,36 @@ Result BuildNspEntries(Entry& e, const title::MetaEntries& meta_entries, std::ve
R_SUCCEED();
}
Result BuildNspEntries(Entry& e, u32 flags, std::vector<NspEntry>& out) {
Result BuildNspEntries(Entry& e, u32 flags, std::vector<NspEntry>& out, bool to_nsz) {
title::MetaEntries meta_entries;
R_TRY(GetMetaEntries(e, meta_entries, flags));
return BuildNspEntries(e, meta_entries, out);
return BuildNspEntries(e, meta_entries, out, to_nsz);
}
void DumpNsp(const std::vector<NspEntry>& entries) {
void DumpNsp(const std::vector<NspEntry>& entries, bool to_nsz) {
std::vector<fs::FsPath> paths;
for (auto& e : entries) {
paths.emplace_back(fs::AppendPath("/dumps/NSP", e.path));
if (to_nsz) {
paths.emplace_back(fs::AppendPath("/dumps/NSZ", e.path));
} else {
paths.emplace_back(fs::AppendPath("/dumps/NSP", e.path));
}
}
auto source = std::make_shared<NspSource>(entries);
dump::Dump(source, paths);
if (to_nsz) {
// todo: log keys error.
keys::Keys keys;
keys::parse_keys(keys, true);
dump::Dump(source, paths, [keys](ProgressBox* pbox, dump::BaseSource* source, dump::WriteSource* writer, const fs::FsPath& path) {
return NszExport(pbox, keys, source, writer, path);
});
} else {
dump::Dump(source, paths);
}
}
} // namespace sphaira::ui::menu::game

View File

@@ -86,7 +86,11 @@ Menu::Menu(Entry& entry) : MenuBase{entry.GetName(), MenuFlag_None}, m_entry{ent
if (!m_entries.empty()) {
options->Add<SidebarEntryCallback>("Export NSP"_i18n, [this](){
DumpGames();
DumpGames(false);
});
options->Add<SidebarEntryCallback>("Export NSZ"_i18n, [this](){
DumpGames(true);
});
options->Add<SidebarEntryCallback>("Export options"_i18n, [this](){
@@ -153,7 +157,7 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
if (touch && m_index == i) {
FireAction(Button::A);
} else {
App::PlaySoundEffect(SoundEffect_Focus);
App::PlaySoundEffect(SoundEffect::Focus);
SetIndex(i);
}
});
@@ -345,14 +349,14 @@ Result Menu::GetNcmSizeOfMetaStatus(MetaEntry& entry) const {
R_SUCCEED();
}
void Menu::DumpGames() {
void Menu::DumpGames(bool to_nsz) {
const auto entries = GetSelectedEntries();
App::PopToMenu();
std::vector<NspEntry> nsps;
BuildNspEntries(m_entry, entries, nsps);
BuildNspEntries(m_entry, entries, nsps, to_nsz);
DumpNsp(nsps);
DumpNsp(nsps, to_nsz);
}
void Menu::DeleteGames() {

View File

@@ -11,6 +11,9 @@
#include "yati/nx/keys.hpp"
#include "yati/nx/crypto.hpp"
#include "utils/utils.hpp"
#include "utils/devoptab.hpp"
#include "title_info.hpp"
#include "app.hpp"
#include "dumper.hpp"
@@ -26,18 +29,6 @@
namespace sphaira::ui::menu::game::meta_nca {
namespace {
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;
}
struct NcaHashSource final : hash::BaseSource {
NcaHashSource(NcmContentStorage* cs, const NcaEntry& entry) : m_cs{cs}, m_entry{entry} {
}
@@ -67,7 +58,7 @@ struct NcaSource final : dump::BaseSource {
Result Read(const std::string& path, void* buf, s64 off, s64 size, u64* bytes_read) override {
const auto it = std::ranges::find_if(m_entries, [&path](auto& e){
return path.find(hexIdToStr(e.content_id).str) != path.npos;
return path.find(utils::hexIdToStr(e.content_id).str) != path.npos;
});
R_UNLESS(it != m_entries.end(), Result_GameBadReadForDump);
@@ -85,11 +76,11 @@ struct NcaSource final : dump::BaseSource {
auto GetName(const std::string& path) const -> std::string {
const auto it = std::ranges::find_if(m_entries, [&path](auto& e){
return path.find(hexIdToStr(e.content_id).str) != path.npos;
return path.find(utils::hexIdToStr(e.content_id).str) != path.npos;
});
if (it != m_entries.end()) {
return hexIdToStr(it->content_id).str;
return utils::hexIdToStr(it->content_id).str;
}
return {};
@@ -97,7 +88,7 @@ struct NcaSource final : dump::BaseSource {
auto GetSize(const std::string& path) const -> s64 {
const auto it = std::ranges::find_if(m_entries, [&path](auto& e){
return path.find(hexIdToStr(e.content_id).str) != path.npos;
return path.find(utils::hexIdToStr(e.content_id).str) != path.npos;
});
if (it != m_entries.end()) {
@@ -172,7 +163,8 @@ Menu::Menu(Entry& entry, const meta::MetaEntry& meta_entry)
std::make_pair(Button::A, Action{"Mount Fs"_i18n, [this](){
// todo: handle error here.
if (!m_entries.empty() && !GetEntry().missing) {
MountNcaFs();
const auto rc = MountNcaFs();
App::PushErrorBox(rc, "Failed to mount NCA"_i18n);
}
}}),
std::make_pair(Button::B, Action{"Back"_i18n, [this](){
@@ -187,16 +179,23 @@ Menu::Menu(Entry& entry, const meta::MetaEntry& meta_entry)
DumpNcas();
});
// todo:
#if 0
options->Add<SidebarEntryCallback>("Export NCA decrypted"_i18n, [this](){
DumpNcas();
}, "Exports the NCA with all fs sections decrypted (NCA header is still encrypted)."_i18n);
#endif
options->Add<SidebarEntryCallback>("Verify NCA 256 hash"_i18n, [this](){
static std::string hash_out;
hash_out.clear();
App::Push<ProgressBox>(m_entry.image, "Hashing"_i18n, hexIdToStr(GetEntry().content_id).str, [this](auto pbox) -> Result{
App::Push<ProgressBox>(m_entry.image, "Hashing"_i18n, utils::hexIdToStr(GetEntry().content_id).str, [this](auto pbox) -> Result{
auto source = std::make_unique<NcaHashSource>(m_meta.cs, GetEntry());
return hash::Hash(pbox, hash::Type::Sha256, source.get(), hash_out);
}, [this](Result rc){
App::PushErrorBox(rc, "Failed to hash file..."_i18n);
const auto str = hexIdToStr(GetEntry().content_id);
const auto str = utils::hexIdToStr(GetEntry().content_id);
if (R_SUCCEEDED(rc)) {
if (std::strncmp(hash_out.c_str(), str.str, std::strlen(str.str))) {
@@ -227,6 +226,7 @@ Menu::Menu(Entry& entry, const meta::MetaEntry& meta_entry)
parse_keys(keys, false);
if (R_FAILED(GetNcmMetaFromMetaStatus(m_meta_entry.status, m_meta))) {
log_write("[NCA-MENU] failed to GetNcmMetaFromMetaStatus()\n");
SetPop();
return;
}
@@ -234,6 +234,7 @@ Menu::Menu(Entry& entry, const meta::MetaEntry& meta_entry)
// get the content meta header.
ncm::ContentMeta content_meta;
if (R_FAILED(ncm::GetContentMeta(m_meta.db, &m_meta.key, content_meta))) {
log_write("[NCA-MENU] failed to ncm::GetContentMeta()\n");
SetPop();
return;
}
@@ -241,6 +242,7 @@ Menu::Menu(Entry& entry, const meta::MetaEntry& meta_entry)
// fetch all the content infos.
std::vector<NcmContentInfo> infos;
if (R_FAILED(ncm::GetContentInfos(m_meta.db, &m_meta.key, content_meta.header, infos))) {
log_write("[NCA-MENU] failed to ncm::GetContentInfos()\n");
SetPop();
return;
}
@@ -252,12 +254,17 @@ Menu::Menu(Entry& entry, const meta::MetaEntry& meta_entry)
ncmContentInfoSizeToU64(&info, &entry.size);
bool has = false;
ncmContentMetaDatabaseHasContent(m_meta.db, &has, &m_meta.key, &info.content_id);
if (R_FAILED(ncmContentMetaDatabaseHasContent(m_meta.db, &has, &m_meta.key, &info.content_id)) || !has) {
log_write("[NCA-MENU] does not have nca!\n");
}
entry.missing = !has;
// decrypt header.
if (has && R_SUCCEEDED(ncmContentStorageReadContentIdFile(m_meta.cs, &entry.header, sizeof(entry.header), &info.content_id, 0))) {
// decrypt header.
log_write("[NCA-MENU] reading to decrypt header\n");
crypto::cryptoAes128Xts(&entry.header, &entry.header, keys.header_key, 0, 0x200, sizeof(entry.header), false);
} else {
log_write("[NCA-MENU] failed to read nca from ncm\n");
}
m_entries.emplace_back(entry);
@@ -283,7 +290,7 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
if (touch && m_index == i) {
FireAction(Button::A);
} else {
App::PlaySoundEffect(SoundEffect_Focus);
App::PlaySoundEffect(SoundEffect::Focus);
SetIndex(i);
}
});
@@ -337,7 +344,7 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
}
gfx::drawTextArgs(vg, x + text_xoffset, y + (h / 2.f), 20.f, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(text_id), "%s", ncm::GetContentTypeStr(e.content_type));
gfx::drawTextArgs(vg, x + text_xoffset + 185, y + (h / 2.f), 20.f, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(text_id), "%s", hexIdToStr(e.content_id).str);
gfx::drawTextArgs(vg, x + text_xoffset + 185, y + (h / 2.f), 20.f, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(text_id), "%s", utils::hexIdToStr(e.content_id).str);
if ((double)e.size / 1024.0 / 1024.0 <= 0.009) {
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f), 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_MIDDLE, theme->GetColour(text_id), "%.2f KiB", (double)e.size / 1024.0);
@@ -384,7 +391,7 @@ void Menu::DumpNcas() {
std::vector<fs::FsPath> paths;
for (auto& e : entries) {
char nca_name[64];
std::snprintf(nca_name, sizeof(nca_name), "%s%s", hexIdToStr(e.content_id).str, e.content_type == NcmContentType_Meta ? ".cnmt.nca" : ".nca");
std::snprintf(nca_name, sizeof(nca_name), "%s%s", utils::hexIdToStr(e.content_id).str, e.content_type == NcmContentType_Meta ? ".cnmt.nca" : ".nca");
fs::FsPath path;
std::snprintf(path, sizeof(path), "/dumps/NCA/%s %s[%016lX][v%u][%s]/%s", name_buf.s, version, m_meta_entry.status.application_id, m_meta_entry.status.version, ncm::GetMetaTypeShortStr(m_meta_entry.status.meta_type), nca_name);
@@ -393,25 +400,36 @@ void Menu::DumpNcas() {
}
auto source = std::make_shared<NcaSource>(m_meta.cs, m_entry.image, entries);
dump::Dump(source, paths, [](Result){}, dump::DumpLocationFlag_All &~ dump::DumpLocationFlag_UsbS2S);
dump::Dump(source, paths, nullptr, dump::DumpLocationFlag_All &~ dump::DumpLocationFlag_UsbS2S);
}
Result Menu::MountNcaFs() {
const auto& e = GetEntry();
// mount using devoptab instead if fails.
FsFileSystemType type;
R_TRY(GetFsFileSystemType(e.header.content_type, type));
if (R_FAILED(GetFsFileSystemType(e.header.content_type, type))) {
fs::FsPath root;
R_TRY(devoptab::MountNcaNcm(m_meta.cs, &e.content_id, root));
// get fs path from ncm.
u64 program_id;
fs::FsPath path;
R_TRY(ncm::GetFsPathFromContentId(m_meta.cs, m_meta.key, e.content_id, &program_id, &path));
auto fs = std::make_shared<filebrowser::FsStdioWrapper>(root, [root](){
devoptab::UmountNca(root);
});
// ensure that mounting worked.
auto fs = std::make_shared<fs::FsNativeId>(program_id, type, path);
R_TRY(fs->GetFsOpenResult());
filebrowser::MountFsHelper(fs, utils::hexIdToStr(e.content_id).str);
} else {
// get fs path from ncm.
u64 program_id;
fs::FsPath path;
R_TRY(ncm::GetFsPathFromContentId(m_meta.cs, m_meta.key, e.content_id, &program_id, &path));
// ensure that mounting worked.
auto fs = std::make_shared<fs::FsNativeId>(program_id, type, path);
R_TRY(fs->GetFsOpenResult());
filebrowser::MountFsHelper(fs, utils::hexIdToStr(e.content_id).str);
}
filebrowser::MountFsHelper(fs, hexIdToStr(e.content_id).str);
R_SUCCEED();
}

View File

@@ -8,6 +8,11 @@
#include "yati/yati.hpp"
#include "yati/nx/nca.hpp"
#include "yati/container/xci.hpp"
#include "utils/utils.hpp"
#include "utils/nsz_dumper.hpp"
#include "utils/devoptab.hpp"
#include "app.hpp"
#include "defines.hpp"
@@ -17,16 +22,39 @@
#include "dumper.hpp"
#include "image.hpp"
#include "title_info.hpp"
#include "threaded_file_transfer.hpp"
#include <cstring>
#include <algorithm>
// from Gamecard-Installer-NX
extern "C" {
Result fsOpenGameCardStorage(FsStorage* out, const FsGameCardHandle* handle, FsGameCardPartitionRaw partition) {
const struct {
FsGameCardHandle handle;
u32 partition;
} in = { *handle, (u32)partition };
return serviceDispatchIn(fsGetServiceSession(), 30, in, .out_num_objects = 1, .out_objects = &out->s);
}
Result fsOpenGameCardDetectionEventNotifier(FsEventNotifier* out) {
return serviceDispatch(fsGetServiceSession(), 501,
.out_num_objects = 1,
.out_objects = &out->s
);
}
}
namespace sphaira::ui::menu::gc {
namespace {
constexpr u32 XCI_MAGIC = std::byteswap(0x48454144);
constexpr u32 REMOUNT_ATTEMPT_MAX = 8; // same as nxdumptool.
constexpr const char* DUMP_BASE_PATH = "/dumps/Gamecard";
constexpr const char* DUMP_GAMECARD_BASE_PATH = "/dumps/Gamecard";
constexpr const char* DUMP_XCZ_BASE_PATH = "/dumps/XCZ";
enum DumpFileType {
DumpFileType_XCI,
@@ -35,6 +63,7 @@ enum DumpFileType {
DumpFileType_UID,
DumpFileType_Cert,
DumpFileType_Initial,
DumpFileType_XCZ,
};
enum DumpFileFlag {
@@ -50,9 +79,9 @@ enum DumpFileFlag {
const char *g_option_list[] = {
"Install",
"Export",
"Mount",
"Exit",
"Export XCI (Gamecard)",
"Export XCZ (Compressed XCI)",
"Mount Fs",
};
auto GetXciSizeFromRomSize(u8 rom_size) -> s64 {
@@ -90,6 +119,7 @@ auto GetDumpTypeStr(u8 type) -> const char* {
case DumpFileType_UID: return " (Card UID).bin";
case DumpFileType_Cert: return " (Certificate).bin";
case DumpFileType_Initial: return " (Initial Data).bin";
case DumpFileType_XCZ: return ".xcz";
}
return "";
@@ -128,22 +158,27 @@ auto BuildFullDumpPath(DumpFileType type, std::span<const ApplicationEntry> entr
const auto base_path = BuildXciBasePath(entries);
fs::FsPath out;
if (use_folder) {
if (App::GetApp()->m_dump_append_folder_with_xci.Get()) {
out = base_path + ".xci/" + base_path + GetDumpTypeStr(type);
} else {
out = base_path + "/" + base_path + GetDumpTypeStr(type);
}
} else {
if (type == DumpFileType_XCZ) {
out = base_path + GetDumpTypeStr(type);
}
return fs::AppendPath(DUMP_XCZ_BASE_PATH, out);
} else {
if (use_folder) {
if (App::GetApp()->m_dump_append_folder_with_xci.Get()) {
out = base_path + ".xci/" + base_path + GetDumpTypeStr(type);
} else {
out = base_path + "/" + base_path + GetDumpTypeStr(type);
}
} else {
out = base_path + GetDumpTypeStr(type);
}
return fs::AppendPath(DUMP_BASE_PATH, out);
return fs::AppendPath(DUMP_GAMECARD_BASE_PATH, out);
}
}
auto BuildFullDumpPath(DumpFileType type, std::span<const ApplicationEntry> entries) -> fs::FsPath {
// check if the base path is too long.
const auto max_len = fs::FsPathReal::FS_REAL_MAX_LENGTH - std::strlen(DUMP_BASE_PATH) - 30;
const auto max_len = fs::FsPathReal::FS_REAL_MAX_LENGTH - std::strlen(DUMP_GAMECARD_BASE_PATH) - 30;
auto use_folder = App::GetApp()->m_dump_app_folder.Get();
for (;;) {
@@ -174,7 +209,7 @@ auto BuildFullDumpPath(DumpFileType type, std::span<const ApplicationEntry> entr
// @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[] = {
static const char mount_partition[] = {
[FsGameCardPartition_Update] = 'U',
[FsGameCardPartition_Normal] = 'N',
[FsGameCardPartition_Secure] = 'S',
@@ -182,7 +217,7 @@ auto BuildGcPath(const char* name, const FsGameCardHandle* handle, FsGameCardPar
};
fs::FsPath path;
std::snprintf(path, sizeof(path), "@Gc%c%08x://%s", mount_parition[partiton], handle->value, name);
std::snprintf(path, sizeof(path), "@Gc%c%08x://%s", mount_partition[partiton], handle->value, name);
return path;
}
@@ -200,7 +235,7 @@ struct XciSource final : dump::BaseSource {
int icon{};
Result Read(const std::string& path, void* buf, s64 off, s64 size, u64* bytes_read) override {
if (path.ends_with(GetDumpTypeStr(DumpFileType_XCI))) {
if (path.ends_with(GetDumpTypeStr(DumpFileType_XCI)) || path.ends_with(GetDumpTypeStr(DumpFileType_XCZ))) {
size = ClipSize(off, size, xci_size);
*bytes_read = size;
return menu->GcStorageRead(buf, off, size);
@@ -231,7 +266,7 @@ struct XciSource final : dump::BaseSource {
}
auto GetSize(const std::string& path) const -> s64 override {
if (path.ends_with(GetDumpTypeStr(DumpFileType_XCI))) {
if (path.ends_with(GetDumpTypeStr(DumpFileType_XCI)) || path.ends_with(GetDumpTypeStr(DumpFileType_XCZ))) {
return xci_size;
} else if (path.ends_with(GetDumpTypeStr(DumpFileType_Set))) {
return id_set.size();
@@ -259,33 +294,175 @@ private:
}
};
struct HashStr {
char str[0x21];
struct Test final : yati::source::Base {
Test(Menu* menu) : m_menu{menu} {
}
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override {
R_TRY(m_menu->GcStorageRead(buf, off, size));
*bytes_read = size;
R_SUCCEED();
}
private:
Menu* m_menu;
};
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;
}
struct NcaReader final : yati::source::Base {
NcaReader(Test* source, s64 offset) : m_source{source}, m_offset{offset} {
// from Gamecard-Installer-NX
Result fsOpenGameCardStorage(FsStorage* out, const FsGameCardHandle* handle, FsGameCardPartitionRaw partition) {
const struct {
FsGameCardHandle handle;
u32 partition;
} in = { *handle, (u32)partition };
}
return serviceDispatchIn(fsGetServiceSession(), 30, in, .out_num_objects = 1, .out_objects = &out->s);
}
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override {
return m_source->Read(buf, m_offset + off, size, bytes_read);
}
Result fsOpenGameCardDetectionEventNotifier(FsEventNotifier* out) {
return serviceDispatch(fsGetServiceSession(), 501,
.out_num_objects = 1,
.out_objects = &out->s
);
private:
Test* m_source;
const s64 m_offset;
};
Result NszExport(ProgressBox* pbox, const keys::Keys& keys, dump::BaseSource* _source, dump::WriteSource* writer, const fs::FsPath& path) {
auto source = (XciSource*)_source;
const auto threaded_write = [&](const std::string& name, s64& read_offset, s64& write_offset, s64 size) -> Result {
if (size > 0) {
pbox->NewTransfer(name);
R_TRY(thread::Transfer(pbox, size,
[&](void* data, s64 off, s64 size, u64* bytes_read) -> Result {
return source->Read(path, data, read_offset + off, size, bytes_read);
},
[&](const void* data, s64 off, s64 size) -> Result {
return writer->Write(data, write_offset + off, size);
}
));
read_offset += size;
write_offset += size;
}
R_SUCCEED();
};
// writes padding between partitions and files.
const auto write_padding = [&](const std::string& name, s64& read_offset, s64& write_offset, s64 size) -> Result {
return threaded_write("Writing padding - " + name, read_offset, write_offset, size);
};
Test yati_source(source->menu);
yati::container::Xci xci{&yati_source};
yati::container::Xci::Root root;
R_TRY(xci.GetRoot(root));
//
s64 read_offset = 0;
s64 write_offset = 0;
for (u32 i = 0; i < std::size(root.partitions); i++) {
auto& partition = root.partitions[i];
auto& hfs0 = partition.hfs0;
auto& collections = partition.collections;
log_write("\tpartition name: %s offset: %zu size: %zu\n", partition.name.c_str(), partition.hfs0_offset, partition.hfs0_size);
// read pading before hfs0
R_TRY(write_padding("hfs0 before", read_offset, write_offset, partition.hfs0_offset - read_offset));
// offset to the hfs0.
const auto hfs0_offset = write_offset;
// offset to the data within the hfs0.
const auto hfs0_data_offset = hfs0_offset + hfs0.GetHfs0Size();
// offset to the hfs0 within the root hfs0.
const auto root_hfs0_data_offset = write_offset - root.hfs0.data_offset;
// calculate the expected size of the partition.
s64 expected_hfs0_data_size = 0;
for (auto& collection : partition.collections) {
expected_hfs0_data_size += collection.size;
}
if (!partition.collections.empty()) {
R_TRY(write_padding(partition.name, read_offset, write_offset, partition.collections[0].offset - read_offset));
} else {
// empty hfs0, write it as is.
log_write("empty hfs0 offset: %zu size: %zu get size: %zu\n", hfs0.data_offset, partition.hfs0_size, hfs0.GetHfs0Size());
R_UNLESS(partition.hfs0_size == hfs0.GetHfs0Size(), 21);
// R_UNLESS(hfs0.data_offset == 0, 14);
R_TRY(write_padding(partition.name, read_offset, write_offset, partition.hfs0_size));
}
const auto nca_creator = [&yati_source](const nca::Header& header, const keys::KeyEntry& title_key, const utils::nsz::Collection& collection) {
return std::make_unique<nca::NcaReader>(
header, &title_key, collection.size,
std::make_shared<NcaReader>(&yati_source, collection.offset)
);
};
// todo: update write offset.
R_TRY(utils::nsz::NszExport(pbox, nca_creator, read_offset, write_offset, collections, keys, source, writer, path));
// update offset / size in file table and calculate new total data size.
s64 new_hfs0_data_size = 0;
for (u32 i = 0; i < std::size(collections); i++) {
auto& collection = collections[i];
auto& file_table = hfs0.file_table[i];
// const auto offset = collection.offset - hfs0_data_offset;
// log_write("offset: %zu\n", offset);
// log_write("collection.offset: %zu\n", collection.offset);
// log_write("hfs0.data_offset: %zu\n", hfs0.data_offset);
// log_write("file_table.data_offset: %zu\n", file_table.data_offset);
// R_UNLESS(file_table.data_offset == offset, 8);
// R_UNLESS(file_table.data_size = collection.size, 9);
// update file and string table from collection.
file_table.data_offset = collection.offset - hfs0_data_offset;
file_table.data_size = collection.size;
hfs0.string_table[i] = collection.name;
new_hfs0_data_size += collection.size;
}
// update offset and size of hfs0 in root file table.
auto& root_file_table = root.hfs0.file_table[i];
const auto hfs0_data_size = root_file_table.data_size - (expected_hfs0_data_size - new_hfs0_data_size);
log_write("hfs0.data_offset: %zu\n", hfs0.data_offset);
log_write("old data offset: %zu\n", root_file_table.data_offset);
log_write("new data offset: %zu\n\n", root_hfs0_data_offset);
log_write("old data size: %zu\n", root_file_table.data_size);
log_write("new data size: %zu\n", hfs0_data_size);
// R_UNLESS(root_file_table.data_offset == root_hfs0_data_offset, 5);
// R_UNLESS(root_file_table.data_size == hfs0_data_size, 6);
root_file_table.data_offset = root_hfs0_data_offset;
root_file_table.data_size = hfs0_data_size;
// re-write updated hfs0 partition.
// R_UNLESS(partition.hfs0_offset == hfs0_offset, 7);
const auto hfs0_data = hfs0.GetHfs0Data();
R_TRY(writer->Write(hfs0_data.data(), hfs0_offset, hfs0_data.size()));
}
// add remaining padding, if needed.
R_TRY(write_padding("hfs0 partition", read_offset, write_offset, read_offset % 512));
// re-write updated root partition.
const auto root_data = root.hfs0.GetHfs0Data();
R_TRY(writer->Write(root_data.data(), root.hfs0_offset, root_data.size()));
log_write("read_offset: %zu\n", read_offset);
log_write("write_offset: %zu\n", write_offset);
// update with actual size.
R_TRY(writer->SetSize(write_offset));
R_SUCCEED();
}
struct GcSource final : yati::source::Base {
@@ -401,51 +578,49 @@ auto ApplicationEntry::GetSize() const -> s64 {
Menu::Menu(u32 flags) : MenuBase{"GameCard"_i18n, flags} {
this->SetActions(
std::make_pair(Button::A, Action{"OK"_i18n, [this](){
if (m_option_index == 3) {
SetPop();
} else {
if (!m_mounted) {
return;
if (!m_mounted) {
return;
}
if (m_option_index == 0) {
if (!App::GetInstallEnable()) {
App::ShowEnableInstallPrompt();
} else {
log_write("[GC] doing install A\n");
App::Push<ui::ProgressBox>(m_icon, "Installing "_i18n, m_entries[m_entry_index].lang_entry.name, [this](auto pbox) -> Result {
auto source = std::make_unique<GcSource>(m_entries[m_entry_index], m_fs.get());
return yati::InstallFromCollections(pbox, source.get(), source->m_collections, source->m_config);
}, [this](Result rc){
App::PushErrorBox(rc, "Gc install failed!"_i18n);
if (R_SUCCEEDED(rc)) {
App::Notify("Gc install success!"_i18n);
}
});
}
} else if (m_option_index == 1) {
auto options = std::make_unique<Sidebar>("Select content to dump"_i18n, Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(std::move(options)));
if (m_option_index == 0) {
if (!App::GetInstallEnable()) {
App::ShowEnableInstallPrompt();
} else {
log_write("[GC] doing install A\n");
App::Push<ui::ProgressBox>(m_icon, "Installing "_i18n, m_entries[m_entry_index].lang_entry.name, [this](auto pbox) -> Result {
auto source = std::make_unique<GcSource>(m_entries[m_entry_index], m_fs.get());
return yati::InstallFromCollections(pbox, source.get(), source->m_collections, source->m_config);
}, [this](Result rc){
App::PushErrorBox(rc, "Gc install failed!"_i18n);
const auto add = [&](const std::string& name, u32 flags){
options->Add<SidebarEntryCallback>(name, [this, flags](){
DumpGames(flags);
m_dirty = true;
}, true);
};
if (R_SUCCEEDED(rc)) {
App::Notify("Gc install success!"_i18n);
}
});
}
} else if (m_option_index == 1) {
auto options = std::make_unique<Sidebar>("Select content to dump"_i18n, Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(std::move(options)));
const auto add = [&](const std::string& name, u32 flags){
options->Add<SidebarEntryCallback>(name, [this, flags](){
DumpGames(flags);
m_dirty = true;
}, true);
};
add("Export All"_i18n, DumpFileFlag_All);
add("Export All Bins"_i18n, DumpFileFlag_AllBin);
add("Export XCI"_i18n, DumpFileFlag_XCI);
add("Export Card ID Set"_i18n, DumpFileFlag_Set);
add("Export Card UID"_i18n, DumpFileFlag_UID);
add("Export Certificate"_i18n, DumpFileFlag_Cert);
add("Export Initial Data"_i18n, DumpFileFlag_Initial);
} else if (m_option_index == 2) {
const auto rc = MountGcFs();
App::PushErrorBox(rc, "Failed to mount GameCard filesystem"_i18n);
}
add("Export All"_i18n, DumpFileFlag_All);
add("Export All Bins"_i18n, DumpFileFlag_AllBin);
add("Export XCI"_i18n, DumpFileFlag_XCI);
add("Export Card ID Set"_i18n, DumpFileFlag_Set);
add("Export Card UID"_i18n, DumpFileFlag_UID);
add("Export Certificate"_i18n, DumpFileFlag_Cert);
add("Export Initial Data"_i18n, DumpFileFlag_Initial);
} else if (m_option_index == 2) {
DumpXcz(0);
} else if (m_option_index == 3) {
const auto rc = MountGcFs();
App::PushErrorBox(rc, "Failed to mount GameCard filesystem"_i18n);
}
}}),
std::make_pair(Button::B, Action{"Back"_i18n, [this](){
@@ -497,7 +672,7 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
if (touch && m_option_index == i) {
FireAction(Button::A);
} else {
App::PlaySoundEffect(SoundEffect_Focus);
App::PlaySoundEffect(SoundEffect::Focus);
m_option_index = i;
}
});
@@ -551,7 +726,7 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
gfx::drawRect(vg, 490, text_y - 45.f / 2.f, 2, 45, theme->GetColour(ThemeEntryID_TEXT_SELECTED));
colour = ThemeEntryID_TEXT_SELECTED;
}
if (i != 3 && !m_mounted) {
if (!m_mounted) {
colour = ThemeEntryID_TEXT_INFO;
}
@@ -632,7 +807,7 @@ Result Menu::GcMount() {
}
// find the nca file, this will never fail for gamecards, see above comment.
const auto str = hexIdToStr(info.content_id);
const auto str = utils::hexIdToStr(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));
});
@@ -735,12 +910,12 @@ Result Menu::GcMountStorage() {
log_write("[GC] m_storage_full_size: %zd rom_size: 0x%X\n", m_storage_full_size, rom_size);
R_UNLESS(m_storage_full_size > 0, Result_GcBadXciRomSize);
R_TRY(fsStorageGetSize(&m_storage, &m_parition_normal_size));
R_TRY(fsStorageGetSize(&m_storage, &m_partition_normal_size));
R_TRY(GcMountPartition(FsGameCardPartitionRaw_Secure));
R_TRY(fsStorageGetSize(&m_storage, &m_parition_secure_size));
R_TRY(fsStorageGetSize(&m_storage, &m_partition_secure_size));
m_storage_trimmed_size = sizeof(header) + trim_size * 512ULL;
m_storage_total_size = m_parition_normal_size + m_parition_secure_size;
m_storage_total_size = m_partition_normal_size + m_partition_secure_size;
m_storage_mounted = true;
log_write("[GC] m_storage_trimmed_size: %zd\n", m_storage_trimmed_size);
@@ -786,11 +961,11 @@ void Menu::GcUnmountPartition() {
}
Result Menu::GcStorageReadInternal(void* buf, s64 off, s64 size, u64* bytes_read) {
if (off < m_parition_normal_size) {
size = std::min<s64>(size, m_parition_normal_size - off);
if (off < m_partition_normal_size) {
size = std::min<s64>(size, m_partition_normal_size - off);
R_TRY(GcMountPartition(FsGameCardPartitionRaw_Normal));
} else {
off = off - m_parition_normal_size;
off = off - m_partition_normal_size;
R_TRY(GcMountPartition(FsGameCardPartitionRaw_Secure));
}
@@ -833,9 +1008,7 @@ Result Menu::GcStorageRead(void* _buf, s64 off, s64 size) {
if (unaligned_size) {
R_TRY(GcStorageReadInternal(data, off, sizeof(data), &bytes_read));
const auto csize = std::min<s64>(size, 0x200 - unaligned_size);
std::memcpy(buf, data + unaligned_size, csize);
std::memcpy(buf, data, unaligned_size);
}
R_SUCCEED();
@@ -867,7 +1040,7 @@ Result Menu::GcOnEvent(bool force) {
log_write("trying to mount\n");
m_mounted = R_SUCCEEDED(GcMount());
if (m_mounted) {
App::PlaySoundEffect(SoundEffect::SoundEffect_Startup);
App::PlaySoundEffect(SoundEffect::Startup);
}
} else {
log_write("trying to unmount\n");
@@ -926,6 +1099,30 @@ void Menu::OnChangeIndex(s64 new_index) {
}
}
Result Menu::DumpXcz(u32 flags) {
R_TRY(GcMountStorage());
auto source = std::make_shared<XciSource>();
source->menu = this;
source->application_name = m_entries[m_entry_index].lang_entry.name;
source->icon = m_icon;
// todo: support for prepending cert area.
std::vector<fs::FsPath> paths;
source->xci_size = m_storage_trimmed_size;
paths.emplace_back(BuildFullDumpPath(DumpFileType_XCZ, m_entries));
// todo: log keys error.
keys::Keys keys;
R_TRY(keys::parse_keys(keys, true));
dump::Dump(source, paths, [keys](ProgressBox* pbox, dump::BaseSource* source, dump::WriteSource* writer, const fs::FsPath& path) {
return NszExport(pbox, keys, source, writer, path);
});
R_SUCCEED();
}
Result Menu::DumpGames(u32 flags) {
// first, try and mount the storage.
// this will fill out the xci header, verify and get sizes.
@@ -986,7 +1183,17 @@ Result Menu::DumpGames(u32 flags) {
paths.emplace_back(BuildFullDumpPath(DumpFileType_Initial, m_entries));
}
dump::Dump(source, paths, [](Result){}, location_flags);
if (0) {
// todo: log keys error.
keys::Keys keys;
keys::parse_keys(keys, true);
dump::Dump(source, paths, [keys](ProgressBox* pbox, dump::BaseSource* source, dump::WriteSource* writer, const fs::FsPath& path) {
return NszExport(pbox, keys, source, writer, path);
});
} else {
dump::Dump(source, paths, nullptr, location_flags);
}
R_SUCCEED();
};
@@ -1105,6 +1312,23 @@ Result Menu::GcGetSecurityInfo(GameCardSecurityInformation& out) {
}
Result Menu::MountGcFs() {
#if 1
R_TRY(GcMountStorage());
const auto& e = m_entries[m_entry_index];
auto source = std::make_shared<Test>(this);
fs::FsPath root;
R_TRY(devoptab::MountXciSource(source, m_storage_trimmed_size, e.lang_entry.name, root));
auto fs = std::make_shared<filebrowser::FsStdioWrapper>(root, [root](){
devoptab::UmountXci(root);
});
filebrowser::MountFsHelper(fs, e.lang_entry.name);
#else
// old code that only mounts secure partition.
const auto& e = m_entries[m_entry_index];
auto fs = std::make_shared<fs::FsNative>(&m_fs->m_fs, false);
@@ -1118,6 +1342,7 @@ Result Menu::MountGcFs() {
};
App::Push<filebrowser::Menu>(fs, fs_entry, "/");
#endif
R_SUCCEED();
}

View File

@@ -197,7 +197,7 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
if (touch && m_index == i) {
FireAction(Button::A);
} else {
App::PlaySoundEffect(SoundEffect_Focus);
App::PlaySoundEffect(SoundEffect::Focus);
SetIndex(i);
}
});
@@ -354,16 +354,16 @@ void DownloadEntries(const Entry& entry) {
PopupList::Items entry_items;
for (const auto& e : gh_entries) {
std::string str;
std::string str = " [" + e.published_at.substr(0, 10) + "]";
if (!e.name.empty()) {
str += e.name + " | ";
str += " " + e.name;
} else {
str += e.tag_name + " | ";
str += " " + e.tag_name;
}
if (e.prerelease) {
str += " (Pre-Release)";
}
str += " [" + e.published_at.substr(0, 10) + "]";
entry_items.emplace_back(str);
}
@@ -396,8 +396,7 @@ void DownloadEntries(const Entry& entry) {
}
if (!using_name || found) {
std::string str = p.name + " | ";
str += " [" + p.updated_at.substr(0, 10) + "]";
std::string str = " [" + p.updated_at.substr(0, 10) + "]" + " " + p.name;
asset_items.emplace_back(str);
api_assets.emplace_back(p);

View File

@@ -1,13 +1,19 @@
#include "app.hpp"
#include "log.hpp"
#include "fs.hpp"
#include "ui/menus/homebrew.hpp"
#include "ui/menus/filebrowser.hpp"
#include "ui/sidebar.hpp"
#include "ui/error_box.hpp"
#include "ui/option_box.hpp"
#include "ui/progress_box.hpp"
#include "ui/nvg_util.hpp"
#include "utils/devoptab.hpp"
#include "utils/profile.hpp"
#include "owo.hpp"
#include "defines.hpp"
#include "i18n.hpp"
@@ -84,7 +90,7 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
if (touch && m_index == i) {
FireAction(Button::A);
} else {
App::PlaySoundEffect(SoundEffect_Focus);
App::PlaySoundEffect(SoundEffect::Focus);
SetIndex(i);
}
});
@@ -192,10 +198,11 @@ void Menu::InstallHomebrew() {
}
void Menu::ScanHomebrew() {
TimeStamp ts;
FreeEntries();
nro_scan("/switch", m_entries);
log_write("nros found: %zu time_taken: %.2f\n", m_entries.size(), ts.GetSecondsD());
{
SCOPED_TIMESTAMP("nro scan");
nro_scan("/switch", m_entries);
}
struct IniUser {
std::vector<NroEntry>& entires;
@@ -458,6 +465,13 @@ void Menu::DisplayOptions() {
}, "Shows all hidden homebrew."_i18n);
});
// for testing stuff.
#if 0
options->Add<SidebarEntrySlider>("Test", 1, 0, 2, 10, [](auto& v_out){
});
#endif
if (!m_entries_current.empty()) {
#if 0
options->Add<SidebarEntryCallback>("Info"_i18n, [this](){
@@ -472,14 +486,10 @@ void Menu::DisplayOptions() {
}, "Hides the selected homebrew.\n\n"
"To unhide homebrew, enable \"Show hidden\" in the sort options."_i18n);
auto mount_option = options->Add<SidebarEntryCallback>("Mount RomFS"_i18n, [this](){
const auto rc = MountRomfsFs();
App::PushErrorBox(rc, "Failed to mount NRO RomFS"_i18n);
}, "Mounts the homebrew RomFS"_i18n);
mount_option->Depends([this](){
return GetEntry().romfs_offset && GetEntry().romfs_size;
}, "This homebrew does not have a RomFS"_i18n);
options->Add<SidebarEntryCallback>("Mount NRO Fs"_i18n, [this](){
const auto rc = MountNroFs();
App::PushErrorBox(rc, "Failed to mount NRO FileSystem"_i18n);
}, "Mounts the NRO FileSystem (icon, nacp and RomFS)."_i18n);
options->Add<SidebarEntryCallback>("Delete"_i18n, [this](){
const auto buf = "Are you sure you want to delete "_i18n + GetEntry().path.toString() + "?";
@@ -510,41 +520,17 @@ void Menu::DisplayOptions() {
}
}
struct NroRomFS final : fs::FsStdio {
NroRomFS(const fs::FsPath& name, const fs::FsPath& root) : FsStdio{true, root}, m_name{name} {
}
~NroRomFS() {
romfsUnmount(m_name);
}
const fs::FsPath m_name;
};
Result Menu::MountRomfsFs() {
static const char* name = "nro_romfs";
static const char* root = "nro_romfs:/";
Result Menu::MountNroFs() {
const auto& e = GetEntry();
// todo: add errors for when nro doesn't have romfs.
R_UNLESS(e.romfs_offset, 0x1);
R_UNLESS(e.romfs_size, 0x1);
fs::FsPath root;
R_TRY(devoptab::MountNro(App::GetApp()->m_fs.get(), e.path, root));
FsFile file;
R_TRY(fsFsOpenFile(fsdevGetDeviceFileSystem("sdmc"), e.path, FsOpenMode_Read, &file));
const auto rc = romfsMountFromFile(file, e.romfs_offset, name);
if (R_FAILED(rc)) {
fsFileClose(&file);
R_THROW(rc);
}
auto fs = std::make_shared<filebrowser::FsStdioWrapper>(root, [](){
romfsUnmount(name);
auto fs = std::make_shared<filebrowser::FsStdioWrapper>(root, [root](){
devoptab::UmountNro(root);
});
filebrowser::MountFsHelper(fs, e.GetName());
filebrowser::MountFsHelper(fs, root);
R_SUCCEED();
}

View File

@@ -0,0 +1,134 @@
#include "ui/menus/image_viewer.hpp"
#include "ui/nvg_util.hpp"
#include "app.hpp"
#include "i18n.hpp"
#include "image.hpp"
namespace sphaira::ui::menu::imageview {
namespace {
} // namespace
Menu::Menu(fs::Fs* fs, const fs::FsPath& path) : m_path{path} {
SetAction(Button::B, Action{[this](){
SetPop();
}});
std::vector<u8> m_image_buf;
const auto rc = fs->read_entire_file(path, m_image_buf);
if (R_FAILED(rc)) {
App::PushErrorBox(rc, "Failed to load image"_i18n);
SetPop();
return;
}
// try and load using nvjpg if possible.
u32 flags = ImageFlag_None;
if (path.ends_with(".jpg") || path.ends_with(".jpeg")) {
flags = ImageFlag_JPEG;
}
const auto result = ImageLoadFromMemory(m_image_buf, flags);
if (result.data.empty()) {
SetPop();
return;
}
m_image = nvgCreateImageRGBA(App::GetVg(), result.w, result.h, 0, result.data.data());
if (m_image <= 0) {
SetPop();
return;
}
m_image_width = result.w;
m_image_height = result.h;
// scale to fit.
const auto ws = SCREEN_WIDTH / m_image_width;
const auto hs = SCREEN_HEIGHT / m_image_height;
m_zoom = std::min(ws, hs);
UpdateSize();
}
Menu::~Menu() {
nvgDeleteImage(App::GetVg(), m_image);
}
void Menu::Update(Controller* controller, TouchInfo* touch) {
Widget::Update(controller, touch);
const auto kdown = controller->m_kdown | controller->m_kheld;
// pan support.
constexpr auto max_pan = 10.f;
constexpr auto max_panx = max_pan;// * (SCREEN_WIDTH / SCREEN_HEIGHT);
constexpr auto max_pany = max_pan;
if (controller->Got(kdown, Button::LS_LEFT)) {
m_xoff += max_panx;
}
if (controller->Got(kdown, Button::LS_RIGHT)) {
m_xoff -= max_panx;
}
if (controller->Got(kdown, Button::LS_UP)) {
m_yoff += max_pany;
}
if (controller->Got(kdown, Button::LS_DOWN)) {
m_yoff -= max_pany;
}
// zoom support, by 1% increments.
constexpr auto max_zoom = 0.01f;
if (controller->Got(kdown, Button::RS_UP)) {
m_zoom += max_zoom;
}
if (controller->Got(kdown, Button::RS_DOWN)) {
m_zoom -= max_zoom;
}
if (controller->Got(kdown, Button::LS_ANY) || controller->Got(kdown, Button::RS_ANY)) {
UpdateSize();
}
}
void Menu::Draw(NVGcontext* vg, Theme* theme) {
gfx::drawRect(vg, 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, nvgRGB(0, 0, 0));
gfx::drawImage(vg, m_xoff + GetX(), m_yoff + GetY(), GetW(), GetH(), m_image);
// todo: when pan/zoom, show image info to the screen.
// todo: maybe show image info by default and option to hide it.
}
void Menu::UpdateSize() {
m_zoom = std::clamp(m_zoom, 0.1f, 4.0f);
// center pos.
const auto cx = SCREEN_WIDTH / 2;
const auto cy = SCREEN_HEIGHT / 2;
// calc position and size.
const auto w = m_image_width * m_zoom;
const auto h = m_image_height * m_zoom;
const auto x = cx - (w / 2);
const auto y = cy - (h / 2);
SetPos(x, y, w, h);
// clip edges.
if (SCREEN_HEIGHT >= h) {
m_yoff = 0;
// m_yoff = std::clamp(m_yoff, -y, +y);
} else {
m_yoff = std::clamp(m_yoff, (SCREEN_HEIGHT - h) - y, (h - SCREEN_HEIGHT) + y);
}
if (SCREEN_WIDTH >= w) {
m_xoff = 0;
// m_xoff = std::clamp(m_xoff, -x, +x);
} else {
m_xoff = std::clamp(m_xoff, (SCREEN_WIDTH - w) - x, (w - SCREEN_WIDTH) + x);
}
}
} // namespace sphaira::ui::menu::imageview

View File

@@ -1,5 +1,3 @@
#if ENABLE_NETWORK_INSTALL
#include "ui/menus/install_stream_menu_base.hpp"
#include "yati/yati.hpp"
#include "app.hpp"
@@ -275,5 +273,3 @@ void Menu::OnInstallClose() {
}
} // namespace sphaira::ui::menu::stream
#endif

View File

@@ -67,16 +67,17 @@ const MiscMenuEntry MISC_MENU_ENTRIES[] = {
"You can backup and restore saves.\n\n"\
"Experimental support for backing up system saves is possible." },
#if 0
{ .name = "Themezer", .title = "Themezer", .func = MiscMenuFuncGenerator<ui::menu::themezer::Menu>, .flag = MiscMenuFlag_Shortcut, .info =
"Download themes from themezer.net. "\
"Themes are downloaded to /themes/sphaira\n"\
"To install the themes, NXThemesInstaller needs to be installed (can be downloaded via the AppStore)." },
#endif
{ .name = "GitHub", .title = "GitHub", .func = MiscMenuFuncGenerator<ui::menu::gh::Menu>, .flag = MiscMenuFlag_Shortcut, .info =
"Download releases directly from GitHub. "\
"Custom entries can be added to /config/sphaira/github" },
#if ENABLE_NETWORK_INSTALL
{ .name = "FTP", .title = "FTP Install", .func = MiscMenuFuncGenerator<ui::menu::ftp::Menu>, .flag = MiscMenuFlag_Install, .info =
"Install apps via FTP.\n\n"\
"NOTE: This feature does not always work, use at your own risk. "\
@@ -93,7 +94,6 @@ const MiscMenuEntry MISC_MENU_ENTRIES[] = {
"NOTE: This feature does not always work, use at your own risk. "\
"If you encounter an issue, do not open an issue, it will not be fixed." },
#endif
{ .name = "GameCard", .title = "GameCard", .func = MiscMenuFuncGenerator<ui::menu::gc::Menu>, .flag = MiscMenuFlag_Shortcut, .info =
"View info on the inserted Game Card (GC). "\
"You can backup and install the inserted GC. "\

View File

@@ -1,5 +1,3 @@
#if ENABLE_NETWORK_INSTALL
#include "ui/menus/mtp_menu.hpp"
#include "usb/usbds.hpp"
#include "app.hpp"
@@ -12,10 +10,10 @@
namespace sphaira::ui::menu::mtp {
Menu::Menu(u32 flags) : stream::Menu{"MTP Install"_i18n, flags} {
m_was_mtp_enabled = App::GetMtpEnable();
m_was_mtp_enabled = haze::IsInit();
if (!m_was_mtp_enabled) {
log_write("[MTP] wasn't enabled, forcefully enabling\n");
App::SetMtpEnable(true);
haze::Init();
}
haze::InitInstallMode(
@@ -31,7 +29,7 @@ Menu::~Menu() {
if (!m_was_mtp_enabled) {
log_write("[MTP] disabling on exit\n");
App::SetMtpEnable(false);
haze::Exit();
}
}
@@ -59,5 +57,3 @@ void Menu::OnDisableInstallMode() {
}
} // namespace sphaira::ui::menu::mtp
#endif

View File

@@ -42,6 +42,42 @@ constexpr const char* NX_SAVE_META_NAME = ".nx_save_meta.bin";
constinit UEvent g_change_uevent;
struct DumpSource final : dump::BaseSource {
DumpSource(std::span<const std::reference_wrapper<Entry>> entries, std::span<const fs::FsPath> paths)
: m_entries{entries}
, m_paths{paths} {
}
Result Read(const std::string& path, void* buf, s64 off, s64 size, u64* bytes_read) override {
R_SUCCEED();
}
auto GetName(const std::string& path) const -> std::string override {
return GetEntry(path).GetName();
}
auto GetSize(const std::string& path) const -> s64 override {
return 0;
}
auto GetIcon(const std::string& path) const -> int override {
return GetEntry(path).image;
}
auto GetEntry(const std::string& path) const -> Entry& {
const auto itr = std::ranges::find_if(m_paths, [&path](auto& e){
return path == e;
});
const auto index = std::distance(m_paths.begin(), itr);
return m_entries[index];
}
private:
std::span<const std::reference_wrapper<Entry>> m_entries;
std::span<const fs::FsPath> m_paths;
};
// https://github.com/J-D-K/JKSV/issues/264#issuecomment-2618962807
struct NXSaveMeta {
u32 magic{}; // NX_SAVE_META_MAGIC
@@ -256,18 +292,6 @@ void LoadControlEntry(Entry& e, bool force_image_load = false) {
}
}
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;
}
auto BuildSaveName(const Entry& e) -> fs::FsPath {
fs::FsPath name_buf = e.GetName();
title::utilsReplaceIllegalCharacters(name_buf, true);
@@ -389,7 +413,7 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
if (touch && m_index == i) {
FireAction(Button::A);
} else {
App::PlaySoundEffect(SoundEffect_Focus);
App::PlaySoundEffect(SoundEffect::Focus);
SetIndex(i);
}
});
@@ -691,14 +715,9 @@ void Menu::DisplayOptions() {
}
void Menu::BackupSaves(std::vector<std::reference_wrapper<Entry>>& entries) {
dump::DumpGetLocation("Select backup location"_i18n, dump::DumpLocationFlag_SdCard|dump::DumpLocationFlag_Stdio, [this, entries](const dump::DumpLocation& location){
dump::DumpGetLocation("Select backup location"_i18n, dump::DumpLocationFlag_SdCard|dump::DumpLocationFlag_Stdio|dump::DumpLocationFlag_Usb, [this, entries](const dump::DumpLocation& location){
App::Push<ProgressBox>(0, "Backup"_i18n, "", [this, entries, location](auto pbox) -> Result {
for (auto& e : entries) {
// the entry may not have loaded yet.
LoadControlEntry(e);
R_TRY(BackupSaveInternal(pbox, location, e, m_compress_save_backup.Get()));
}
R_SUCCEED();
return BackupSaveInternal(pbox, location, entries, m_compress_save_backup.Get());
}, [](Result rc){
App::PushErrorBox(rc, "Backup failed!"_i18n);
@@ -936,172 +955,167 @@ Result Menu::RestoreSaveInternal(ProgressBox* pbox, const Entry& e, const fs::Fs
R_SUCCEED();
}
Result Menu::BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& location, const Entry& e, bool compressed, bool is_auto) const {
std::unique_ptr<fs::Fs> fs;
if (location.entry.type == dump::DumpLocationType_Stdio) {
fs = std::make_unique<fs::FsStdio>(true, location.stdio[location.entry.index].mount);
} else if (location.entry.type == dump::DumpLocationType_SdCard) {
fs = std::make_unique<fs::FsNativeSd>();
} else {
std::unreachable();
Result Menu::BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& location, std::span<const std::reference_wrapper<Entry>> entries, bool compressed, bool is_auto) const {
std::vector<fs::FsPath> paths;
for (auto& e : entries) {
// ensure that we have title name and icon loaded.
LoadControlEntry(e);
paths.emplace_back(BuildSavePath(e, is_auto));
}
pbox->SetTitle(e.GetName());
if (e.image) {
pbox->SetImage(e.image);
} else if (auto data = title::Get(e.application_id); data && !data->icon.empty()) {
pbox->SetImageDataConst(data->icon);
} else {
pbox->SetImage(0);
}
auto source = std::make_shared<DumpSource>(entries, paths);
const auto save_data_space_id = (FsSaveDataSpaceId)e.save_data_space_id;
return dump::Dump(pbox, source, location, paths, [&](ui::ProgressBox* pbox, dump::BaseSource* _source, dump::WriteSource* writer, const fs::FsPath& path) -> Result {
const auto source = (DumpSource*)_source;
const auto& e = source->GetEntry(path);
// try and get the journal and data size.
FsSaveDataExtraData extra{};
R_TRY(fsReadSaveDataFileSystemExtraDataBySaveDataSpaceId(&extra, sizeof(extra), save_data_space_id, e.save_data_id));
pbox->SetTitle(e.GetName());
if (e.image) {
pbox->SetImage(e.image);
} else if (auto data = title::Get(e.application_id); data && !data->icon.empty()) {
pbox->SetImageDataConst(data->icon);
} else {
pbox->SetImage(0);
}
FsSaveDataAttribute attr{};
attr.application_id = e.application_id;
attr.uid = e.uid;
attr.system_save_data_id = e.system_save_data_id;
attr.save_data_type = e.save_data_type;
attr.save_data_rank = e.save_data_rank;
attr.save_data_index = e.save_data_index;
const auto save_data_space_id = (FsSaveDataSpaceId)e.save_data_space_id;
// try and open the save file system
fs::FsNativeSave save_fs{(FsSaveDataType)e.save_data_type, save_data_space_id, &attr, true};
R_TRY(save_fs.GetFsOpenResult());
// try and get the journal and data size.
FsSaveDataExtraData extra{};
R_TRY(fsReadSaveDataFileSystemExtraDataBySaveDataSpaceId(&extra, sizeof(extra), save_data_space_id, e.save_data_id));
// get a list of collections.
filebrowser::FsDirCollections collections;
R_TRY(filebrowser::FsView::get_collections(&save_fs, "/", "", collections));
FsSaveDataAttribute attr{};
attr.application_id = e.application_id;
attr.uid = e.uid;
attr.system_save_data_id = e.system_save_data_id;
attr.save_data_type = e.save_data_type;
attr.save_data_rank = e.save_data_rank;
attr.save_data_index = e.save_data_index;
// the save file may be empty, this isn't an error, but we exit early.
R_UNLESS(!collections.empty(), 0x0);
// try and open the save file system
fs::FsNativeSave save_fs{(FsSaveDataType)e.save_data_type, save_data_space_id, &attr, true};
R_TRY(save_fs.GetFsOpenResult());
const auto t = (time_t)extra.timestamp;
const auto tm = std::localtime(&t);
// get a list of collections.
filebrowser::FsDirCollections collections;
R_TRY(filebrowser::FsView::get_collections(&save_fs, "/", "", collections));
// pre-calculate the time rather than calculate it in the loop.
zip_fileinfo zip_info_default{};
zip_info_default.tmz_date.tm_sec = tm->tm_sec;
zip_info_default.tmz_date.tm_min = tm->tm_min;
zip_info_default.tmz_date.tm_hour = tm->tm_hour;
zip_info_default.tmz_date.tm_mday = tm->tm_mday;
zip_info_default.tmz_date.tm_mon = tm->tm_mon;
zip_info_default.tmz_date.tm_year = tm->tm_year;
// the save file may be empty, this isn't an error, but we exit early.
R_UNLESS(!collections.empty(), 0x0);
const auto path = fs::AppendPath(fs->Root(), BuildSavePath(e, is_auto));
const auto temp_path = path + ".temp";
const auto t = (time_t)extra.timestamp;
const auto tm = std::localtime(&t);
fs->CreateDirectoryRecursivelyWithPath(temp_path);
ON_SCOPE_EXIT(fs->DeleteFile(temp_path));
// pre-calculate the time rather than calculate it in the loop.
zip_fileinfo zip_info_default{};
zip_info_default.tmz_date.tm_sec = tm->tm_sec;
zip_info_default.tmz_date.tm_min = tm->tm_min;
zip_info_default.tmz_date.tm_hour = tm->tm_hour;
zip_info_default.tmz_date.tm_mday = tm->tm_mday;
zip_info_default.tmz_date.tm_mon = tm->tm_mon;
zip_info_default.tmz_date.tm_year = tm->tm_year;
// zip to memory if less than 1GB and not applet mode.
// TODO: use my mmz code from ftpsrv to stream zip creation.
// this will allow for zipping to memory and flushing every X bytes
// such as flushing every 8MB.
const auto file_download = App::IsApplet() || e.size >= 1024ULL * 1024ULL * 1024ULL;
// zip to memory if less than 1GB and not applet mode.
// TODO: use my mmz code from ftpsrv to stream zip creation.
// this will allow for zipping to memory and flushing every X bytes
// such as flushing every 8MB.
const auto file_download = App::IsApplet() || e.size >= 1024ULL * 1024ULL * 1024ULL;
mz::MzMem mz_mem{};
zlib_filefunc64_def file_func;
if (!file_download) {
mz::FileFuncMem(&mz_mem, &file_func);
} else {
mz::FileFuncStdio(&file_func);
}
mz::MzMem mz_mem{};
zlib_filefunc64_def file_func;
if (!file_download) {
mz::FileFuncMem(&mz_mem, &file_func);
} else {
dump::FileFuncWriter(writer, &file_func);
}
{
auto zfile = zipOpen2_64(temp_path, APPEND_STATUS_CREATE, nullptr, &file_func);
R_UNLESS(zfile, Result_ZipOpen2_64);
ON_SCOPE_EXIT(zipClose(zfile, "sphaira v" APP_VERSION_HASH));
// add save meta.
{
const NXSaveMeta meta{
.magic = NX_SAVE_META_MAGIC,
.version = NX_SAVE_META_VERSION,
.attr = extra.attr,
.owner_id = extra.owner_id,
.timestamp = extra.timestamp,
.flags = extra.flags,
.unk_x54 = extra.unk_x54,
.data_size = extra.data_size,
.journal_size = extra.journal_size,
.commit_id = extra.commit_id,
.raw_size = e.size,
auto zfile = zipOpen2_64(path, APPEND_STATUS_CREATE, nullptr, &file_func);
R_UNLESS(zfile, Result_ZipOpen2_64);
ON_SCOPE_EXIT(zipClose(zfile, "sphaira v" APP_VERSION_HASH));
// add save meta.
{
const NXSaveMeta meta{
.magic = NX_SAVE_META_MAGIC,
.version = NX_SAVE_META_VERSION,
.attr = extra.attr,
.owner_id = extra.owner_id,
.timestamp = extra.timestamp,
.flags = extra.flags,
.unk_x54 = extra.unk_x54,
.data_size = extra.data_size,
.journal_size = extra.journal_size,
.commit_id = extra.commit_id,
.raw_size = e.size,
};
R_UNLESS(ZIP_OK == zipOpenNewFileInZip(zfile, NX_SAVE_META_NAME, &zip_info_default, NULL, 0, NULL, 0, NULL, Z_DEFLATED, Z_NO_COMPRESSION), Result_ZipOpenNewFileInZip);
ON_SCOPE_EXIT(zipCloseFileInZip(zfile));
R_UNLESS(ZIP_OK == zipWriteInFileInZip(zfile, &meta, sizeof(meta)), Result_ZipWriteInFileInZip);
}
const auto zip_add = [&](const fs::FsPath& file_path) -> Result {
const char* file_name_in_zip = file_path.s;
// strip root path (/ or ums0:)
if (!std::strncmp(file_name_in_zip, save_fs.Root(), std::strlen(save_fs.Root()))) {
file_name_in_zip += std::strlen(save_fs.Root());
}
// root paths are banned in zips, they will warn when extracting otherwise.
while (file_name_in_zip[0] == '/') {
file_name_in_zip++;
}
pbox->NewTransfer(file_name_in_zip);
const auto level = compressed ? Z_DEFAULT_COMPRESSION : Z_NO_COMPRESSION;
if (ZIP_OK != zipOpenNewFileInZip(zfile, file_name_in_zip, &zip_info_default, NULL, 0, NULL, 0, NULL, Z_DEFLATED, level)) {
log_write("failed to add zip for %s\n", file_path.s);
R_THROW(Result_ZipOpenNewFileInZip);
}
ON_SCOPE_EXIT(zipCloseFileInZip(zfile));
return thread::TransferZip(pbox, zfile, &save_fs, file_path);
};
R_UNLESS(ZIP_OK == zipOpenNewFileInZip(zfile, NX_SAVE_META_NAME, &zip_info_default, NULL, 0, NULL, 0, NULL, Z_DEFLATED, Z_NO_COMPRESSION), Result_ZipOpenNewFileInZip);
ON_SCOPE_EXIT(zipCloseFileInZip(zfile));
R_UNLESS(ZIP_OK == zipWriteInFileInZip(zfile, &meta, sizeof(meta)), Result_ZipWriteInFileInZip);
}
const auto zip_add = [&](const fs::FsPath& file_path) -> Result {
const char* file_name_in_zip = file_path.s;
// strip root path (/ or ums0:)
if (!std::strncmp(file_name_in_zip, save_fs.Root(), std::strlen(save_fs.Root()))) {
file_name_in_zip += std::strlen(save_fs.Root());
}
// root paths are banned in zips, they will warn when extracting otherwise.
while (file_name_in_zip[0] == '/') {
file_name_in_zip++;
}
pbox->NewTransfer(file_name_in_zip);
const auto level = compressed ? Z_DEFAULT_COMPRESSION : Z_NO_COMPRESSION;
if (ZIP_OK != zipOpenNewFileInZip(zfile, file_name_in_zip, &zip_info_default, NULL, 0, NULL, 0, NULL, Z_DEFLATED, level)) {
log_write("failed to add zip for %s\n", file_path.s);
R_THROW(Result_ZipOpenNewFileInZip);
}
ON_SCOPE_EXIT(zipCloseFileInZip(zfile));
return thread::TransferZip(pbox, zfile, &save_fs, file_path);
};
// loop through every save file and store to zip.
for (const auto& collection : collections) {
for (const auto& file : collection.files) {
const auto file_path = fs::AppendPath(collection.path, file.name);
R_TRY(zip_add(file_path));
}
}
}
// if we dumped the save to ram, flush the data to file.
const auto is_file_based_emummc = App::IsFileBaseEmummc();
if (!file_download) {
pbox->NewTransfer("Flushing zip to file");
R_TRY(fs->CreateFile(temp_path, mz_mem.buf.size(), 0));
fs::File file;
R_TRY(fs->OpenFile(temp_path, FsOpenMode_Write, &file));
R_TRY(thread::Transfer(pbox, mz_mem.buf.size(),
[&](void* data, s64 off, s64 size, u64* bytes_read) -> Result {
size = std::min<s64>(size, mz_mem.buf.size() - off);
std::memcpy(data, mz_mem.buf.data() + off, size);
*bytes_read = size;
R_SUCCEED();
},
[&](const void* data, s64 off, s64 size) -> Result {
const auto rc = file.Write(off, data, size, FsWriteOption_None);
if (is_file_based_emummc) {
svcSleepThread(2e+6); // 2ms
// loop through every save file and store to zip.
for (const auto& collection : collections) {
for (const auto& file : collection.files) {
const auto file_path = fs::AppendPath(collection.path, file.name);
R_TRY(zip_add(file_path));
}
return rc;
}
));
}
}
fs->DeleteFile(path);
R_TRY(fs->RenameFile(temp_path, path));
// if we dumped the save to ram, flush the data to file.
if (!file_download) {
pbox->NewTransfer("Flushing zip to file");
R_TRY(writer->SetSize(mz_mem.buf.size()));
R_SUCCEED();
R_TRY(thread::Transfer(pbox, mz_mem.buf.size(),
[&](void* data, s64 off, s64 size, u64* bytes_read) -> Result {
size = std::min<s64>(size, mz_mem.buf.size() - off);
std::memcpy(data, mz_mem.buf.data() + off, size);
*bytes_read = size;
R_SUCCEED();
},
[&](const void* data, s64 off, s64 size) -> Result {
return writer->Write(data, off, size);
}
));
}
R_SUCCEED();
});
}
Result Menu::BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& location, Entry& e, bool compressed, bool is_auto) const {
std::vector<std::reference_wrapper<Entry>> entries;
entries.emplace_back(e);
return BackupSaveInternal(pbox, location, entries, compressed, is_auto);
}
Result Menu::MountSaveFs() {
@@ -1109,7 +1123,7 @@ Result Menu::MountSaveFs() {
if (e.system_save_data_id) {
fs::FsPath root;
R_TRY(devoptab::MountFromSavePath(e.system_save_data_id, root));
R_TRY(devoptab::MountSaveSystem(e.system_save_data_id, root));
auto fs = std::make_shared<filebrowser::FsStdioWrapper>(root, [&e](){
devoptab::UnmountSave(e.system_save_data_id);

View File

@@ -1,3 +1,4 @@
#if 0
#include "ui/menus/themezer.hpp"
#include "ui/menus/ghdl.hpp"
#include "ui/progress_box.hpp"
@@ -444,7 +445,7 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
if (touch && m_index == i) {
FireAction(Button::A);
} else {
App::PlaySoundEffect(SoundEffect_Focus);
App::PlaySoundEffect(SoundEffect::Focus);
SetIndex(i);
}
});
@@ -723,3 +724,4 @@ void Menu::DisplayOptions() {
}
} // namespace sphaira::ui::menu::themezer
#endif

View File

@@ -1,5 +1,3 @@
#if ENABLE_NETWORK_INSTALL
#include "ui/menus/usb_menu.hpp"
#include "yati/yati.hpp"
#include "app.hpp"
@@ -7,14 +5,18 @@
#include "log.hpp"
#include "ui/nvg_util.hpp"
#include "i18n.hpp"
#include "haze_helper.hpp"
#include "utils/thread.hpp"
#include <cstring>
namespace sphaira::ui::menu::usb {
namespace {
constexpr u64 CONNECTION_TIMEOUT = UINT64_MAX;
constexpr u64 TRANSFER_TIMEOUT = UINT64_MAX;
constexpr u64 FINISHED_TIMEOUT = 1e+9 * 3; // 3 seconds.
constexpr u64 CONNECTION_TIMEOUT = 3e+9;
constexpr u64 TRANSFER_TIMEOUT = 3e+9;
constexpr u64 FINISHED_TIMEOUT = 3e+9; // 3 seconds.
void thread_func(void* user) {
auto app = static_cast<Menu*>(user);
@@ -33,10 +35,10 @@ Menu::Menu(u32 flags) : MenuBase{"USB"_i18n, flags} {
}});
// if mtp is enabled, disable it for now.
m_was_mtp_enabled = App::GetMtpEnable();
m_was_mtp_enabled = haze::IsInit();
if (m_was_mtp_enabled) {
App::Notify("Disable MTP for usb install"_i18n);
App::SetMtpEnable(false);
haze::Exit();
}
// 3 second timeout for transfers.
@@ -47,15 +49,18 @@ Menu::Menu(u32 flags) : MenuBase{"USB"_i18n, flags} {
}
if (m_state != State::Failed) {
threadCreate(&m_thread, thread_func, this, nullptr, 1024*32, PRIO_PREEMPTIVE, 1);
utils::CreateThread(&m_thread, thread_func, this, 1024*32);
threadStart(&m_thread);
}
}
Menu::~Menu() {
// signal for thread to exit and wait.
m_stop_source.request_stop();
m_usb_source->SignalCancel();
if (R_FAILED(waitSingleHandle(m_thread.handle, 0))) {
m_usb_source->SignalCancel();
m_stop_source.request_stop();
}
threadWaitForExit(&m_thread);
threadClose(&m_thread);
@@ -65,7 +70,7 @@ Menu::~Menu() {
if (m_was_mtp_enabled) {
App::Notify("Re-enabled MTP"_i18n);
App::SetMtpEnable(true);
haze::Init();
}
}
@@ -92,20 +97,23 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
m_state = State::Progress;
log_write("got connection\n");
App::Push<ui::ProgressBox>(0, "Installing "_i18n, "", [this](auto pbox) -> Result {
ON_SCOPE_EXIT(m_usb_source->Finished(FINISHED_TIMEOUT));
// if we are doing s2s install, skip verifying the nca contents.
yati::ConfigOverride config_override{};
if (m_usb_source->IsStream()) {
config_override.skip_nca_hash_verify = true;
config_override.skip_rsa_header_fixed_key_verify = true;
config_override.skip_rsa_npdm_fixed_key_verify = true;
}
log_write("inside progress box\n");
for (const auto& file_name : m_names) {
for (u32 i = 0; i < std::size(m_names); i++) {
const auto& file_name = m_names[i];
s64 file_size;
R_TRY(m_usb_source->OpenFile(i, file_size));
ON_SCOPE_EXIT(m_usb_source->CloseFile());
// if we are doing s2s install, skip verifying the nca contents.
yati::ConfigOverride config_override{};
if (m_usb_source->IsStream()) {
config_override.skip_nca_hash_verify = true;
config_override.skip_rsa_header_fixed_key_verify = true;
config_override.skip_rsa_npdm_fixed_key_verify = true;
}
pbox->SetTitle(file_name);
m_usb_source->SetFileNameForTranfser(file_name);
const auto rc = yati::InstallFromSource(pbox, m_usb_source.get(), file_name, config_override);
if (R_FAILED(rc)) {
m_usb_source->SignalCancel();
@@ -191,5 +199,3 @@ void Menu::ThreadFunction() {
}
} // namespace sphaira::ui::menu::usb
#endif

View File

@@ -0,0 +1,277 @@
#include "ui/music_player.hpp"
#include "ui/nvg_util.hpp"
#include "app.hpp"
#include "i18n.hpp"
#include "image.hpp"
namespace sphaira::ui::music {
namespace {
constexpr u64 MAX_SEEK_DELTA = 30;
constexpr float VOLUME_DELTA = 0.20;
// returns seconds as: hh:mm:ss (from movienx)
inline auto TimeFormat(u64 sec) -> std::string {
char buf[9];
if (sec < 60) {
if (!sec) {
return "0:00";
}
std::sprintf(buf, "0:%02lu", sec % 60);
} else if (sec < 3600) {
std::sprintf(buf, "%lu:%02lu", ((sec / 60) % 60), sec % 60);
} else {
std::sprintf(buf, "%lu:%02lu:%02lu", ((sec / 3600) % 24), ((sec / 60) % 60), sec % 60);
}
return std::string{buf};
}
} // namespace
Menu::Menu(fs::Fs* fs, const fs::FsPath& path) {
SetAction(Button::B, Action{[this](){
SetPop();
}});
SetAction(Button::A, Action{[this](){
PauseToggle();
}});
SetAction(Button::LEFT, Action{[this](){
SeekBack();
}});
SetAction(Button::RIGHT, Action{[this](){
SeekForward();
}});
SetAction(Button::RS_UP, Action{[this](){
IncreaseVolume();
}});
SetAction(Button::RS_DOWN, Action{[this](){
DecreaseVolume();
}});
App::SetBackgroundMusicPause(true);
App::SetAutoSleepDisabled(true);
if (auto rc = lblInitialize(); R_FAILED(rc)) {
log_write("lblInitialize() failed: 0x%X\n", rc);
}
if (auto rc = audio::OpenSong(fs, path, 0, &m_song); R_FAILED(rc)) {
App::PushErrorBox(rc, "Failed to load music"_i18n);
SetPop();
return;
}
audio::GetInfo(m_song, &m_info);
audio::GetMeta(m_song, &m_meta);
if (!m_meta.image.empty()) {
m_icon = nvgCreateImageMem(App::GetVg(), 0, m_meta.image.data(), m_meta.image.size());
}
if (m_icon > 0) {
if (m_meta.title.empty()) {
m_meta.title = path.toString();
// only keep file name.
if (auto i = m_meta.title.find_last_of('/'); i != std::string::npos) {
m_meta.title = m_meta.title.substr(i + 1);
}
// remove extension.
if (auto i = m_meta.title.find_last_of('.'); i != std::string::npos) {
m_meta.title = m_meta.title.substr(0, i);
}
}
if (m_meta.artist.empty()) {
m_meta.artist = "Artist: Unknown";
}
if (m_meta.album.empty()) {
m_meta.album = "Album: Unknown";
}
}
audio::PlaySong(m_song);
}
Menu::~Menu() {
if (m_song) {
audio::CloseSong(&m_song);
}
if (m_icon) {
nvgDeleteImage(App::GetVg(), m_icon);
}
App::SetAutoSleepDisabled(false);
App::SetBackgroundMusicPause(false);
// restore backlight.
appletSetLcdBacklightOffEnabled(false);
lblExit();
}
void Menu::Update(Controller* controller, TouchInfo* touch) {
Widget::Update(controller, touch);
if (controller->m_kdown) {
LblBacklightSwitchStatus status;
// if any button was pressed and the screen is disabled, restore it.
if (R_SUCCEEDED(lblGetBacklightSwitchStatus(&status))) {
if (status != LblBacklightSwitchStatus_Enabled) {
appletSetLcdBacklightOffEnabled(false);
} else if (controller->GotDown(Button::Y)) {
// use applet here because it handles restoring backlight
// when pressing the home / power button.
appletSetLcdBacklightOffEnabled(true);
}
}
}
}
void Menu::Draw(NVGcontext* vg, Theme* theme) {
audio::Progress song_progress{};
audio::State song_state;
if (R_FAILED(audio::GetProgress(m_song, &song_progress, &song_state))) {
log_write("failed get song_progress\n");
SetPop();
return;
}
if (song_state == audio::State::Finished || song_state == audio::State::Error) {
log_write("got finished, doing pop now\n");
SetPop();
return;
}
const auto duration = (float)m_info.sample_count / (float)m_info.sample_rate;
const auto progress = (float)song_progress.played / (float)m_info.sample_rate;
const auto remaining = duration - progress;
const auto get_inner = [](const Vec4& bar, float progress, float duration) {
auto bar_inner = bar;
bar_inner.y += 2;
bar_inner.h -= 2 * 2;
bar_inner.x += 2;
bar_inner.w -= 2 * 2;
bar_inner.w *= progress / duration;
return bar_inner;
};
gfx::dimBackground(vg);
if (m_icon) {
const float icon_size = 220;
// draw background grid.
const auto pad = 30;
const auto gridy = (SCREEN_HEIGHT / 2) - (icon_size / 2) - pad;
const auto gridh = icon_size + pad * 2;
const Vec4 grid{this->osd_bar_outline.x, gridy, this->osd_bar_outline.w, gridh};
gfx::drawRect(vg, grid, theme->GetColour(ThemeEntryID_GRID), 15);
nvgSave(vg);
nvgIntersectScissor(vg, grid.x + pad, grid.y + pad, grid.w - pad * 2, grid.h - pad * 2);
ON_SCOPE_EXIT(nvgRestore(vg));
// draw icon.
const Vec4 icon{grid.x + pad, grid.y + pad, icon_size, icon_size};
gfx::drawImage(vg, icon, m_icon, 0);
// draw meta info.
const auto xoff = icon.x + icon_size + pad;
const auto wend = grid.w - (xoff - grid.x) - 30;
m_scroll_title.Draw(vg, true, xoff, icon.y + 50, wend, 22, NVG_ALIGN_LEFT|NVG_ALIGN_BOTTOM, theme->GetColour(ThemeEntryID_TEXT), m_meta.title);
m_scroll_artist.Draw(vg, true, xoff, icon.y + 90, wend, 20, NVG_ALIGN_LEFT|NVG_ALIGN_BOTTOM, theme->GetColour(ThemeEntryID_TEXT_INFO), m_meta.artist);
m_scroll_album.Draw(vg, true, xoff, icon.y + 130, wend, 20, NVG_ALIGN_LEFT|NVG_ALIGN_BOTTOM, theme->GetColour(ThemeEntryID_TEXT_INFO), m_meta.album);
// draw progress bar.
const Vec4 progress_bar{xoff, grid.y + grid.h - 30 - 60, osd_bar_outline.w - (xoff - osd_bar_outline.x) - 30, 10.f};
const auto inner = get_inner(progress_bar, progress, duration);
gfx::drawRect(vg, progress_bar, theme->GetColour(ThemeEntryID_PROGRESSBAR_BACKGROUND), 3);
gfx::drawRect(vg, inner, theme->GetColour(ThemeEntryID_PROGRESSBAR), 3);
// draw progress time text.
const Vec2 time_text_left{progress_bar.x, progress_bar.y + progress_bar.h + 20};
const Vec2 time_text_right{progress_bar.x + progress_bar.w, progress_bar.y + progress_bar.h + 20};
gfx::drawText(vg, time_text_left, 18.f, theme->GetColour(ThemeEntryID_TEXT), TimeFormat(progress).c_str(), NVG_ALIGN_LEFT | NVG_ALIGN_TOP);
gfx::drawText(vg, time_text_right, 18.f, theme->GetColour(ThemeEntryID_TEXT), TimeFormat(duration).c_str(), NVG_ALIGN_RIGHT | NVG_ALIGN_TOP);
} else {
// draw background grid.
gfx::drawRect(vg, this->osd_bar_outline, theme->GetColour(ThemeEntryID_POPUP), 15);
// draw progress bar.
const auto inner = get_inner(this->osd_progress_bar, progress, duration);
gfx::drawRect(vg, this->osd_progress_bar, theme->GetColour(ThemeEntryID_PROGRESSBAR_BACKGROUND), 3);
gfx::drawRect(vg, inner, theme->GetColour(ThemeEntryID_PROGRESSBAR), 3);
// draw chapter markers (if any)
if (m_info.looping) {
const auto loop = (float)m_info.loop_start / (float)m_info.sample_rate;
const auto marker = Vec4{osd_progress_bar.x + (osd_progress_bar.w * loop / duration), osd_progress_bar.y - 4.f, 3.f, osd_progress_bar.h + 8.f};
gfx::drawRect(vg, marker, theme->GetColour(ThemeEntryID_TEXT_INFO));
}
// draw progress time text.
gfx::drawText(vg, this->osd_time_text_left, 20.f, theme->GetColour(ThemeEntryID_TEXT), TimeFormat((progress)).c_str(), NVG_ALIGN_RIGHT | NVG_ALIGN_TOP);
gfx::drawText(vg, this->osd_time_text_right, 20.f, theme->GetColour(ThemeEntryID_TEXT), ('-' + TimeFormat((remaining))).c_str(), NVG_ALIGN_LEFT | NVG_ALIGN_TOP);
}
}
void Menu::PauseToggle() {
audio::State state{};
audio::GetProgress(m_song, nullptr, &state);
if (state == audio::State::Playing) {
audio::PauseSong(m_song);
} else if (state == audio::State::Paused) {
audio::PlaySong(m_song);
}
}
void Menu::SeekForward() {
audio::Progress progress{};
audio::GetProgress(m_song, &progress, nullptr);
const u64 max_delta = m_info.sample_rate * MAX_SEEK_DELTA;
u64 next = std::min<u64>(progress.played + (m_info.sample_count / 10), m_info.sample_count);
next = std::min<u64>(next, progress.played + max_delta);
audio::SeekSong(m_song, next);
}
void Menu::SeekBack() {
audio::Progress progress{};
audio::GetProgress(m_song, &progress, nullptr);
const s64 max_delta = m_info.sample_rate * MAX_SEEK_DELTA;
u64 next = std::max(s64(progress.played) - s64(m_info.sample_count / 10), s64(0));
next = std::max<s64>(next, s64(progress.played) - max_delta);
audio::SeekSong(m_song, next);
}
void Menu::IncreaseVolume() {
float volume;
if (R_SUCCEEDED(audio::GetVolumeSong(m_song, &volume))) {
audio::SetVolumeSong(m_song, volume + VOLUME_DELTA);
log_write("volume: %.2f\n", volume);
}
}
void Menu::DecreaseVolume() {
float volume;
if (R_SUCCEEDED(audio::GetVolumeSong(m_song, &volume))) {
audio::SetVolumeSong(m_song, volume - VOLUME_DELTA);
log_write("volume: %.2f\n", volume);
}
}
} // namespace sphaira::ui::music

View File

@@ -6,7 +6,11 @@
#include "log.hpp"
#include "threaded_file_transfer.hpp"
#include "i18n.hpp"
#include "utils/thread.hpp"
#include <cstring>
#include <cmath>
namespace sphaira::ui {
namespace {
@@ -17,13 +21,44 @@ void threadFunc(void* arg) {
d->pbox->RequestExit();
}
// https://github.com/memononen/nanovg/blob/f93799c078fa11ed61c078c65a53914c8782c00b/example/demo.c#L500
void drawSpinner(NVGcontext* vg, Theme* theme, float cx, float cy, float r, float t)
{
float a0 = 0.0f + t*6;
float a1 = NVG_PI + t*6;
float r0 = r;
float r1 = r * 0.75f;
float ax,ay, bx,by;
NVGpaint paint;
nvgSave(vg);
auto colourb = theme->GetColour(ThemeEntryID_PROGRESSBAR);
colourb.a = 0.5;
nvgBeginPath(vg);
nvgArc(vg, cx,cy, r0, a0, a1, NVG_CW);
nvgArc(vg, cx,cy, r1, a1, a0, NVG_CCW);
nvgClosePath(vg);
ax = cx + cosf(a0) * (r0+r1)*0.5f;
ay = cy + sinf(a0) * (r0+r1)*0.5f;
bx = cx + cosf(a1) * (r0+r1)*0.5f;
by = cy + sinf(a1) * (r0+r1)*0.5f;
paint = nvgLinearGradient(vg, ax,ay, bx,by, nvgRGBA(0,0,0,0), colourb);
nvgFillPaint(vg, paint);
nvgFill(vg);
nvgRestore(vg);
}
} // namespace
ProgressBox::ProgressBox(int image, const std::string& action, const std::string& title, const ProgressBoxCallback& callback, const ProgressBoxDoneCallback& done, int cpuid, int prio, int stack_size)
ProgressBox::ProgressBox(int image, const std::string& action, const std::string& title, const ProgressBoxCallback& callback, const ProgressBoxDoneCallback& done)
: m_done{done}
, m_action{action}
, m_title{title}
, m_image{image} {
App::SetAutoSleepDisabled(true);
if (App::GetApp()->m_progress_boost_mode.Get()) {
App::SetBoostMode(true);
}
@@ -45,10 +80,9 @@ ProgressBox::ProgressBox(int image, const std::string& action, const std::string
// create cancel event.
ueventCreate(&m_uevent, false);
m_cpuid = cpuid;
m_thread_data.pbox = this;
m_thread_data.callback = callback;
if (R_FAILED(threadCreate(&m_thread, threadFunc, &m_thread_data, nullptr, stack_size, prio, cpuid))) {
if (R_FAILED(utils::CreateThread(&m_thread, threadFunc, &m_thread_data))) {
log_write("failed to create thead\n");
}
if (R_FAILED(threadStart(&m_thread))) {
@@ -68,9 +102,12 @@ ProgressBox::~ProgressBox() {
}
FreeImage();
m_done(m_thread_data.result);
if (m_done) {
m_done(m_thread_data.result);
}
App::SetBoostMode(false);
App::SetAutoSleepDisabled(false);
}
auto ProgressBox::Update(Controller* controller, TouchInfo* touch) -> void {
@@ -122,7 +159,7 @@ auto ProgressBox::Draw(NVGcontext* vg, Theme* theme) -> void {
const auto center_x = m_pos.x + m_pos.w/2;
const auto end_y = m_pos.y + m_pos.h;
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 };
const Vec4 prog_bar = { center_x - progress_bar_w / 2, end_y - 95, progress_bar_w, 12 };
nvgSave(vg);
nvgIntersectScissor(vg, GetX(), GetY(), GetW(), GetH());
@@ -140,7 +177,10 @@ auto ProgressBox::Draw(NVGcontext* vg, Theme* theme) -> void {
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), 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);
gfx::drawTextArgs(vg, prog_bar.x + prog_bar.w + pad, prog_bar.y + prog_bar.h / 2, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT), "%u%%", percentage);
const auto rad = 15;
drawSpinner(vg, theme, prog_bar.x - pad - rad, prog_bar.y + prog_bar.h / 2, rad, armTicksToNs(armGetSystemTick()) / 1e+9);
const double speed_mb = (double)speed / (1024.0 * 1024.0);
const double speed_kb = (double)speed / (1024.0);
@@ -219,6 +259,17 @@ auto ProgressBox::NewTransfer(const std::string& transfer) -> ProgressBox& {
return *this;
}
auto ProgressBox::ResetTranfser() -> ProgressBox& {
mutexLock(&m_mutex);
m_size = 0;
m_offset = 0;
m_last_offset = 0;
m_timestamp.Update();
mutexUnlock(&m_mutex);
Yield();
return *this;
}
auto ProgressBox::UpdateTransfer(s64 offset, s64 size) -> ProgressBox& {
mutexLock(&m_mutex);
m_size = size;

View File

@@ -27,7 +27,7 @@ ScrollableText::ScrollableText(const std::string& text, float x, float y, float
}
m_y_off -= m_step;
m_index++;
App::PlaySoundEffect(SoundEffect_Scroll);
App::PlaySoundEffect(SoundEffect::Scroll);
}}),
std::make_pair(Button::LS_UP, Action{[this](){
if (m_y_off == m_y_off_base) {
@@ -35,7 +35,7 @@ ScrollableText::ScrollableText(const std::string& text, float x, float y, float
}
m_y_off += m_step;
m_index--;
App::PlaySoundEffect(SoundEffect_Scroll);
App::PlaySoundEffect(SoundEffect::Scroll);
}})
);

View File

@@ -154,6 +154,52 @@ void SidebarEntryBool::Draw(NVGcontext* vg, Theme* theme, const Vec4& root_pos,
SidebarEntryBase::DrawEntry(vg, theme, m_title, m_option ? m_true_str : m_false_str, m_option);
}
SidebarEntrySlider::SidebarEntrySlider(const std::string& title, float value, float min, float max, int steps, const Callback& cb, const std::string& info)
: SidebarEntryBase{title, info}
, m_value{value}
, m_min{min}
, m_max{max}
, m_steps{steps}
, m_callback{cb} {
SetAction(Button::LEFT, Action{[this](){
if (!IsEnabled()) {
DependsClick();
} else {
m_value = std::clamp(m_value - m_inc, m_min, m_max);
// m_callback(m_option);
} }
});
SetAction(Button::RIGHT, Action{[this](){
if (!IsEnabled()) {
DependsClick();
} else {
m_value = std::clamp(m_value + m_inc, m_min, m_max);
// m_callback(m_option);
} }
});
m_duration = m_max - m_min;
m_inc = m_duration / (float)(m_steps);
}
void SidebarEntrySlider::Draw(NVGcontext* vg, Theme* theme, const Vec4& root_pos, bool left) {
SidebarEntryBase::Draw(vg, theme, root_pos, left);
const float barh = 7;
const Vec4 bar{m_pos.x + 15.f, m_pos.y + (m_pos.h / 2.f) - barh / 2, m_pos.w - 15.f * 2, barh};
gfx::drawRect(vg, bar, theme->GetColour(ThemeEntryID_PROGRESSBAR_BACKGROUND), 3);
auto inner = bar;
inner.w *= m_value / m_duration;
gfx::drawRect(vg, inner, theme->GetColour(ThemeEntryID_PROGRESSBAR), 3);
for (int i = 0; i <= m_steps; i++) {
const auto loop = m_inc * (float)i;
const auto marker = Vec4{bar.x + (bar.w * loop / m_duration), bar.y - 4.f, 3.f, bar.h + 8.f};
gfx::drawRect(vg, marker, theme->GetColour(ThemeEntryID_TEXT_INFO));
}
}
SidebarEntryCallback::SidebarEntryCallback(const std::string& title, const Callback& cb, bool pop_on_click, const std::string& info)
: SidebarEntryBase{title, info}
, m_callback{cb}
@@ -422,6 +468,13 @@ void Sidebar::SetupButtons() {
SetPop();
}})
);
// disable jump page if the item is using left/right buttons.
if (HasAction(Button::LEFT) || HasAction(Button::RIGHT)) {
m_list->SetPageJump(false);
} else {
m_list->SetPageJump(true);
}
}
} // namespace sphaira::ui

View File

@@ -33,7 +33,7 @@ void Widget::Update(Controller* controller, TouchInfo* touch) {
for (const auto& [button, action] : m_actions) {
if ((action.m_type & ActionType::DOWN) && controller->GotDown(button)) {
if (static_cast<u64>(button) & static_cast<u64>(Button::ANY_BUTTON)) {
App::PlaySoundEffect(SoundEffect_Focus);
App::PlaySoundEffect(SoundEffect::Focus);
}
action.Invoke(true);
break;
@@ -83,7 +83,7 @@ void Widget::RemoveAction(Button button) {
auto Widget::FireAction(Button b, u8 type) -> bool {
for (const auto& [button, action] : m_actions) {
if (button == b && (action.m_type & type)) {
App::PlaySoundEffect(SoundEffect_Focus);
App::PlaySoundEffect(SoundEffect::Focus);
action.Invoke(true);
return true;
}

View File

@@ -16,8 +16,6 @@
// The USB transfer code was taken from Haze (part of Atmosphere).
#if ENABLE_NETWORK_INSTALL
#include "usb/base.hpp"
#include "log.hpp"
#include "defines.hpp"
@@ -100,5 +98,3 @@ Result Base::TransferAll(bool read, void *data, u32 size, u64 timeout) {
}
} // namespace sphaira::usb
#endif

View File

@@ -0,0 +1,79 @@
#include "usb/usb_dumper.hpp"
#include "usb/usb_api.hpp"
#include "defines.hpp"
#include "log.hpp"
#include <ranges>
namespace sphaira::usb::dump {
namespace {
using namespace usb::api;
} // namespace
Usb::Usb(u64 transfer_timeout) {
m_usb = std::make_unique<usb::UsbDs>(transfer_timeout);
m_open_result = m_usb->Init();
}
Usb::~Usb() {
if (m_was_connected && R_SUCCEEDED(m_usb->IsUsbConnected(0))) {
SendHeader send_header{MAGIC, CMD_QUIT};
SendAndVerify(&send_header, sizeof(send_header));
}
}
Result Usb::IsUsbConnected(u64 timeout) {
return m_usb->IsUsbConnected(timeout);
}
Result Usb::WaitForConnection(std::string_view path, u64 timeout) {
m_was_connected = false;
// ensure that we are connected.
R_TRY(m_open_result);
R_TRY(m_usb->IsUsbConnected(timeout));
SendHeader send_header{MAGIC, CMD_EXPORT, (u32)path.length()};
R_TRY(SendAndVerify(&send_header, sizeof(send_header), timeout));
R_TRY(SendAndVerify(path.data(), path.length(), timeout));
m_was_connected = true;
R_SUCCEED();
}
Result Usb::CloseFile() {
SendDataHeader send_header{0, 0};
return SendAndVerify(&send_header, sizeof(send_header));
}
void Usb::SignalCancel() {
m_usb->Cancel();
}
Result Usb::Write(const void* buf, u64 off, u32 size) {
SendDataHeader send_header{off, size, crc32cCalculate(buf, size)};
R_TRY(SendAndVerify(&send_header, sizeof(send_header)));
return SendAndVerify(buf, size);
}
// casts away const, but it does not modify the buffer!
Result Usb::SendAndVerify(const void* data, u32 size, u64 timeout, ResultHeader* out) {
R_TRY(m_usb->TransferAll(false, const_cast<void*>(data), size, timeout));
ResultHeader recv_header;
R_TRY(m_usb->TransferAll(true, &recv_header, sizeof(recv_header), timeout));
R_TRY(recv_header.Verify());
if (out) *out = recv_header;
R_SUCCEED();
}
Result Usb::SendAndVerify(const void* data, u32 size, ResultHeader* out) {
return SendAndVerify(data, size, m_usb->GetTransferTimeout(), out);
}
} // namespace sphaira::usb::dump

View File

@@ -0,0 +1,120 @@
#include "usb/usb_installer.hpp"
#include "usb/usb_api.hpp"
#include "defines.hpp"
#include "log.hpp"
#include <ranges>
namespace sphaira::usb::install {
namespace {
using namespace usb::api;
} // namespace
Usb::Usb(u64 transfer_timeout) {
m_usb = std::make_unique<usb::UsbDs>(transfer_timeout);
m_open_result = m_usb->Init();
}
Usb::~Usb() {
if (m_was_connected && R_SUCCEEDED(m_usb->IsUsbConnected(0))) {
SendHeader send_header{MAGIC, CMD_QUIT};
SendAndVerify(&send_header, sizeof(send_header));
}
}
Result Usb::IsUsbConnected(u64 timeout) {
return m_usb->IsUsbConnected(timeout);
}
Result Usb::WaitForConnection(u64 timeout, std::vector<std::string>& out_names) {
m_was_connected = false;
// ensure that we are connected.
R_TRY(m_open_result);
R_TRY(m_usb->IsUsbConnected(timeout));
SendHeader send_header{MAGIC, RESULT_OK};
ResultHeader recv_header;
R_TRY(SendAndVerify(&send_header, sizeof(send_header), timeout, &recv_header))
std::vector<char> names(recv_header.arg3);
R_TRY(m_usb->TransferAll(true, names.data(), names.size(), timeout));
out_names.clear();
for (const auto& name : std::views::split(names, '\n')) {
if (!name.empty()) {
auto& it = out_names.emplace_back(name.data(), name.size());
log_write("[USB] got name: %s\n", it.c_str());
}
}
m_flags = recv_header.arg4;
m_was_connected = true;
R_SUCCEED();
}
Result Usb::OpenFile(u32 index, s64& file_size) {
log_write("doing open file\n");
SendHeader send_header{MAGIC, CMD_OPEN, index};
ResultHeader recv_header;
R_TRY(SendAndVerify(&send_header, sizeof(send_header), &recv_header))
log_write("did open file\n");
const auto flags = recv_header.arg3 >> 16;
const auto file_size_msb = recv_header.arg3 & 0xFFFF;
const auto file_size_lsb = recv_header.arg4;
m_flags = flags;
file_size = ((u64)file_size_msb << 32) | file_size_lsb;
R_SUCCEED();
}
Result Usb::CloseFile() {
SendDataHeader send_header{0, 0};
return SendAndVerify(&send_header, sizeof(send_header));
}
void Usb::SignalCancel() {
m_usb->Cancel();
}
u32 Usb::GetFlags() const {
return m_flags;
}
Result Usb::Read(void* buf, u64 off, u32 size, u64* bytes_read) {
SendDataHeader send_header{off, size};
ResultHeader recv_header;
R_TRY(SendAndVerify(&send_header, sizeof(send_header), &recv_header))
// adjust the size and read the data.
size = recv_header.arg3;
R_TRY(m_usb->TransferAll(true, buf, size));
// verify crc32c.
R_UNLESS(crc32cCalculate(buf, size) == recv_header.arg4, 3);
*bytes_read = size;
R_SUCCEED();
}
// casts away const, but it does not modify the buffer!
Result Usb::SendAndVerify(const void* data, u32 size, u64 timeout, ResultHeader* out) {
R_TRY(m_usb->TransferAll(false, const_cast<void*>(data), size, timeout));
ResultHeader recv_header;
R_TRY(m_usb->TransferAll(true, &recv_header, sizeof(recv_header), timeout));
R_TRY(recv_header.Verify());
if (out) *out = recv_header;
R_SUCCEED();
}
Result Usb::SendAndVerify(const void* data, u32 size, ResultHeader* out) {
return SendAndVerify(data, size, m_usb->GetTransferTimeout(), out);
}
} // namespace sphaira::usb::install

View File

@@ -1,16 +1,11 @@
// The USB protocol was taken from Tinfoil, by Adubbz.
#if ENABLE_NETWORK_INSTALL
#include "usb/usb_uploader.hpp"
#include "usb/tinfoil.hpp"
#include "usb/usb_api.hpp"
#include "log.hpp"
#include "defines.hpp"
namespace sphaira::usb::upload {
namespace {
namespace tinfoil = usb::tinfoil;
const UsbHsInterfaceFilter FILTER{
.Flags = UsbHsInterfaceFilterFlags_idVendor |
UsbHsInterfaceFilterFlags_idProduct |
@@ -36,6 +31,8 @@ const UsbHsInterfaceFilter FILTER{
constexpr u8 INDEX = 0;
using namespace usb::api;
} // namespace
Usb::Usb(u64 transfer_timeout) {
@@ -46,69 +43,90 @@ Usb::Usb(u64 transfer_timeout) {
Usb::~Usb() {
}
Result Usb::WaitForConnection(u64 timeout, u8 flags, std::span<const std::string> names) {
Result Usb::WaitForConnection(u64 timeout, std::span<const std::string> names) {
R_TRY(m_usb->IsUsbConnected(timeout));
// build name table.
std::string names_list;
for (auto& name : names) {
names_list += name + '\n';
}
tinfoil::TUSHeader header{};
header.magic = tinfoil::Magic_List0;
header.nspListSize = names_list.length();
header.flags = flags;
// send.
SendHeader send_header;
R_TRY(m_usb->TransferAll(true, &send_header, sizeof(send_header), timeout));
R_TRY(send_header.Verify());
R_TRY(m_usb->TransferAll(false, &header, sizeof(header), timeout));
// send table info.
R_TRY(SendResult(RESULT_OK, names_list.length()));
// send name table.
R_TRY(m_usb->TransferAll(false, names_list.data(), names_list.length(), timeout));
R_SUCCEED();
}
Result Usb::PollCommands() {
tinfoil::USBCmdHeader header;
R_TRY(m_usb->TransferAll(true, &header, sizeof(header)));
R_UNLESS(header.magic == tinfoil::Magic_Command0, Result_UsbUploadBadMagic);
SendHeader send_header;
R_TRY(m_usb->TransferAll(true, &send_header, sizeof(send_header)));
R_TRY(send_header.Verify());
if (header.cmdId == tinfoil::USBCmdId::EXIT) {
if (send_header.cmd == CMD_QUIT) {
R_TRY(SendResult(RESULT_OK));
R_THROW(Result_UsbUploadExit);
} else if (header.cmdId == tinfoil::USBCmdId::FILE_RANGE) {
return FileRangeCmd(header.dataSize);
} else if (send_header.cmd == CMD_OPEN) {
s64 file_size;
u16 flags;
R_TRY(Open(send_header.arg3, file_size, flags));
const auto size_lsb = file_size & 0xFFFFFFFF;
const auto size_msb = ((file_size >> 32) & 0xFFFF) | (flags << 16);
return SendResult(RESULT_OK, size_msb, size_lsb);
} else {
R_TRY(SendResult(RESULT_ERROR));
R_THROW(Result_UsbUploadBadCommand);
}
}
Result Usb::FileRangeCmd(u64 data_size) {
tinfoil::FileRangeCmdHeader header;
R_TRY(m_usb->TransferAll(true, &header, sizeof(header)));
Result Usb::file_transfer_loop() {
log_write("doing file transfer\n");
std::string path(header.nspNameLen, '\0');
R_TRY(m_usb->TransferAll(true, path.data(), header.nspNameLen));
// get offset + size.
SendDataHeader send_header;
R_TRY(m_usb->TransferAll(true, &send_header, sizeof(send_header)));
// send response header.
R_TRY(m_usb->TransferAll(false, &header, sizeof(header)));
s64 curr_off = 0x0;
s64 end_off = header.size;
s64 read_size = header.size;
m_buf.resize(header.size);
while (curr_off < end_off) {
if (curr_off + read_size >= end_off) {
read_size = end_off - curr_off;
}
u64 bytes_read;
R_TRY(Read(path, m_buf.data(), header.offset + curr_off, read_size, &bytes_read));
R_TRY(m_usb->TransferAll(false, m_buf.data(), bytes_read));
curr_off += bytes_read;
// check if we should finish now.
if (send_header.offset == 0 && send_header.size == 0) {
log_write("finished\n");
R_TRY(SendResult(RESULT_OK));
return Result_UsbUploadExit;
}
// read file and calculate the hash.
u64 bytes_read;
m_buf.resize(send_header.size);
log_write("reading buffer: %zu\n", m_buf.size());
R_TRY(Read(m_buf.data(), send_header.offset, m_buf.size(), &bytes_read));
const auto crc32 = crc32Calculate(m_buf.data(), m_buf.size());
log_write("read the buffer: %zu\n", bytes_read);
// respond back with the length of the data and the crc32.
R_TRY(SendResult(RESULT_OK, m_buf.size(), crc32));
log_write("sent result with crc\n");
// send the data.
R_TRY(m_usb->TransferAll(false, m_buf.data(), m_buf.size()));
log_write("sent the data\n");
R_SUCCEED();
}
} // namespace sphaira::usb::upload
Result Usb::SendResult(u32 result, u32 arg3, u32 arg4) {
ResultHeader recv_header{MAGIC, result, arg3, arg4};
return m_usb->TransferAll(false, &recv_header, sizeof(recv_header));
}
#endif
} // namespace sphaira::usb::upload

View File

@@ -1,20 +1,9 @@
#if ENABLE_NETWORK_INSTALL
#include "usb/usbds.hpp"
#include "log.hpp"
#include "defines.hpp"
#include <ranges>
#include <cstring>
Result usbDsGetSpeed(UsbDeviceSpeed *out) {
if (hosversionBefore(8,0,0)) {
return MAKERESULT(Module_Libnx, LibnxError_IncompatSysVer);
}
serviceAssumeDomain(usbDsGetServiceSession());
return serviceDispatchOut(usbDsGetServiceSession(), hosversionAtLeast(11,0,0) ? 11 : 12, *out);
}
auto GetUsbDsStateStr(UsbState state) -> const char* {
switch (state) {
case UsbState_Detached: return "Detached";
@@ -30,8 +19,7 @@ auto GetUsbDsStateStr(UsbState state) -> const char* {
}
auto GetUsbDsSpeedStr(UsbDeviceSpeed speed) -> const char* {
// todo: remove this cast when libnx pr is merged.
switch ((u32)speed) {
switch (speed) {
case UsbDeviceSpeed_None: return "None";
case UsbDeviceSpeed_Low: return "USB 1.0 Low Speed";
case UsbDeviceSpeed_Full: return "USB 1.1 Full Speed";
@@ -204,6 +192,12 @@ Result UsbDs::Init() {
R_TRY(usbDsInterface_EnableInterface(m_interface));
R_TRY(usbDsEnable());
// not needed because the api sends the size of the sent data
// before each write, so there's not need to send a zlt.
#if 0
R_TRY(usbDsEndpoint_SetZlt(m_endpoints[UsbSessionEndpoint_In], true));
#endif
log_write("success USB init\n");
R_SUCCEED();
}
@@ -321,15 +315,6 @@ Result UsbDs::WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) {
}
Result UsbDs::TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 remaining, u32 size, u32 *out_urb_id) {
if (ep == UsbSessionEndpoint_In) {
if (size && remaining == size && !(size % (u32)m_max_packet_size)) {
log_write("[USBDS] SetZlt(true)\n");
R_TRY(usbDsEndpoint_SetZlt(m_endpoints[ep], true));
} else {
R_TRY(usbDsEndpoint_SetZlt(m_endpoints[ep], false));
}
}
return usbDsEndpoint_PostBufferAsync(m_endpoints[ep], buffer, size, out_urb_id);
}
@@ -344,5 +329,3 @@ Result UsbDs::GetTransferResult(UsbSessionEndpoint ep, u32 urb_id, u32 *out_requ
}
} // namespace sphaira::usb
#endif

Some files were not shown because too many files have changed in this diff Show More