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 out
usb_test/ usb_test/
__pycache__
usb_*.spec

View File

@@ -18,17 +18,6 @@
"inherits":["core"], "inherits":["core"],
"cacheVariables": { "cacheVariables": {
"CMAKE_BUILD_TYPE": "MinSizeRel", "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 "LTO": true
} }
}, },
@@ -38,8 +27,8 @@
"inherits":["core"], "inherits":["core"],
"cacheVariables": { "cacheVariables": {
"CMAKE_BUILD_TYPE": "MinSizeRel", "CMAKE_BUILD_TYPE": "MinSizeRel",
"ENABLE_NETWORK_INSTALL": true, "LTO": false,
"LTO": false "DEV_BUILD": true
} }
} }
], ],
@@ -49,11 +38,6 @@
"configurePreset": "Release", "configurePreset": "Release",
"jobs": 16 "jobs": 16
}, },
{
"name": "ReleaseWithInstall",
"configurePreset": "ReleaseWithInstall",
"jobs": 16
},
{ {
"name": "Dev", "name": "Dev",
"configurePreset": "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 add_executable(sphaira
source/ui/menus/appstore.cpp source/ui/menus/appstore.cpp
source/ui/menus/file_viewer.cpp source/ui/menus/file_viewer.cpp
source/ui/menus/image_viewer.cpp
source/ui/menus/filebrowser.cpp source/ui/menus/filebrowser.cpp
source/ui/menus/file_picker.cpp source/ui/menus/file_picker.cpp
source/ui/menus/homebrew.cpp source/ui/menus/homebrew.cpp
@@ -65,6 +66,7 @@ add_executable(sphaira
source/ui/widget.cpp source/ui/widget.cpp
source/ui/list.cpp source/ui/list.cpp
source/ui/scrolling_text.cpp source/ui/scrolling_text.cpp
source/ui/music_player.cpp
source/app.cpp source/app.cpp
source/download.cpp source/download.cpp
@@ -89,29 +91,39 @@ add_executable(sphaira
source/title_info.cpp source/title_info.cpp
source/minizip_helper.cpp source/minizip_helper.cpp
source/fatfs.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_common.cpp
source/utils/devoptab_romfs.cpp
source/utils/devoptab_save.cpp source/utils/devoptab_save.cpp
source/utils/devoptab_nro.cpp
source/utils/devoptab_nca.cpp
source/utils/devoptab_nsp.cpp source/utils/devoptab_nsp.cpp
source/utils/devoptab_xci.cpp source/utils/devoptab_xci.cpp
source/utils/devoptab_zip.cpp source/utils/devoptab_zip.cpp
source/utils/devoptab_bfsar.cpp
source/usb/base.cpp source/usb/base.cpp
source/usb/usbds.cpp source/usb/usbds.cpp
source/usb/usbhs.cpp source/usb/usbhs.cpp
source/usb/usb_uploader.cpp source/usb/usb_uploader.cpp
source/usb/usb_installer.cpp
source/usb/usb_dumper.cpp
source/yati/yati.cpp source/yati/yati.cpp
source/yati/container/nsp.cpp source/yati/container/nsp.cpp
source/yati/container/xci.cpp source/yati/container/xci.cpp
source/yati/source/file.cpp source/yati/source/file.cpp
source/yati/source/usb.cpp
source/yati/source/stream.cpp source/yati/source/stream.cpp
source/yati/source/stream_file.cpp source/yati/source/stream_file.cpp
source/yati/nx/es.cpp source/yati/nx/es.cpp
source/yati/nx/keys.cpp source/yati/nx/keys.cpp
source/yati/nx/nca.cpp source/yati/nx/nca.cpp
source/yati/nx/ncz.cpp
source/yati/nx/ncm.cpp source/yati/nx/ncm.cpp
source/yati/nx/ns.cpp source/yati/nx/ns.cpp
@@ -123,7 +135,8 @@ target_compile_definitions(sphaira PRIVATE
-DAPP_VERSION="${sphaira_VERSION}" -DAPP_VERSION="${sphaira_VERSION}"
-DAPP_VERSION_HASH="${sphaira_VERSION_HASH}" -DAPP_VERSION_HASH="${sphaira_VERSION_HASH}"
-DCURL_NO_OLDIES=1 -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 target_compile_options(sphaira PRIVATE
@@ -177,12 +190,12 @@ FetchContent_Declare(ftpsrv
FetchContent_Declare(libhaze FetchContent_Declare(libhaze
GIT_REPOSITORY https://github.com/ITotalJustice/libhaze.git GIT_REPOSITORY https://github.com/ITotalJustice/libhaze.git
GIT_TAG 0be1523 GIT_TAG f0b2a14
) )
FetchContent_Declare(libpulsar FetchContent_Declare(libpulsar
GIT_REPOSITORY https://github.com/ITotalJustice/switch-libpulsar.git GIT_REPOSITORY https://github.com/ITotalJustice/switch-libpulsar.git
GIT_TAG de656e4 GIT_TAG ac7bc97
) )
FetchContent_Declare(nanovg FetchContent_Declare(nanovg
@@ -197,7 +210,7 @@ FetchContent_Declare(stb
FetchContent_Declare(yyjson FetchContent_Declare(yyjson
GIT_REPOSITORY https://github.com/ibireme/yyjson.git GIT_REPOSITORY https://github.com/ibireme/yyjson.git
GIT_TAG 0.11.1 GIT_TAG 0.12.0
) )
FetchContent_Declare(minIni FetchContent_Declare(minIni
@@ -213,7 +226,7 @@ FetchContent_Declare(zstd
FetchContent_Declare(libusbhsfs FetchContent_Declare(libusbhsfs
GIT_REPOSITORY https://github.com/ITotalJustice/libusbhsfs.git GIT_REPOSITORY https://github.com/ITotalJustice/libusbhsfs.git
GIT_TAG d0a973e GIT_TAG 625269b
) )
FetchContent_Declare(libnxtc FetchContent_Declare(libnxtc
@@ -226,31 +239,47 @@ FetchContent_Declare(nvjpg
GIT_TAG 45680e7 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) set(USE_NEW_ZSTD ON)
# has issues with some homebrew and game icons (oxenfree, overwatch2). # 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_STATIC ON)
set(ZSTD_BUILD_SHARED OFF) 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_DECOMPRESSION ON)
set(ZSTD_BUILD_DICTBUILDER OFF) set(ZSTD_BUILD_DICTBUILDER OFF)
set(ZSTD_LEGACY_SUPPORT OFF) set(ZSTD_LEGACY_SUPPORT OFF)
set(ZSTD_MULTITHREAD_SUPPORT OFF)
set(ZSTD_BUILD_PROGRAMS OFF) set(ZSTD_BUILD_PROGRAMS OFF)
set(ZSTD_BUILD_TESTS OFF) set(ZSTD_BUILD_TESTS OFF)
set(MININI_LIB_NAME minIni) set(MININI_LIB_NAME minIni)
set(MININI_USE_STDIO ON) set(MININI_USE_STDIO ON)
set(MININI_USE_NX OFF) 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") if (CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")
set(NANOVG_DEBUG ON) set(NANOVG_DEBUG ON)
endif() endif()
set(NANOVG_NO_JPEG OFF) set(NANOVG_NO_JPEG OFF)
set(NANOVG_NO_PNG OFF) set(NANOVG_NO_PNG OFF)
set(NANOVG_NO_BMP ON) set(NANOVG_NO_BMP OFF)
set(NANOVG_NO_PSD ON) set(NANOVG_NO_PSD ON)
set(NANOVG_NO_TGA ON) set(NANOVG_NO_TGA ON)
set(NANOVG_NO_GIF ON) set(NANOVG_NO_GIF ON)
@@ -283,6 +312,9 @@ FetchContent_MakeAvailable(
libusbhsfs libusbhsfs
libnxtc libnxtc
nvjpg nvjpg
dr_libs
id3v2lib
libusbdvd
) )
set(FTPSRV_LIB_BUILD TRUE) set(FTPSRV_LIB_BUILD TRUE)
@@ -324,6 +356,9 @@ target_include_directories(ftpsrv_helper PUBLIC ${ftpsrv_SOURCE_DIR}/src/platfor
add_library(stb INTERFACE) add_library(stb INTERFACE)
target_include_directories(stb INTERFACE ${stb_SOURCE_DIR}) 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 add_library(libnxtc
${libnxtc_SOURCE_DIR}/source/nxtc.c ${libnxtc_SOURCE_DIR}/source/nxtc.c
${libnxtc_SOURCE_DIR}/source/nxtc_log.c ${libnxtc_SOURCE_DIR}/source/nxtc_log.c
@@ -331,6 +366,28 @@ add_library(libnxtc
) )
target_include_directories(libnxtc PUBLIC ${libnxtc_SOURCE_DIR}/include) 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) if (USE_NVJPG)
add_library(nvjpg add_library(nvjpg
${nvjpg_SOURCE_DIR}/lib/decoder.cpp ${nvjpg_SOURCE_DIR}/lib/decoder.cpp
@@ -357,7 +414,6 @@ endif()
add_library(fatfs add_library(fatfs
source/ff16/diskio.c source/ff16/diskio.c
source/ff16/ff.c source/ff16/ff.c
source/ff16/ffunicode.c
) )
target_include_directories(fatfs PUBLIC source/ff16) target_include_directories(fatfs PUBLIC source/ff16)
@@ -380,6 +436,9 @@ target_link_libraries(sphaira PRIVATE
# libusbhsfs # libusbhsfs
libnxtc libnxtc
fatfs fatfs
dr_libs
id3v2lib
libusbdvd
${minizip_lib} ${minizip_lib}
ZLIB::ZLIB ZLIB::ZLIB

View File

@@ -2,13 +2,13 @@
#include "nanovg.h" #include "nanovg.h"
#include "nanovg/dk_renderer.hpp" #include "nanovg/dk_renderer.hpp"
#include "pulsar.h"
#include "ui/widget.hpp" #include "ui/widget.hpp"
#include "ui/notification.hpp" #include "ui/notification.hpp"
#include "owo.hpp" #include "owo.hpp"
#include "option.hpp" #include "option.hpp"
#include "fs.hpp" #include "fs.hpp"
#include "log.hpp" #include "log.hpp"
#include "utils/audio.hpp"
#ifdef USE_NVJPG #ifdef USE_NVJPG
#include <nvjpg.hpp> #include <nvjpg.hpp>
@@ -22,16 +22,7 @@
namespace sphaira { namespace sphaira {
enum SoundEffect { using SoundEffect = audio::SoundEffect;
SoundEffect_Music,
SoundEffect_Focus,
SoundEffect_Scroll,
SoundEffect_Limit,
SoundEffect_Startup,
SoundEffect_Install,
SoundEffect_Error,
SoundEffect_MAX,
};
enum class LaunchType { enum class LaunchType {
Normal, Normal,
@@ -108,6 +99,10 @@ public:
static auto GetLanguage() -> long; static auto GetLanguage() -> long;
static auto GetTextScrollSpeed() -> long; static auto GetTextScrollSpeed() -> long;
static auto GetNszCompressLevel() -> u8;
static auto GetNszThreadCount() -> u8;
static auto GetNszBlockExponent() -> u8;
static void SetMtpEnable(bool enable); static void SetMtpEnable(bool enable);
static void SetFtpEnable(bool enable); static void SetFtpEnable(bool enable);
static void SetNxlinkEnable(bool enable); static void SetNxlinkEnable(bool enable);
@@ -153,8 +148,12 @@ public:
void LoadTheme(const ThemeMeta& meta); void LoadTheme(const ThemeMeta& meta);
void CloseTheme(); void CloseTheme();
void CloseThemeBackgroundMusic();
void ScanThemes(const std::string& path); void ScanThemes(const std::string& path);
void ScanThemeEntries(); 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. // helper that converts 1.2.3 to a u32 used for comparisons.
static auto GetVersionFromString(const char* str) -> u32; 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_log_enabled{INI_SECTION, "log_enabled", false};
option::OptionBool m_replace_hbmenu{INI_SECTION, "replace_hbmenu", 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::OptionString m_theme_path{INI_SECTION, "theme", DEFAULT_THEME_PATH};
option::OptionBool m_theme_music{INI_SECTION, "theme_music", true}; option::OptionBool m_theme_music{INI_SECTION, "theme_music", true};
option::OptionBool m_12hour_time{INI_SECTION, "12hour_time", false}; 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_append_folder_with_xci{"dump", "append_folder_with_xci", true};
option::OptionBool m_dump_trim_xci{"dump", "trim_xci", false}; 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_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::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 // todo: move this into it's own menu
option::OptionLong m_text_scroll_speed{"accessibility", "text_scroll_speed", 1}; // normal 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 #ifdef USE_NVJPG
nj::Decoder m_decoder; nj::Decoder m_decoder;

View File

@@ -577,6 +577,7 @@ enum class SphairaResult : Result {
UsbDsBadDeviceSpeed, UsbDsBadDeviceSpeed,
NcaBadMagic,
NspBadMagic, NspBadMagic,
XciBadMagic, XciBadMagic,
XciSecurePartitionNotFound, XciSecurePartitionNotFound,
@@ -648,6 +649,17 @@ enum class SphairaResult : Result {
YatiNcmDbCorruptHeader, YatiNcmDbCorruptHeader,
// unable to total infos from ncm database. // unable to total infos from ncm database.
YatiNcmDbCorruptInfos, 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) #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(ThemezerFailedToDownloadTheme),
MAKE_SPHAIRA_RESULT_ENUM(MainFailedToDownloadUpdate), MAKE_SPHAIRA_RESULT_ENUM(MainFailedToDownloadUpdate),
MAKE_SPHAIRA_RESULT_ENUM(UsbDsBadDeviceSpeed), MAKE_SPHAIRA_RESULT_ENUM(UsbDsBadDeviceSpeed),
MAKE_SPHAIRA_RESULT_ENUM(NspBadMagic), MAKE_SPHAIRA_RESULT_ENUM(NspBadMagic),
MAKE_SPHAIRA_RESULT_ENUM(XciBadMagic), MAKE_SPHAIRA_RESULT_ENUM(XciBadMagic),
MAKE_SPHAIRA_RESULT_ENUM(NcaBadMagic),
MAKE_SPHAIRA_RESULT_ENUM(XciSecurePartitionNotFound), MAKE_SPHAIRA_RESULT_ENUM(XciSecurePartitionNotFound),
MAKE_SPHAIRA_RESULT_ENUM(EsBadTitleKeyType), MAKE_SPHAIRA_RESULT_ENUM(EsBadTitleKeyType),
MAKE_SPHAIRA_RESULT_ENUM(EsPersonalisedTicketDeviceIdMissmatch), MAKE_SPHAIRA_RESULT_ENUM(EsPersonalisedTicketDeviceIdMissmatch),
@@ -729,7 +744,9 @@ enum : Result {
MAKE_SPHAIRA_RESULT_ENUM(EsInvalidTicketFromatVersion), MAKE_SPHAIRA_RESULT_ENUM(EsInvalidTicketFromatVersion),
MAKE_SPHAIRA_RESULT_ENUM(EsInvalidTicketKeyType), MAKE_SPHAIRA_RESULT_ENUM(EsInvalidTicketKeyType),
MAKE_SPHAIRA_RESULT_ENUM(EsInvalidTicketKeyRevision), MAKE_SPHAIRA_RESULT_ENUM(EsInvalidTicketKeyRevision),
MAKE_SPHAIRA_RESULT_ENUM(OwoBadArgs), MAKE_SPHAIRA_RESULT_ENUM(OwoBadArgs),
MAKE_SPHAIRA_RESULT_ENUM(UsbCancelled), MAKE_SPHAIRA_RESULT_ENUM(UsbCancelled),
MAKE_SPHAIRA_RESULT_ENUM(UsbBadMagic), MAKE_SPHAIRA_RESULT_ENUM(UsbBadMagic),
MAKE_SPHAIRA_RESULT_ENUM(UsbBadVersion), MAKE_SPHAIRA_RESULT_ENUM(UsbBadVersion),
@@ -744,6 +761,7 @@ enum : Result {
MAKE_SPHAIRA_RESULT_ENUM(UsbUploadBadTransferSize), MAKE_SPHAIRA_RESULT_ENUM(UsbUploadBadTransferSize),
MAKE_SPHAIRA_RESULT_ENUM(UsbUploadBadTotalSize), MAKE_SPHAIRA_RESULT_ENUM(UsbUploadBadTotalSize),
MAKE_SPHAIRA_RESULT_ENUM(UsbUploadBadCommand), MAKE_SPHAIRA_RESULT_ENUM(UsbUploadBadCommand),
MAKE_SPHAIRA_RESULT_ENUM(YatiContainerNotFound), MAKE_SPHAIRA_RESULT_ENUM(YatiContainerNotFound),
MAKE_SPHAIRA_RESULT_ENUM(YatiNcaNotFound), MAKE_SPHAIRA_RESULT_ENUM(YatiNcaNotFound),
MAKE_SPHAIRA_RESULT_ENUM(YatiInvalidNcaReadSize), MAKE_SPHAIRA_RESULT_ENUM(YatiInvalidNcaReadSize),
@@ -765,6 +783,16 @@ enum : Result {
MAKE_SPHAIRA_RESULT_ENUM(YatiCertNotFound), MAKE_SPHAIRA_RESULT_ENUM(YatiCertNotFound),
MAKE_SPHAIRA_RESULT_ENUM(YatiNcmDbCorruptHeader), MAKE_SPHAIRA_RESULT_ENUM(YatiNcmDbCorruptHeader),
MAKE_SPHAIRA_RESULT_ENUM(YatiNcmDbCorruptInfos), 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 #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_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; } }}; // #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. // mutex helpers.
#define SCOPED_MUTEX(mutex) \ #define SCOPED_MUTEX(mutex) \
mutexLock(mutex); \ mutexLock(mutex); \

View File

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

View File

@@ -2,16 +2,21 @@
#include "fs.hpp" #include "fs.hpp"
#include "location.hpp" #include "location.hpp"
#include "ui/progress_box.hpp"
#include <switch.h> #include <switch.h>
#include <vector> #include <vector>
#include <memory> #include <memory>
#include <functional> #include <functional>
#include <minizip/ioapi.h>
namespace sphaira::dump { namespace sphaira::dump {
enum DumpLocationType { enum DumpLocationType {
// dump using native fs. // dump using native fs.
DumpLocationType_SdCard, DumpLocationType_SdCard,
// dump to usb pc.
DumpLocationType_Usb,
// dump to usb using tinfoil protocol. // dump to usb using tinfoil protocol.
DumpLocationType_UsbS2S, DumpLocationType_UsbS2S,
// speed test, only reads the data, doesn't write anything. // speed test, only reads the data, doesn't write anything.
@@ -24,11 +29,12 @@ enum DumpLocationType {
enum DumpLocationFlag { enum DumpLocationFlag {
DumpLocationFlag_SdCard = 1 << DumpLocationType_SdCard, DumpLocationFlag_SdCard = 1 << DumpLocationType_SdCard,
DumpLocationFlag_Usb = 1 << DumpLocationType_Usb,
DumpLocationFlag_UsbS2S = 1 << DumpLocationType_UsbS2S, DumpLocationFlag_UsbS2S = 1 << DumpLocationType_UsbS2S,
DumpLocationFlag_DevNull = 1 << DumpLocationType_DevNull, DumpLocationFlag_DevNull = 1 << DumpLocationType_DevNull,
DumpLocationFlag_Stdio = 1 << DumpLocationType_Stdio, DumpLocationFlag_Stdio = 1 << DumpLocationType_Stdio,
DumpLocationFlag_Network = 1 << DumpLocationType_Network, 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 { struct DumpEntry {
@@ -48,17 +54,36 @@ struct BaseSource {
virtual auto GetName(const std::string& path) const -> std::string = 0; virtual auto GetName(const std::string& path) const -> std::string = 0;
virtual auto GetSize(const std::string& path) const -> s64 = 0; virtual auto GetSize(const std::string& path) const -> s64 = 0;
virtual auto GetIcon(const std::string& path) const -> int { return 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. // called after dump has finished.
using OnExit = std::function<void(Result rc)>; using OnExit = std::function<void(Result rc)>;
using OnLocation = std::function<void(const DumpLocation& loc)>; 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. // 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(). // 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. // 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 } // namespace sphaira::dump

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -15,7 +15,10 @@ enum class Mode {
SingleThreadedIfSmaller, 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 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)>; using WriteCallback = std::function<Result(const void* data, s64 off, s64 size)>;
// used for pull api // used for pull api
@@ -33,6 +36,7 @@ using StartCallback2 = std::function<Result(StartThreadCallback start, PullCallb
// reads data from rfunc into wfunc. // 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 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. // 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); 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 { namespace sphaira::ui::menu::fileview {
struct Menu final : MenuBase { 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"; }; auto GetShortTitle() const -> const char* override { return "File"; };
void Update(Controller* controller, TouchInfo* touch) override; void Update(Controller* controller, TouchInfo* touch) override;
@@ -15,8 +15,8 @@ struct Menu final : MenuBase {
void OnFocusGained() override; void OnFocusGained() override;
private: private:
fs::Fs* const m_fs;
const fs::FsPath m_path; const fs::FsPath m_path;
fs::FsNativeSd m_fs{};
fs::File m_file{}; fs::File m_file{};
s64 m_file_size{}; s64 m_file_size{};
s64 m_file_offset{}; 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_extension{}; // did we already search for an ext?
bool checked_internal_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 selected{}; // is this file selected?
bool done_stat{}; // have we checked file_size / count.
auto IsFile() const -> bool { auto IsFile() const -> bool {
return type == FsDirEntryType_File; return type == FsDirEntryType_File;
@@ -305,9 +306,11 @@ struct FsView final : Widget {
void DisplayOptions(); void DisplayOptions();
void DisplayAdvancedOptions(); void DisplayAdvancedOptions();
void MountNspFs(); using MountFsFunc = Result(*)(fs::Fs *fs, const fs::FsPath &path, fs::FsPath &out_path);
void MountXciFs(); // using MountFsFunc = std::function<Result(fs::Fs *fs, const fs::FsPath &path, fs::FsPath &out_path)>;
void MountZipFs(); using UmountFsFunc = std::function<void(const fs::FsPath &mount)>;
void MountFileFs(const MountFsFunc& mount_func, const UmountFsFunc& umount_func);
// private: // private:
Base* m_menu{}; Base* m_menu{};
@@ -437,6 +440,9 @@ protected:
auto CreateFs(const FsEntry& fs_entry) -> std::shared_ptr<fs::Fs>; 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: protected:
static constexpr inline const char* INI_SECTION = "filebrowser"; 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 ext1, std::string_view ext2) -> bool;
auto IsExtension(std::string_view ext, std::span<const std::string_view> list) -> bool; auto IsExtension(std::string_view ext, std::span<const std::string_view> list) -> bool;
struct FsStdioWrapper final : fs::FsStdio { struct FsStdioWrapper final : fs::FsStdio {
using OnExit = std::function<void(void)>; using OnExit = std::function<void(void)>;
FsStdioWrapper(const fs::FsPath& root, const OnExit& on_exit) : fs::FsStdio{true, root}, m_on_exit{on_exit} { 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 { struct Entry {
u64 app_id{}; u64 app_id{};
u8 type{}; u8 last_event{};
NacpLanguageEntry lang{}; NacpLanguageEntry lang{};
int image{}; int image{};
bool selected{}; bool selected{};
@@ -84,7 +84,8 @@ private:
} }
void DeleteGames(); void DeleteGames();
void DumpGames(u32 flags); void ExportOptions(bool to_nsz);
void DumpGames(u32 flags, bool to_nsz);
void CreateSaves(AccountUid uid); void CreateSaves(AccountUid uid);
private: private:
@@ -98,6 +99,10 @@ private:
bool m_is_reversed{}; bool m_is_reversed{};
bool m_dirty{}; 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_sort{INI_SECTION, "sort", SortType::SortType_Updated};
option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Descending}; option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Descending};
option::OptionLong m_layout{INI_SECTION, "layout", LayoutType::LayoutType_Grid}; option::OptionLong m_layout{INI_SECTION, "layout", LayoutType::LayoutType_Grid};
@@ -160,13 +165,13 @@ struct ContentInfoEntry {
std::vector<NcmRightsId> ncm_rights_id{}; std::vector<NcmRightsId> ncm_rights_id{};
}; };
auto BuildNspPath(const Entry& e, const NsApplicationContentMetaStatus& status) -> fs::FsPath; auto BuildNspPath(const Entry& e, const NsApplicationContentMetaStatus& status, bool to_nsz = false) -> fs::FsPath;
Result BuildContentEntry(const NsApplicationContentMetaStatus& status, ContentInfoEntry& out); 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); 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); 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); Result BuildNspEntries(Entry& e, u32 flags, std::vector<NspEntry>& out, bool to_nsz = false);
// dumps the array of nsp entries. // 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 } // namespace sphaira::ui::menu::game

View File

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

View File

@@ -7,7 +7,8 @@
#include <span> #include <span>
#include <memory> #include <memory>
namespace sphaira::ui::menu::gc { // todo: pr to libnx
extern "C" {
typedef enum { typedef enum {
FsGameCardPartitionRaw_None = -1, FsGameCardPartitionRaw_None = -1,
@@ -15,6 +16,13 @@ typedef enum {
FsGameCardPartitionRaw_Secure = 1, FsGameCardPartitionRaw_Secure = 1,
} FsGameCardPartitionRaw; } 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./ // The below structs are taken from nxdumptool./
//////////////////////////////////////////////// ////////////////////////////////////////////////
@@ -88,6 +96,33 @@ typedef struct {
static_assert(sizeof(GameCardInitialData) == 0x200); 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 { typedef struct {
u8 maker_code; ///< GameCardUidMakerCode. u8 maker_code; ///< GameCardUidMakerCode.
u8 version; ///< TODO: determine whether this matches GameCardVersion or not. u8 version; ///< TODO: determine whether this matches GameCardVersion or not.
@@ -198,6 +233,7 @@ private:
void FreeImage(); void FreeImage();
void OnChangeIndex(s64 new_index); void OnChangeIndex(s64 new_index);
Result DumpGames(u32 flags); Result DumpGames(u32 flags);
Result DumpXcz(u32 flags);
Result MountGcFs(); Result MountGcFs();
@@ -222,12 +258,12 @@ private:
FsStorage m_storage{}; FsStorage m_storage{};
// size of normal partition. // size of normal partition.
s64 m_parition_normal_size{}; s64 m_partition_normal_size{};
// size of secure partition. // size of secure partition.
s64 m_parition_secure_size{}; s64 m_partition_secure_size{};
// used size reported in the xci header. // used size reported in the xci header.
s64 m_storage_trimmed_size{}; 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{}; s64 m_storage_total_size{};
// reported size via rom_size in the xci header. // reported size via rom_size in the xci header.
s64 m_storage_full_size{}; s64 m_storage_full_size{};

View File

@@ -71,7 +71,7 @@ private:
return m_sort.Get() >= SortType_UpdatedStar; return m_sort.Get() >= SortType_UpdatedStar;
} }
Result MountRomfsFs(); Result MountNroFs();
private: private:
static constexpr inline const char* INI_SECTION = "homebrew"; 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; auto BuildSavePath(const Entry& e, bool is_auto) const -> fs::FsPath;
Result RestoreSaveInternal(ProgressBox* pbox, const Entry& e, const fs::FsPath& path) const; 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(); Result MountSaveFs();

View File

@@ -1,3 +1,4 @@
#if 0
#pragma once #pragma once
#include "ui/menus/menu_base.hpp" #include "ui/menus/menu_base.hpp"
@@ -175,3 +176,4 @@ private:
}; };
} // namespace sphaira::ui::menu::themezer } // 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, int image,
const std::string& action, const std::string& action,
const std::string& title, const std::string& title,
const ProgressBoxCallback& callback, const ProgressBoxDoneCallback& done = [](Result rc){}, const ProgressBoxCallback& callback, const ProgressBoxDoneCallback& done = nullptr
int cpuid = 1, int prio = PRIO_PREEMPTIVE, int stack_size = 1024*128
); );
~ProgressBox(); ~ProgressBox();
@@ -28,6 +27,8 @@ struct ProgressBox final : Widget {
auto SetActionName(const std::string& action) -> ProgressBox&; auto SetActionName(const std::string& action) -> ProgressBox&;
auto SetTitle(const std::string& title) -> ProgressBox&; auto SetTitle(const std::string& title) -> ProgressBox&;
auto NewTransfer(const std::string& transfer) -> ProgressBox&; auto NewTransfer(const std::string& transfer) -> ProgressBox&;
// zeros the saved offset.
auto ResetTranfser() -> ProgressBox&;
auto UpdateTransfer(s64 offset, s64 size) -> ProgressBox&; auto UpdateTransfer(s64 offset, s64 size) -> ProgressBox&;
// not const in order to avoid copy by using std::swap // not const in order to avoid copy by using std::swap
auto SetImage(int image) -> ProgressBox&; 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; auto CopyFile(const fs::FsPath& src, const fs::FsPath& dst, bool single_threaded = false) -> Result;
void Yield(); void Yield();
auto GetCpuId() const {
return m_cpuid;
}
auto OnDownloadProgressCallback() { auto OnDownloadProgressCallback() {
return [this](s64 dltotal, s64 dlnow, s64 ultotal, s64 ulnow){ return [this](s64 dltotal, s64 dlnow, s64 ultotal, s64 ulnow){
if (this->ShouldExit()) { if (this->ShouldExit()) {
@@ -103,7 +100,6 @@ private:
ScrollingText m_scroll_title{}; ScrollingText m_scroll_title{};
ScrollingText m_scroll_transfer{}; ScrollingText m_scroll_transfer{};
int m_cpuid{};
int m_image{}; int m_image{};
bool m_own_image{}; bool m_own_image{};
}; };

View File

@@ -91,6 +91,25 @@ private:
std::string m_false_str; 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 { class SidebarEntryCallback final : public SidebarEntryBase {
public: public:
using Callback = std::function<void()>; using Callback = std::function<void()>;

View File

@@ -1,7 +1,6 @@
#pragma once #pragma once
#include "nanovg.h" #include "nanovg.h"
#include "pulsar.h"
#include "fs.hpp" #include "fs.hpp"
#include <switch.h> #include <switch.h>
@@ -229,6 +228,7 @@ struct ThemeMeta {
struct Theme { struct Theme {
ThemeMeta meta; ThemeMeta meta;
ElementEntry elements[ThemeEntryID_MAX]; ElementEntry elements[ThemeEntryID_MAX];
fs::FsPath music_path;
auto GetColour(ThemeEntryID id) const { auto GetColour(ThemeEntryID id) const {
return elements[id].colour; return elements[id].colour;
@@ -291,11 +291,13 @@ enum class Button : u64 {
LS_RIGHT = static_cast<u64>(HidNpadButton_StickLRight), LS_RIGHT = static_cast<u64>(HidNpadButton_StickLRight),
LS_UP = static_cast<u64>(HidNpadButton_StickLUp), LS_UP = static_cast<u64>(HidNpadButton_StickLUp),
LS_DOWN = static_cast<u64>(HidNpadButton_StickLDown), 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_LEFT = static_cast<u64>(HidNpadButton_StickRLeft),
RS_RIGHT = static_cast<u64>(HidNpadButton_StickRRight), RS_RIGHT = static_cast<u64>(HidNpadButton_StickRRight),
RS_UP = static_cast<u64>(HidNpadButton_StickRUp), RS_UP = static_cast<u64>(HidNpadButton_StickRUp),
RS_DOWN = static_cast<u64>(HidNpadButton_StickRDown), 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_LEFT = static_cast<u64>(HidNpadButton_AnyLeft),
ANY_RIGHT = static_cast<u64>(HidNpadButton_AnyRight), 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); Usb(u64 transfer_timeout);
virtual ~Usb(); 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) { Result IsUsbConnected(u64 timeout) {
return m_usb->IsUsbConnected(timeout); return m_usb->IsUsbConnected(timeout);
} }
// waits for connection and then sends file list. // 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. // polls for command, executes transfer if possible.
// will return Result_Exit if exit command is recieved. // will return Result_Exit if exit command is recieved.
Result PollCommands(); Result PollCommands();
private: Result file_transfer_loop();
Result FileRangeCmd(u64 data_size);
private: private:
std::unique_ptr<usb::UsbHs> m_usb; Result SendResult(u32 result, u32 arg3 = 0, u32 arg4 = 0);
std::vector<u8> m_buf;
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 } // namespace sphaira::usb::upload

View File

@@ -2,11 +2,6 @@
#include "base.hpp" #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 GetUsbDsStateStr(UsbState state) -> const char*;
auto GetUsbDsSpeedStr(UsbDeviceSpeed speed) -> 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 #pragma once
#include <switch.h>
#include "fs.hpp" #include "fs.hpp"
#include "yati/source/base.hpp"
#include <switch.h>
#include <memory>
namespace sphaira::devoptab { namespace sphaira::devoptab {
// mounts to "lower_case_hex_id:/" // 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); void UnmountSave(u64 id);
// todo:
Result MountZip(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path); Result MountZip(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path);
void UmountZip(const fs::FsPath& mount); 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); void UmountNsp(const fs::FsPath& mount);
Result MountXci(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path); 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); 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 } // namespace sphaira::devoptab

View File

@@ -1,35 +1,84 @@
#pragma once #pragma once
#include "yati/source/file.hpp" #include "yati/source/file.hpp"
#include "utils/lru.hpp"
#include <memory> #include <memory>
#include <span>
namespace sphaira::devoptab::common { namespace sphaira::devoptab::common {
// max entries per devoptab, should be enough. // max entries per devoptab, should be enough.
enum { MAX_ENTRIES = 4 }; enum { MAX_ENTRIES = 4 };
// buffers data in 512k chunks to maximise throughput. struct BufferedDataBase : yati::source::Base {
// not suitable if random access >= 512k is common. BufferedDataBase(const std::shared_ptr<yati::source::Base>& _source, u64 _size)
// if that is needed, see the LRU cache varient used for fatfs. : source{_source}
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)}
, capacity{_size} { , capacity{_size} {
} }
Result Read(void *buf, s64 off, s64 size); virtual Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override {
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: private:
std::unique_ptr<yati::source::Base> source;
const u64 capacity;
u64 m_off{}; u64 m_off{};
u64 m_size{}; 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); 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 { namespace sphaira::utils {
struct ScopedTimestampProfile { struct ScopedTimestampProfile final {
ScopedTimestampProfile(const std::string& name) : m_name{name} { ScopedTimestampProfile(const std::string& name) : m_name{name} {
} }
~ScopedTimestampProfile() { ~ScopedTimestampProfile() {
Log();
}
void Log() {
log_write("\t[%s] time taken: %.2fs %.2fms\n", m_name.c_str(), m_ts.GetSecondsD(), m_ts.GetMsD()); 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 { struct Nsp final : Base {
using Base::Base; using Base::Base;
Result GetCollections(Collections& out) override; Result GetCollections(Collections& out) override;
Result GetCollections(Collections& out, s64 off);
// builds nsp meta data and the size of the entier nsp. // 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 } // namespace sphaira::yati::container

View File

@@ -9,18 +9,62 @@ namespace sphaira::yati::container {
struct Xci final : Base { 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 { struct Partition {
// name of the partition. // name of the partition.
std::string name; std::string name;
// offset of this hfs0.
s64 hfs0_offset;
s64 hfs0_size;
Hfs0 hfs0;
// all the collections for this partition, may be empty. // all the collections for this partition, may be empty.
Collections collections; Collections collections;
}; };
struct Root {
// offset of this hfs0.
s64 hfs0_offset;
Hfs0 hfs0;
std::vector<Partition> partitions;
};
using Partitions = std::vector<Partition>; using Partitions = std::vector<Partition>;
using Base::Base; using Base::Base;
Result GetCollections(Collections& out) override; 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 } // 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); 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 } // 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(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 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. // fills out with the list of common / personalised rights ids.
Result GetCommonTickets(std::vector<FsRightsId>& out); Result GetCommonTickets(std::vector<FsRightsId>& out);
@@ -218,6 +219,11 @@ bool IsRightsIdFound(const FsRightsId& id, std::span<const FsRightsId> ids);
// wrapper around ipc. // wrapper around ipc.
Result GetCommonTicketAndCertificate(const FsRightsId& rights_id, std::vector<u8>& tik_out, std::vector<u8>& cert_out); Result GetCommonTicketAndCertificate(const FsRightsId& rights_id, std::vector<u8>& tik_out, std::vector<u8>& cert_out);
// fetches data from system es save. // 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); 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 } // namespace sphaira::es

View File

@@ -3,8 +3,11 @@
#include "fs.hpp" #include "fs.hpp"
#include "keys.hpp" #include "keys.hpp"
#include "ncm.hpp" #include "ncm.hpp"
#include "yati/source/base.hpp"
#include <switch.h> #include <switch.h>
#include <vector> #include <vector>
#include <memory>
namespace sphaira::nca { namespace sphaira::nca {
@@ -15,7 +18,7 @@ namespace sphaira::nca {
#define NCA_SECTOR_SIZE 0x200 #define NCA_SECTOR_SIZE 0x200
#define NCA_XTS_SECTION_SIZE 0xC00 #define NCA_XTS_SECTION_SIZE 0xC00
#define NCA_SECTION_TOTAL 0x4 #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_PROGRAM_LOGO_OFFSET 0x8000
#define NCA_META_CNMT_OFFSET 0xC20 #define NCA_META_CNMT_OFFSET 0xC20
@@ -94,6 +97,22 @@ struct SectionTableEntry {
u32 media_end_offset; // divided by 0x200. u32 media_end_offset; // divided by 0x200.
u8 _0x8[0x4]; // unknown. u8 _0x8[0x4]; // unknown.
u8 _0xC[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 { struct LayerRegion {
@@ -139,6 +158,55 @@ static_assert(sizeof(HierarchicalSha256Data) == 0xF8);
static_assert(sizeof(IntegrityMetaInfo) == 0xF8); static_assert(sizeof(IntegrityMetaInfo) == 0xF8);
static_assert(sizeof(HierarchicalSha256Data) == sizeof(IntegrityMetaInfo)); 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 { struct FsHeader {
u16 version; // always 2. u16 version; // always 2.
u8 fs_type; // see FileSystemType. u8 fs_type; // see FileSystemType.
@@ -152,12 +220,16 @@ struct FsHeader {
IntegrityMetaInfo integrity_meta_info; // used for romfs IntegrityMetaInfo integrity_meta_info; // used for romfs
} hash_data; } hash_data;
u8 patch_info[0x40]; PatchInfo patch_info;
u64 section_ctr; u64 section_ctr;
u8 spares_info[0x30]; u8 spares_info[0x30];
u8 compression_info[0x28]; CompressionInfo compression_info;
u8 meta_data_hash_data_info[0x30]; u8 meta_data_hash_data_info[0x30];
u8 reserved[0x30]; u8 reserved[0x30];
auto IsValid() const -> bool {
return version == 2;
}
}; };
static_assert(sizeof(FsHeader) == 0x200); static_assert(sizeof(FsHeader) == 0x200);
static_assert(sizeof(FsHeader::hash_data) == 0xF8); static_assert(sizeof(FsHeader::hash_data) == 0xF8);
@@ -203,6 +275,10 @@ struct Header {
FsHeader fs_header[NCA_SECTION_TOTAL]; FsHeader fs_header[NCA_SECTION_TOTAL];
auto IsValid() const -> bool {
return magic == NCA3_MAGIC;
}
auto GetKeyGeneration() const -> u8 { auto GetKeyGeneration() const -> u8 {
if (old_key_gen < key_gen) { if (old_key_gen < key_gen) {
return key_gen; return key_gen;
@@ -220,12 +296,25 @@ struct Header {
key_gen = key_generation; 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); static_assert(sizeof(Header) == 0xC00);
auto GetContentTypeStr(u8 content_type) -> const char*; auto GetContentTypeStr(u8 content_type) -> const char*;
auto GetDistributionTypeStr(u8 distribution_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 DecryptKeak(const keys::Keys& keys, Header& header);
Result EncryptKeak(const keys::Keys& keys, Header& header, u8 key_generation); Result EncryptKeak(const keys::Keys& keys, Header& header, u8 key_generation);
Result VerifyFixedKey(const Header& header); 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*; 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 } // namespace sphaira::nca

View File

@@ -1,6 +1,7 @@
#pragma once #pragma once
#include "fs.hpp" #include "fs.hpp"
#include "yati/source/base.hpp"
#include <switch.h> #include <switch.h>
#include <vector> #include <vector>
@@ -82,4 +83,16 @@ static constexpr inline bool HasRequiredSystemVersion(const NcmContentMetaKey *k
// fills program id and out path of the control nca. // 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); 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 } // namespace sphaira::ncm

View File

@@ -1,6 +1,13 @@
#pragma once #pragma once
#include "yati/source/base.hpp"
#include "utils/lru.hpp"
#include "defines.hpp"
#include <switch.h> #include <switch.h>
#include <vector>
#include <memory>
#include <zstd.h>
namespace sphaira::ncz { namespace sphaira::ncz {
@@ -8,7 +15,11 @@ namespace sphaira::ncz {
// todo: byteswap this // todo: byteswap this
#define NCZ_BLOCK_MAGIC std::byteswap(0x4E435A424C4F434BUL) #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 { struct Header {
u64 magic; // NCZ_SECTION_MAGIC u64 magic; // NCZ_SECTION_MAGIC
@@ -23,11 +34,21 @@ struct BlockHeader {
u8 block_size_exponent; u8 block_size_exponent;
u32 total_blocks; u32 total_blocks;
u64 decompressed_size; 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 { struct Block {
u32 size; u32 size;
}; };
using Blocks = std::vector<Block>;
struct BlockInfo { struct BlockInfo {
u64 offset; // compressed offset. u64 offset; // compressed offset.
@@ -50,5 +71,39 @@ struct Section {
return off < offset + size && off >= offset; 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 } // 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;
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 { virtual bool IsStream() const {
return false; return false;
} }

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -7,6 +7,7 @@
#include "i18n.hpp" #include "i18n.hpp"
#include "location.hpp" #include "location.hpp"
#include "threaded_file_transfer.hpp" #include "threaded_file_transfer.hpp"
#include "haze_helper.hpp"
#include "ui/sidebar.hpp" #include "ui/sidebar.hpp"
#include "ui/error_box.hpp" #include "ui/error_box.hpp"
@@ -18,29 +19,179 @@
#include "yati/source/stream.hpp" #include "yati/source/stream.hpp"
#include "usb/usb_uploader.hpp" #include "usb/usb_uploader.hpp"
#include "usb/tinfoil.hpp" #include "usb/usb_dumper.hpp"
#include "usb/usbds.hpp"
namespace sphaira::dump { namespace sphaira::dump {
namespace { 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 { struct DumpLocationEntry {
const DumpLocationType type; const DumpLocationType type;
const char* name; 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[]{ constexpr DumpLocationEntry DUMP_LOCATIONS[]{
{ DumpLocationType_SdCard, "microSD card (/dumps/)" }, { 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)" }, { DumpLocationType_UsbS2S, "USB transfer (Switch 2 Switch)" },
#endif
{ DumpLocationType_DevNull, "/dev/null (Speed Test)" }, { DumpLocationType_DevNull, "/dev/null (Speed Test)" },
}; };
#if ENABLE_NETWORK_INSTALL
struct UsbTest final : usb::upload::Usb, yati::source::Stream { struct UsbTest final : usb::upload::Usb, yati::source::Stream {
UsbTest(ui::ProgressBox* pbox, BaseSource* source) : Usb{UINT64_MAX} { UsbTest(ui::ProgressBox* pbox, BaseSource* source, std::span<const fs::FsPath> paths)
m_pbox = pbox; : Usb{UINT64_MAX}
m_source = source; , 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 { 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(); 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) { if (m_pull) {
return Stream::Read(buf, off, size, bytes_read); return Stream::Read(buf, off, size, bytes_read);
} else { } 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) { Result Open(u32 index, s64& out_size, u16& out_flags) override {
if (m_path != path) { const auto path = m_paths[index];
m_path = path; const auto size = m_source->GetSize(path);
m_progress = 0; m_progress = 0;
m_pull_offset = 0; m_pull_offset = 0;
Stream::Reset(); Stream::Reset();
m_size = m_source->GetSize(path); m_size = size;
m_pbox->SetImage(m_source->GetIcon(path)); m_pbox->SetImage(m_source->GetIcon(path));
m_pbox->SetTitle(m_source->GetName(path)); m_pbox->SetTitle(m_source->GetName(path));
m_pbox->NewTransfer(m_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)); R_TRY(m_source->Read(path, buf, off, size, bytes_read));
m_offset += *bytes_read; m_offset += *bytes_read;
@@ -93,16 +251,55 @@ struct UsbTest final : usb::upload::Usb, yati::source::Stream {
private: private:
ui::ProgressBox* m_pbox{}; ui::ProgressBox* m_pbox{};
BaseSource* m_source{}; BaseSource* m_source{};
std::string m_path{}; std::span<const fs::FsPath> m_paths{};
thread::PullCallback m_pull{}; thread::PullCallback m_pull{};
s64 m_offset{}; s64 m_offset{};
s64 m_size{}; s64 m_size{};
s64 m_progress{}; s64 m_progress{};
s64 m_pull_offset{}; 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(); const auto is_file_based_emummc = App::IsFileBaseEmummc();
for (const auto& path : paths) { for (const auto& path : paths) {
@@ -122,13 +319,17 @@ Result DumpToFile(ui::ProgressBox* pbox, fs::Fs* fs, const fs::FsPath& root, Bas
{ {
fs::File file; fs::File file;
R_TRY(fs->OpenFile(temp_path, FsOpenMode_Write, &file)); R_TRY(fs->OpenFile(temp_path, FsOpenMode_Write, &file));
auto write_source = std::make_unique<WriteFileSource>(&file);
if (custom_transfer) {
R_TRY(custom_transfer(pbox, source, write_source.get(), path));
} else {
R_TRY(thread::Transfer(pbox, file_size, R_TRY(thread::Transfer(pbox, file_size,
[&](void* data, s64 off, s64 size, u64* bytes_read) -> Result { [&](void* data, s64 off, s64 size, u64* bytes_read) -> Result {
return source->Read(path, data, off, size, bytes_read); return source->Read(path, data, off, size, bytes_read);
}, },
[&](const void* data, s64 off, s64 size) -> Result { [&](const void* data, s64 off, s64 size) -> Result {
const auto rc = file.Write(off, data, size, FsWriteOption_None); const auto rc = write_source->Write(data, off, size);
if (is_file_based_emummc) { if (is_file_based_emummc) {
svcSleepThread(2e+6); // 2ms svcSleepThread(2e+6); // 2ms
} }
@@ -136,6 +337,7 @@ Result DumpToFile(ui::ProgressBox* pbox, fs::Fs* fs, const fs::FsPath& root, Bas
} }
)); ));
} }
}
fs->DeleteFile(base_path); fs->DeleteFile(base_path);
R_TRY(fs->RenameFile(temp_path, base_path)); R_TRY(fs->RenameFile(temp_path, base_path));
@@ -144,53 +346,50 @@ Result DumpToFile(ui::ProgressBox* pbox, fs::Fs* fs, const fs::FsPath& root, Bas
R_SUCCEED(); 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{}; 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{}; 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 DumpToUsbS2SInternal(ui::ProgressBox* pbox, UsbTest* usb) {
Result DumpToUsbS2SStream(ui::ProgressBox* pbox, UsbTest* usb, std::span<const fs::FsPath> paths) {
auto source = usb->GetSource(); 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); const auto file_size = source->GetSize(path);
R_TRY(thread::TransferPull(pbox, file_size, R_TRY(thread::TransferPull(pbox, file_size,
[&](void* data, s64 off, s64 size, u64* bytes_read) -> Result { [&](void* data, s64 off, s64 size, u64* bytes_read) -> Result {
return usb->ReadInternal(path, data, off, size, bytes_read); 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); usb->SetPullCallback(pull);
R_TRY(start()); R_TRY(start());
while (!pbox->ShouldExit()) { while (!pbox->ShouldExit()) {
R_TRY(usb->PollCommands()); const auto rc = usb->file_transfer_loop();
if (R_FAILED(rc)) {
if (usb->GetPullOffset() >= file_size) { if (rc == Result_UsbUploadExit) {
R_SUCCEED(); break;
} else {
R_THROW(rc);
}
} }
} }
R_THROW(0xFFFF); return pbox->ShouldExitResult();
} }
)); ));
} }
R_SUCCEED(); return pbox->ShouldExitResult();
}
Result DumpToUsbS2SRandom(ui::ProgressBox* pbox, UsbTest* usb) {
while (!pbox->ShouldExit()) {
R_TRY(usb->PollCommands());
}
R_THROW(0xFFFF);
} }
Result DumpToUsbS2S(ui::ProgressBox* pbox, BaseSource* source, std::span<const fs::FsPath> paths) { 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); file_list.emplace_back(path);
} }
// auto usb = std::make_unique<UsbTest>(pbox, entries); auto usb = std::make_unique<UsbTest>(pbox, source, paths);
auto usb = std::make_unique<UsbTest>(pbox, source); constexpr u64 timeout = 3e+9;
constexpr u64 timeout = 1e+9;
while (!pbox->ShouldExit()) { while (!pbox->ShouldExit()) {
if (R_SUCCEEDED(usb->IsUsbConnected(timeout))) { if (R_SUCCEEDED(usb->IsUsbConnected(timeout))) {
pbox->NewTransfer("USB connected, sending file list"_i18n); pbox->NewTransfer("USB connected, sending file list"_i18n);
u8 flags = usb::tinfoil::USBFlag_NONE; if (R_SUCCEEDED(usb->WaitForConnection(timeout, file_list))) {
if (App::GetApp()->m_dump_usb_transfer_stream.Get()) {
flags |= usb::tinfoil::USBFlag_STREAM;
}
if (R_SUCCEEDED(usb->WaitForConnection(timeout, flags, file_list))) {
pbox->NewTransfer("Sent file list, waiting for command..."_i18n); pbox->NewTransfer("Sent file list, waiting for command..."_i18n);
Result rc; Result rc = DumpToUsbS2SInternal(pbox, usb.get());
if (flags & usb::tinfoil::USBFlag_STREAM) {
rc = DumpToUsbS2SStream(pbox, usb.get(), paths);
} else {
rc = DumpToUsbS2SRandom(pbox, usb.get());
}
// wait for exit command. // wait for exit command.
if (R_SUCCEEDED(rc)) { 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) { for (auto path : paths) {
R_TRY(pbox->ShouldExitResult()); R_TRY(pbox->ShouldExitResult());
@@ -255,15 +442,21 @@ Result DumpToDevNull(ui::ProgressBox* pbox, BaseSource* source, std::span<const
pbox->SetTitle(source->GetName(path)); pbox->SetTitle(source->GetName(path));
pbox->NewTransfer(path); pbox->NewTransfer(path);
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, R_TRY(thread::Transfer(pbox, file_size,
[&](void* data, s64 off, s64 size, u64* bytes_read) -> Result { [&](void* data, s64 off, s64 size, u64* bytes_read) -> Result {
return source->Read(path, data, off, size, bytes_read); return source->Read(path, data, off, size, bytes_read);
}, },
[&](const void* data, s64 off, s64 size) -> Result { [&](const void* data, s64 off, s64 size) -> Result {
R_SUCCEED(); return write_source->Write(data, off, size);
} }
)); ));
} }
}
R_SUCCEED(); R_SUCCEED();
} }
@@ -318,13 +511,13 @@ Result DumpToNetwork(ui::ProgressBox* pbox, const location::Entry& loc, BaseSour
} // namespace } // 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; DumpLocation out;
ui::PopupList::Items items; ui::PopupList::Items items;
std::vector<DumpEntry> dump_entries; std::vector<DumpEntry> dump_entries;
out.network = location::Load(); 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++) { for (s32 i = 0; i < std::size(out.network); i++) {
dump_entries.emplace_back(DumpLocationType_Network, i); dump_entries.emplace_back(DumpLocationType_Network, i);
items.emplace_back(out.network[i].name); 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++) { for (s32 i = 0; i < std::size(DUMP_LOCATIONS); i++) {
if (location_flags & (1 << DUMP_LOCATIONS[i].type)) { if (location_flags & (1 << DUMP_LOCATIONS[i].type)) {
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); dump_entries.emplace_back(DUMP_LOCATIONS[i].type, i);
items.emplace_back(i18n::get(DUMP_LOCATIONS[i].name)); items.emplace_back(i18n::get(DUMP_LOCATIONS[i].name));
} }
} }
}
App::Push<ui::PopupList>( App::Push<ui::PopupList>(
title, items, [dump_entries, out, on_loc](auto op_index) mutable { title, items, [dump_entries, out, on_loc](auto op_index) mutable {
out.entry = dump_entries[*op_index]; out.entry = dump_entries[*op_index];
log_write("got entry: %u index: %zu\n", out.entry.type, *op_index);
on_loc(out); on_loc(out);
} }
); );
} }
void Dump(const std::shared_ptr<BaseSource>& source, const DumpLocation& location, const std::vector<fs::FsPath>& paths, const OnExit& on_exit) { Result Dump(ui::ProgressBox* pbox, const std::shared_ptr<BaseSource>& source, const DumpLocation& location, const std::vector<fs::FsPath>& paths, const CustomTransfer& custom_transfer) {
App::Push<ui::ProgressBox>(0, "Exporting"_i18n, "", [source, paths, location](auto pbox) -> Result {
if (location.entry.type == DumpLocationType_Network) { if (location.entry.type == DumpLocationType_Network) {
R_TRY(DumpToNetwork(pbox, location.network[location.entry.index], source.get(), paths)); R_TRY(DumpToNetwork(pbox, location.network[location.entry.index], source.get(), paths));
} else if (location.entry.type == DumpLocationType_Stdio) { } else if (location.entry.type == DumpLocationType_Stdio) {
R_TRY(DumpToStdio(pbox, location.stdio[location.entry.index], source.get(), paths)); R_TRY(DumpToStdio(pbox, location.stdio[location.entry.index], source.get(), paths, custom_transfer));
} else if (location.entry.type == DumpLocationType_SdCard) { } else if (location.entry.type == DumpLocationType_SdCard) {
R_TRY(DumpToFileNative(pbox, source.get(), paths)); 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) { } else if (location.entry.type == DumpLocationType_UsbS2S) {
#if ENABLE_NETWORK_INSTALL
R_TRY(DumpToUsbS2S(pbox, source.get(), paths)); R_TRY(DumpToUsbS2S(pbox, source.get(), paths));
#endif
} else if (location.entry.type == DumpLocationType_DevNull) { } else if (location.entry.type == DumpLocationType_DevNull) {
R_TRY(DumpToDevNull(pbox, source.get(), paths)); 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){ }, [on_exit](Result rc){
App::PushErrorBox(rc, "Export failed!"_i18n); 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"); log_write("dump successfull!!!\n");
} }
if (on_exit) {
on_exit(rc); 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 } // namespace sphaira::dump

View File

@@ -1,3 +1,5 @@
#include "utils/devoptab.hpp"
#include "utils/devoptab_common.hpp"
#include "fatfs.hpp" #include "fatfs.hpp"
#include "defines.hpp" #include "defines.hpp"
#include "log.hpp" #include "log.hpp"
@@ -6,6 +8,7 @@
#include <array> #include <array>
#include <algorithm> #include <algorithm>
#include <span> #include <span>
#include <memory>
#include <cstring> #include <cstring>
#include <cstdio> #include <cstdio>
@@ -15,98 +18,77 @@
namespace sphaira::fatfs { namespace sphaira::fatfs {
namespace { 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 // todo: replace with off+size and have the data be in another struct
// in order to be more lcache efficient. // in order to be more lcache efficient.
struct BufferedFileData { struct FsStorageSource final : yati::source::Base {
u8* data{}; FsStorageSource(FsStorage* s) : m_s{*s} {
u64 off{};
u64 size{};
~BufferedFileData() {
if (data) {
free(data);
}
} }
void Allocate(u64 new_size) { Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override {
data = (u8*)realloc(data, new_size * sizeof(*data)); R_TRY(fsStorageRead(&m_s, off, buf, size));
off = 0; *bytes_read = size;
size = 0; R_SUCCEED();
}
};
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 GetSize(s64* size) {
return fsStorageGetSize(&m_s, size);
} }
// 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: private:
ListEntry* list_head{}; FsStorage m_s;
ListEntry* list_tail{};
std::vector<ListEntry> list_flat_array{};
}; };
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 { enum BisMountType {
BisMountType_PRODINFOF, BisMountType_PRODINFOF,
@@ -117,10 +99,7 @@ enum BisMountType {
struct FatStorageEntry { struct FatStorageEntry {
FsStorage storage; FsStorage storage;
s64 storage_size; std::unique_ptr<devoptab::common::LruBufferedData> buffered;
LruBufferedData lru_cache[2];
BufferedFileData buffered_small[1024]; // 1MiB (usually).
BufferedFileData buffered_large[2]; // 1MiB
FATFS fs; FATFS fs;
devoptab_t devoptab; devoptab_t devoptab;
}; };
@@ -141,93 +120,39 @@ static_assert(std::size(BIS_MOUNT_ENTRIES) == FF_VOLUMES);
FatStorageEntry g_fat_storage[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) { void fill_stat(const char* path, const FILINFO* fno, struct stat *st) {
// 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) {
memset(st, 0, sizeof(*st)); memset(st, 0, sizeof(*st));
st->st_nlink = 1; 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) { if (fno->fattrib & AM_DIR) {
st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH; st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
} else { } 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) { 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); 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; return r->_errno = 0;
} }
int fat_close(struct _reent *r, void *fd) { int fat_close(struct _reent *r, void *fd) {
if (FR_OK != f_close((FIL*)fd)) { auto file = static_cast<File*>(fd);
return set_errno(r, ENOENT);
if (file->files) {
for (u32 i = 0; i < file->file_count; i++) {
f_close(&file->files[i]);
} }
free(file->files);
}
return r->_errno = 0; return r->_errno = 0;
} }
ssize_t fat_read(struct _reent *r, void *fd, char *ptr, size_t len) { ssize_t fat_read(struct _reent *r, void *fd, char *ptr, size_t len) {
auto file = static_cast<File*>(fd);
UINT total_bytes_read = 0;
while (len) {
UINT bytes_read; UINT bytes_read;
if (FR_OK != f_read((FIL*)fd, ptr, len, &bytes_read)) { auto fil = get_current_file(file);
if (!fil) {
log_write("[FATFS] failed to get fil\n");
return set_errno(r, ENOENT); return set_errno(r, ENOENT);
} }
return bytes_read; 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 total_bytes_read;
} }
off_t fat_seek(struct _reent *r, void *fd, off_t pos, int dir) { 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) { if (dir == SEEK_CUR) {
pos += f_tell((FIL*)fd); pos += file->off;
} else if (dir == SEEK_END) { } else if (dir == SEEK_END) {
pos = f_size((FIL*)fd); pos = size;
} }
if (FR_OK != f_lseek((FIL*)fd, pos)) { file->off = std::clamp<u64>(pos, 0, size);
set_errno(r, ENOENT); set_current_file_pos(file);
return 0;
}
r->_errno = 0; r->_errno = 0;
return f_tell((FIL*)fd); return file->off;
} }
int fat_fstat(struct _reent *r, void *fd, struct stat *st) { 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. */ /* Only fill the attr and size field, leaving the timestamp blank. */
FILINFO info = {0}; FILINFO info{};
info.fattrib = file->obj.attr; info.fsize = get_size_from_files(file);
info.fsize = file->obj.objsize;
/* Fill stat info. */ /* Fill stat info. */
fill_stat(&info, st); fill_stat(nullptr, &info, st);
return r->_errno = 0; return r->_errno = 0;
} }
DIR_ITER* fat_diropen(struct _reent *r, DIR_ITER *dirState, const char *path) { 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); set_errno(r, ENOENT);
return NULL; 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) { 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 set_errno(r, ENOENT);
} }
return r->_errno = 0; return r->_errno = 0;
} }
int fat_dirnext(struct _reent *r, DIR_ITER *dirState, char *filename, struct stat *filestat) { int fat_dirnext(struct _reent *r, DIR_ITER *dirState, char *filename, struct stat *filestat) {
auto dir = static_cast<Dir*>(dirState->dirStruct);
FILINFO fno{}; FILINFO fno{};
if (FR_OK != f_readdir((FDIR*)dirState->dirStruct, &fno)) {
if (FR_OK != f_readdir(&dir->dir, &fno)) {
return set_errno(r, ENOENT); 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); strcpy(filename, fno.fname);
fill_stat(&fno, filestat); fill_stat(dir->path, &fno, filestat);
return r->_errno = 0; return r->_errno = 0;
} }
int fat_dirclose(struct _reent *r, DIR_ITER *dirState) { 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 set_errno(r, ENOENT);
} }
return r->_errno = 0; 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); return set_errno(r, ENOENT);
} }
fill_stat(&fno, st); fill_stat(file, &fno, st);
return r->_errno = 0; return r->_errno = 0;
} }
constexpr devoptab_t DEVOPTAB = { constexpr devoptab_t DEVOPTAB = {
.structSize = sizeof(FIL), .structSize = sizeof(File),
.open_r = fat_open, .open_r = fat_open,
.close_r = fat_close, .close_r = fat_close,
.read_r = fat_read, .read_r = fat_read,
.seek_r = fat_seek, .seek_r = fat_seek,
.fstat_r = fat_fstat, .fstat_r = fat_fstat,
.stat_r = fat_lstat, .stat_r = fat_lstat,
.dirStateSize = sizeof(FDIR), .dirStateSize = sizeof(Dir),
.diropen_r = fat_diropen, .diropen_r = fat_diropen,
.dirreset_r = fat_dirreset, .dirreset_r = fat_dirreset,
.dirnext_r = fat_dirnext, .dirnext_r = fat_dirnext,
@@ -378,37 +373,55 @@ constexpr devoptab_t DEVOPTAB = {
.lstat_r = fat_lstat, .lstat_r = fat_lstat,
}; };
Mutex g_mutex{};
bool g_is_init{};
} // namespace } // namespace
Result MountAll() { Result MountAll() {
SCOPED_MUTEX(&g_mutex);
if (g_is_init) {
R_SUCCEED();
}
for (u32 i = 0; i < FF_VOLUMES; i++) { for (u32 i = 0; i < FF_VOLUMES; i++) {
auto& fat = g_fat_storage[i]; auto& fat = g_fat_storage[i];
const auto& bis = BIS_MOUNT_ENTRIES[i]; const auto& bis = BIS_MOUNT_ENTRIES[i];
log_write("[FAT] %s\n", bis.volume_name); // log_write("[FAT] %s\n", bis.volume_name);
fat.lru_cache[0].Init(fat.buffered_small);
fat.lru_cache[1].Init(fat.buffered_large);
fat.devoptab = DEVOPTAB; fat.devoptab = DEVOPTAB;
fat.devoptab.name = bis.volume_name; fat.devoptab.name = bis.volume_name;
fat.devoptab.deviceData = &fat; fat.devoptab.deviceData = &fat;
R_TRY(fsOpenBisStorage(&fat.storage, bis.id)); R_TRY(fsOpenBisStorage(&fat.storage, bis.id));
R_TRY(fsStorageGetSize(&fat.storage, &fat.storage_size)); auto source = std::make_shared<FsStorageSource>(&fat.storage);
log_write("[FAT] BIS SUCCESS %s\n", bis.volume_name);
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); 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); 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(); R_SUCCEED();
} }
void UnmountAll() { void UnmountAll() {
SCOPED_MUTEX(&g_mutex);
if (!g_is_init) {
return;
}
for (u32 i = 0; i < FF_VOLUMES; i++) { for (u32 i = 0; i < FF_VOLUMES; i++) {
auto& fat = g_fat_storage[i]; auto& fat = g_fat_storage[i];
const auto& bis = BIS_MOUNT_ENTRIES[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) { Result fatfs_read(u8 num, void* dst, u64 offset, u64 size) {
// log_write("[FAT] num: %u\n", num); // log_write("[FAT] num: %u\n", num);
auto& fat = sphaira::fatfs::g_fat_storage[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" } // extern "C"

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -4,6 +4,9 @@
#include <mbedtls/md5.h> #include <mbedtls/md5.h>
#include <utility> #include <utility>
#include <zlib.h>
#include <zstd.h>
namespace sphaira::hash { namespace sphaira::hash {
namespace { namespace {
@@ -59,12 +62,152 @@ private:
struct HashSource { struct HashSource {
virtual ~HashSource() = default; 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; 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 { 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); m_seed = crc32CalculateWithSeed(m_seed, buf, size);
} }
@@ -88,7 +231,7 @@ struct HashMd5 final : HashSource {
mbedtls_md5_free(&m_ctx); 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); mbedtls_md5_update_ret(&m_ctx, (const u8*)buf, size);
} }
@@ -113,7 +256,7 @@ struct HashSha1 final : HashSource {
sha1ContextCreate(&m_ctx); 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); sha1ContextUpdate(&m_ctx, buf, size);
} }
@@ -138,7 +281,7 @@ struct HashSha256 final : HashSource {
sha256ContextCreate(&m_ctx); 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); 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); return source->Read(data, off, size, bytes_read);
}, },
[&](const void* data, s64 off, s64 size) -> Result { [&](const void* data, s64 off, s64 size) -> Result {
hash->Update(data, size); hash->Update(data, size, file_size);
R_SUCCEED(); R_SUCCEED();
} }
)); ));
@@ -184,6 +327,9 @@ auto GetTypeStr(Type type) -> const char* {
case Type::Md5: return "MD5"; case Type::Md5: return "MD5";
case Type::Sha1: return "SHA1"; case Type::Sha1: return "SHA1";
case Type::Sha256: return "SHA256"; 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 ""; 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::Md5: return Hash(pbox, std::make_unique<HashMd5>(), source, out);
case Type::Sha1: return Hash(pbox, std::make_unique<HashSha1>(), 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::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(); std::unreachable();
} }

View File

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

View File

@@ -1,6 +1,7 @@
#include "location.hpp" #include "location.hpp"
#include "fs.hpp" #include "fs.hpp"
#include "app.hpp" #include "app.hpp"
#include "usbdvd.hpp"
#include <ff.h> #include <ff.h>
#include <cstring> #include <cstring>
@@ -76,9 +77,22 @@ auto Load() -> Entries {
} }
auto GetStdio(bool write) -> StdioEntries { 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()) { if (!App::GetHddEnable()) {
log_write("[USBHSFS] not enabled\n"); log_write("[USBHSFS] not enabled\n");
return {}; return out;
} }
static UsbHsFsDevice devices[0x20]; 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 connected: %u\n", usbHsFsGetPhysicalDeviceCount());
log_write("[USBHSFS] got count: %u\n", count); log_write("[USBHSFS] got count: %u\n", count);
StdioEntries out{};
for (s32 i = 0; i < count; i++) { for (s32 i = 0; i < count; i++) {
const auto& e = devices[i]; const auto& e = devices[i];
@@ -109,9 +121,6 @@ auto GetStdio(bool write) -> StdioEntries {
auto GetFat() -> StdioEntries { auto GetFat() -> StdioEntries {
StdioEntries out{}; StdioEntries out{};
// todo: move this somewhere else.
out.emplace_back("Qlaunch_romfs:/", "Qlaunch RomFS (Read Only)", true);
for (auto& e : VolumeStr) { for (auto& e : VolumeStr) {
char path[64]; char path[64];
std::snprintf(path, sizeof(path), "%s:/", e); std::snprintf(path, sizeof(path), "%s:/", e);

View File

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

View File

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

View File

@@ -4,6 +4,8 @@
#include <cstring> #include <cstring>
#include <cstdio> #include <cstdio>
#include "log.hpp"
namespace sphaira::mz { namespace sphaira::mz {
namespace { 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) { if ((mode & ZLIB_FILEFUNC_MODE_READWRITEFILTER) == ZLIB_FILEFUNC_MODE_READ) {
mode_fopen = "rb"; mode_fopen = "rb";
} else if (mode & ZLIB_FILEFUNC_MODE_EXISTING) { } else if (mode & ZLIB_FILEFUNC_MODE_EXISTING) {
log_write("[ZIP] opening r/w\n");
mode_fopen = "r+b"; mode_fopen = "r+b";
} else if (mode & ZLIB_FILEFUNC_MODE_CREATE) { } else if (mode & ZLIB_FILEFUNC_MODE_CREATE) {
log_write("[ZIP] opening r/w +\n");
mode_fopen = "wb"; mode_fopen = "wb";
} else { } else {
return NULL; 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) { uLong minizip_read_file_func_stdio(voidpf opaque, voidpf stream, void* buf, uLong size) {
auto file = static_cast<std::FILE*>(stream); auto file = static_cast<std::FILE*>(stream);
log_write("[ZIP] doing read\n");
return std::fread(buf, 1, size, file); return std::fread(buf, 1, size, file);
} }

View File

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

View File

@@ -4,6 +4,7 @@
#include "nro.hpp" #include "nro.hpp"
#include "log.hpp" #include "log.hpp"
#include "fs.hpp" #include "fs.hpp"
#include "utils/thread.hpp"
#include <cstring> #include <cstring>
#include <vector> #include <vector>
@@ -24,7 +25,7 @@ namespace {
using Socket = int; using Socket = int;
constexpr s32 SERVER_PORT = NXLINK_SERVER_PORT; constexpr s32 SERVER_PORT = NXLINK_SERVER_PORT;
constexpr s32 CLIENT_PORT = NXLINK_CLIENT_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_OK = 0;
constexpr s32 ERR_FILE = -1; constexpr s32 ERR_FILE = -1;
@@ -35,7 +36,7 @@ constexpr const char UDP_MAGIC_SERVER[] = {"nxboot"};
constexpr const char UDP_MAGIC_CLIENT[] = {"bootnx"}; constexpr const char UDP_MAGIC_CLIENT[] = {"bootnx"};
Thread g_thread{}; Thread g_thread{};
std::mutex g_mutex{}; Mutex g_mutex{};
std::atomic_bool g_quit{false}; std::atomic_bool g_quit{false};
bool g_is_running{false}; bool g_is_running{false};
NxlinkCallback g_callback{}; NxlinkCallback g_callback{};
@@ -55,8 +56,10 @@ struct SocketWrapper {
} }
} }
void nonBlocking() { void nonBlocking() {
if (this->sock > 0) {
fcntl(sock, F_SETFL, fcntl(sock, F_GETFL) | O_NONBLOCK); fcntl(sock, F_SETFL, fcntl(sock, F_GETFL) | O_NONBLOCK);
} }
}
Socket operator=(Socket s) { return this->sock = s; } Socket operator=(Socket s) { return this->sock = s; }
operator int() { return this->sock; } operator int() { return this->sock; }
Socket sock{}; Socket sock{};
@@ -106,11 +109,11 @@ void WriteCallbackProgress(NxlinkCallbackType type, s64 offset, s64 size) {
g_callback(&data); 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); auto p = static_cast<u8*>(buf);
int got{}, left{size}; int got{}, left{size};
while (!g_quit && got < 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 (len == -1) {
if (errno != EWOULDBLOCK && errno != EAGAIN) { if (errno != EWOULDBLOCK && errno != EAGAIN) {
return false; return false;
@@ -124,11 +127,11 @@ auto recvall(int sock, void* buf, int size) -> bool {
return !g_quit; 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); auto p = static_cast<const u8*>(buf);
int sent{}, left{size}; int sent{}, left{size};
while (!g_quit && sent < 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 (len == -1) {
if (errno != EWOULDBLOCK && errno != EAGAIN) { if (errno != EWOULDBLOCK && errno != EAGAIN) {
return false; return false;
@@ -141,6 +144,23 @@ auto sendall(Socket sock, const void* buf, int size) -> bool {
return !g_quit; 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> { auto get_file_data(Socket sock, int max) -> std::vector<u8> {
std::vector<u8> buf(max); std::vector<u8> buf(max);
std::vector<u8> chunk(ZLIB_CHUNK); 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) { void loop(void* args) {
log_write("in nxlink thread func\n"); log_write("[NXLINK] in nxlink thread func\n");
const sockaddr_in servaddr{ const sockaddr_in servaddr{
.sin_family = AF_INET, .sin_family = AF_INET,
.sin_port = htons(SERVER_PORT), .sin_port = htons(SERVER_PORT),
@@ -213,33 +233,33 @@ void loop(void* args) {
SocketWrapper sock_udp(AF_INET, SOCK_DGRAM, 0); SocketWrapper sock_udp(AF_INET, SOCK_DGRAM, 0);
if (sock < 0 || sock_udp < 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; continue;
} }
u32 tmpval = 1; u32 tmpval = 1;
if (0 > setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &tmpval, sizeof(tmpval))) { 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; continue;
} }
if (0 > setsockopt(sock_udp, SOL_SOCKET, SO_REUSEADDR, &tmpval, sizeof(tmpval))) { 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; continue;
} }
if (0 > bind(sock, (const sockaddr*)&servaddr, sizeof(servaddr))) { 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; continue;
} }
if (0 > bind(sock_udp, (const sockaddr*)&servaddr, sizeof(servaddr))) { 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; continue;
} }
if (0 > listen(sock, 10)) { 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; continue;
} }
@@ -252,7 +272,7 @@ void loop(void* args) {
pfds[1].events = POLLIN; pfds[1].events = POLLIN;
while (!g_quit) { 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) { if (poll_rc < 0) {
break; break;
} else if (poll_rc == 0) { } else if (poll_rc == 0) {
@@ -264,48 +284,52 @@ void loop(void* args) {
if (pfds[1].revents & POLLIN) { if (pfds[1].revents & POLLIN) {
char recvbuf[6]; char recvbuf[6];
socklen_t from_len = sizeof(sa_remote); socklen_t from_len = sizeof(sa_remote);
auto udp_len = recvfrom(sock_udp, recvbuf, sizeof(recvbuf), 0, (sockaddr*)&sa_remote, &from_len); if (!recvall(sock_udp, recvbuf, sizeof(recvbuf), (sockaddr*)&sa_remote, &from_len)) {
if (udp_len == sizeof(recvbuf) && !std::strncmp(recvbuf, UDP_MAGIC_SERVER, std::strlen(UDP_MAGIC_SERVER))) { log_write("[NXLINK] failed to get udp socket: 0x%X %s\n", socketGetLastResult(), strerror(errno));
// log_write("got udp len: %d - %.*s\n", udp_len, udp_len, recvbuf); continue;
}
if (!std::strncmp(recvbuf, UDP_MAGIC_SERVER, std::strlen(UDP_MAGIC_SERVER))) {
sa_remote.sin_family = AF_INET; sa_remote.sin_family = AF_INET;
sa_remote.sin_port = htons(NXLINK_CLIENT_PORT); 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 (!sendall(sock_udp, UDP_MAGIC_CLIENT, std::strlen(UDP_MAGIC_CLIENT), (const sockaddr*)&sa_remote, sizeof(sa_remote))) {
if (udp_len != std::strlen(UDP_MAGIC_CLIENT)) { log_write("[NXLINK] failed to send udp socket: 0x%X %s\n", socketGetLastResult(), strerror(errno));
log_write("nxlink failed to send udp packet\n");
continue; continue;
} }
} }
} }
socklen_t accept_len = sizeof(sa_remote); 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) { if (connfd < 0) {
log_write("[NXLINK] failed to accept socket: 0x%X %s\n", socketGetLastResult(), strerror(errno));
continue; continue;
} }
WriteCallbackNone(NxlinkCallbackType_Connected); WriteCallbackNone(NxlinkCallbackType_Connected);
u32 namelen{}; u32 namelen{};
if (!recvall(connfd, &namelen, sizeof(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; continue;
} }
fs::FsPath name{}; fs::FsPath name{};
if (namelen >= sizeof(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; continue;
} }
if (!recvall(connfd, name, namelen)) { 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; continue;
} }
log_write("got name: %s\n", name.s); log_write("[NXLINK] got name: %s\n", name.s);
u32 filesize{}; u32 filesize{};
if (!recvall(connfd, &filesize, sizeof(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; continue;
} }
@@ -319,7 +343,7 @@ void loop(void* args) {
// tell nxlink that we want this file // tell nxlink that we want this file
if (!sendall(connfd, &ERR_OK, sizeof(ERR_OK))) { 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; continue;
} }
@@ -329,6 +353,7 @@ void loop(void* args) {
WriteCallbackFile(NxlinkCallbackType_WriteEnd, name); WriteCallbackFile(NxlinkCallbackType_WriteEnd, name);
if (file_data.empty()) { if (file_data.empty()) {
log_write("[NXLINK] failed to get file data: 0x%X %s\n", socketGetLastResult(), strerror(errno));
continue; continue;
} }
@@ -343,7 +368,7 @@ void loop(void* args) {
// if (R_FAILED(rc = create_directories(fs, path))) { // if (R_FAILED(rc = create_directories(fs, path))) {
if (R_FAILED(rc = fs.CreateDirectoryRecursivelyWithPath(path))) { if (R_FAILED(rc = fs.CreateDirectoryRecursivelyWithPath(path))) {
sendall(connfd, &ERR_FILE, sizeof(ERR_FILE)); 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; continue;
} }
@@ -351,7 +376,7 @@ void loop(void* args) {
const auto temp_path = path + "~"; const auto temp_path = path + "~";
if (R_FAILED(rc = fs.CreateFile(temp_path, file_data.size(), 0)) && rc != FsError_PathAlreadyExists) { if (R_FAILED(rc = fs.CreateFile(temp_path, file_data.size(), 0)) && rc != FsError_PathAlreadyExists) {
sendall(connfd, &ERR_FILE, sizeof(ERR_FILE)); 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; continue;
} }
ON_SCOPE_EXIT(fs.DeleteFile(temp_path)); ON_SCOPE_EXIT(fs.DeleteFile(temp_path));
@@ -360,13 +385,13 @@ void loop(void* args) {
fs::File f; fs::File f;
if (R_FAILED(rc = fs.OpenFile(temp_path, FsOpenMode_Write, &f))) { if (R_FAILED(rc = fs.OpenFile(temp_path, FsOpenMode_Write, &f))) {
sendall(connfd, &ERR_FILE, sizeof(ERR_FILE)); 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; continue;
} }
if (R_FAILED(rc = f.SetSize(file_data.size()))) { if (R_FAILED(rc = f.SetSize(file_data.size()))) {
sendall(connfd, &ERR_FILE, sizeof(ERR_FILE)); 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; continue;
} }
@@ -374,7 +399,7 @@ void loop(void* args) {
while (offset < file_data.size()) { while (offset < file_data.size()) {
svcSleepThread(YieldType_WithoutCoreMigration); svcSleepThread(YieldType_WithoutCoreMigration);
u64 chunk_size = ZLIB_CHUNK; u64 chunk_size = 1024*1024;
if (offset + chunk_size > file_data.size()) { if (offset + chunk_size > file_data.size()) {
chunk_size = file_data.size() - offset; 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 = fsFileWrite(&f, 0, file_data.data(), file_data.size(), FsWriteOption_None))) {
if (R_FAILED(rc)) { if (R_FAILED(rc)) {
sendall(connfd, &ERR_FILE, sizeof(ERR_FILE)); 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; continue;
} }
} }
if (R_FAILED(rc = fs.DeleteFile(path)) && rc != FsError_PathNotFound) { 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; continue;
} }
if (R_FAILED(rc = fs.RenameFile(temp_path, path))) { 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; 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))) { 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; continue;
} }
@@ -435,7 +462,7 @@ void loop(void* args) {
} }
args += nxlinked; 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))) { if (R_SUCCEEDED(sphaira::nro_launch(path, args))) {
g_quit = true; g_quit = true;
} }
@@ -449,7 +476,7 @@ void loop(void* args) {
extern "C" { extern "C" {
bool nxlinkInitialize(NxlinkCallback callback) { bool nxlinkInitialize(NxlinkCallback callback) {
std::scoped_lock lock{g_mutex}; SCOPED_MUTEX(&g_mutex);
if (g_is_running) { if (g_is_running) {
return false; return false;
} }
@@ -458,13 +485,13 @@ bool nxlinkInitialize(NxlinkCallback callback) {
g_quit = false; g_quit = false;
Result rc; Result rc;
if (R_FAILED(rc = threadCreate(&g_thread, loop, nullptr, nullptr, 1024*64, PRIO_PREEMPTIVE, 2))) { if (R_FAILED(rc = sphaira::utils::CreateThread(&g_thread, loop, nullptr, 1024*64))) {
log_write("failed to create nxlink thread: 0x%X\n", rc); log_write("[NXLINK] failed to create nxlink thread: 0x%X\n", rc);
return false; return false;
} }
if (R_FAILED(rc = threadStart(&g_thread))) { 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); threadClose(&g_thread);
return false; return false;
} }
@@ -473,17 +500,15 @@ bool nxlinkInitialize(NxlinkCallback callback) {
} }
void nxlinkExit() { void nxlinkExit() {
std::scoped_lock lock{g_mutex}; SCOPED_MUTEX(&g_mutex);
if (g_is_running) {
g_is_running = false; g_is_running = false;
}
g_quit = true; g_quit = true;
threadWaitForExit(&g_thread); threadWaitForExit(&g_thread);
threadClose(&g_thread); threadClose(&g_thread);
} }
void nxlinkSignalExit() { void nxlinkSignalExit() {
std::scoped_lock lock{g_mutex}; SCOPED_MUTEX(&g_mutex);
g_quit = true; 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); m_value = ini_getbool(m_section.c_str(), name, m_default_value, App::CONFIG_PATH);
} else if constexpr(std::is_same_v<T, long>) { } else if constexpr(std::is_same_v<T, long>) {
m_value = ini_getl(m_section.c_str(), name, m_default_value, App::CONFIG_PATH); 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>) { } else if constexpr(std::is_same_v<T, std::string>) {
char buf[FS_MAX_PATH]; char buf[FS_MAX_PATH];
ini_gets(m_section.c_str(), name, m_default_value.c_str(), buf, sizeof(buf), App::CONFIG_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); ini_putl(m_section.c_str(), m_name.c_str(), value, App::CONFIG_PATH);
} else if constexpr(std::is_same_v<T, long>) { } else if constexpr(std::is_same_v<T, long>) {
ini_putl(m_section.c_str(), m_name.c_str(), value, App::CONFIG_PATH); 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>) { } 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); 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); m_value = ini_parse_getbool(value, m_default_value);
} else if constexpr(std::is_same_v<T, long>) { } else if constexpr(std::is_same_v<T, long>) {
m_value = ini_parse_getl(value, m_default_value); 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>) { } else if constexpr(std::is_same_v<T, std::string>) {
m_value = value; 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<bool>;
template struct OptionBase<long>; template struct OptionBase<long>;
template struct OptionBase<float>;
template struct OptionBase<std::string>; template struct OptionBase<std::string>;
} // namespace sphaira::option } // namespace sphaira::option

View File

@@ -3,6 +3,7 @@
#include "defines.hpp" #include "defines.hpp"
#include "app.hpp" #include "app.hpp"
#include "minizip_helper.hpp" #include "minizip_helper.hpp"
#include "utils/thread.hpp"
#include <vector> #include <vector>
#include <algorithm> #include <algorithm>
@@ -72,13 +73,13 @@ public:
}; };
struct ThreadData { 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; auto GetResults() volatile -> Result;
void WakeAllThreads(); void WakeAllThreads();
auto IsAnyRunning() volatile const -> bool { auto IsAnyRunning() volatile const -> bool {
return read_running || write_running; return read_running || decompress_running || write_running;
} }
auto GetWriteOffset() volatile const -> s64 { auto GetWriteOffset() volatile const -> s64 {
@@ -100,6 +101,17 @@ struct ThreadData {
void SetReadResult(Result result) { void SetReadResult(Result result) {
read_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. // wake up write thread as it may be waiting on data that never comes.
condvarWakeOne(std::addressof(can_write)); condvarWakeOne(std::addressof(can_write));
@@ -110,6 +122,10 @@ struct ThreadData {
void SetWriteResult(Result result) { void SetWriteResult(Result result) {
write_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()); ueventSignal(GetDoneEvent());
} }
@@ -122,9 +138,12 @@ struct ThreadData {
Result Pull(void* data, s64 size, u64* bytes_read); Result Pull(void* data, s64 size, u64* bytes_read);
Result readFuncInternal(); Result readFuncInternal();
Result decompressFuncInternal();
Result writeFuncInternal(); Result writeFuncInternal();
private: 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 SetWriteBuf(std::vector<u8>& buf, s64 size);
Result GetWriteBuf(std::vector<u8>& buf_out, s64& off_out); Result GetWriteBuf(std::vector<u8>& buf_out, s64& off_out);
Result SetPullBuf(std::vector<u8>& buf, s64 size); Result SetPullBuf(std::vector<u8>& buf, s64 size);
@@ -136,21 +155,30 @@ private:
// these need to be copied // these need to be copied
ui::ProgressBox* const pbox; ui::ProgressBox* const pbox;
const ReadCallback& rfunc; const ReadCallback& rfunc;
const DecompressCallback& dfunc;
const WriteCallback& wfunc; const WriteCallback& wfunc;
// these need to be created // these need to be created
Mutex mutex{}; Mutex read_mutex{};
Mutex write_mutex{};
Mutex pull_mutex{}; Mutex pull_mutex{};
CondVar can_read{}; CondVar can_read{};
CondVar can_write{}; CondVar can_write{};
CondVar can_decompress{};
CondVar can_decompress_write{};
// only used when pull is active.
CondVar can_pull{}; CondVar can_pull{};
CondVar can_pull_write{}; CondVar can_pull_write{};
UEvent m_uevent_done{}; UEvent m_uevent_done{};
UEvent m_uevent_progres{}; UEvent m_uevent_progres{};
RingBuf<2> read_buffers{};
RingBuf<2> write_buffers{}; RingBuf<2> write_buffers{};
std::vector<u8> pull_buffer{}; std::vector<u8> pull_buffer{};
s64 pull_buffer_offset{}; s64 pull_buffer_offset{};
@@ -159,27 +187,35 @@ private:
// these are shared between threads // these are shared between threads
std::atomic<s64> read_offset{}; std::atomic<s64> read_offset{};
std::atomic<s64> decompress_offset{};
std::atomic<s64> write_offset{}; std::atomic<s64> write_offset{};
std::atomic<Result> read_result{}; std::atomic<Result> read_result{};
std::atomic<Result> decompress_result{};
std::atomic<Result> write_result{}; std::atomic<Result> write_result{};
std::atomic<Result> pull_result{}; std::atomic<Result> pull_result{};
std::atomic_bool read_running{true}; std::atomic_bool read_running{true};
std::atomic_bool decompress_running{true};
std::atomic_bool write_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} : pbox{_pbox}
, rfunc{_rfunc} , rfunc{_rfunc}
, dfunc{_dfunc}
, wfunc{_wfunc} , wfunc{_wfunc}
, read_buffer_size{buffer_size} , read_buffer_size{buffer_size}
, write_size{size} { , write_size{size} {
mutexInit(std::addressof(mutex)); mutexInit(std::addressof(read_mutex));
mutexInit(std::addressof(write_mutex));
mutexInit(std::addressof(pull_mutex)); mutexInit(std::addressof(pull_mutex));
condvarInit(std::addressof(can_read)); 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_write));
condvarInit(std::addressof(can_pull)); condvarInit(std::addressof(can_pull));
condvarInit(std::addressof(can_pull_write)); 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 { auto ThreadData::GetResults() volatile -> Result {
R_TRY(pbox->ShouldExitResult()); R_TRY(pbox->ShouldExitResult());
R_TRY(read_result.load()); R_TRY(read_result.load());
R_TRY(decompress_result.load());
R_TRY(write_result.load()); R_TRY(write_result.load());
R_TRY(pull_result.load()); R_TRY(pull_result.load());
R_SUCCEED(); R_SUCCEED();
@@ -198,44 +235,80 @@ auto ThreadData::GetResults() volatile -> Result {
void ThreadData::WakeAllThreads() { void ThreadData::WakeAllThreads() {
condvarWakeAll(std::addressof(can_read)); condvarWakeAll(std::addressof(can_read));
condvarWakeAll(std::addressof(can_write)); 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));
condvarWakeAll(std::addressof(can_pull_write)); 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)); 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) { Result ThreadData::SetWriteBuf(std::vector<u8>& buf, s64 size) {
buf.resize(size); buf.resize(size);
mutexLock(std::addressof(mutex)); mutexLock(std::addressof(write_mutex));
if (!write_buffers.ringbuf_free()) { if (!write_buffers.ringbuf_free()) {
if (!write_running) { if (!decompress_running) {
R_SUCCEED(); 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()); R_TRY(GetResults());
write_buffers.ringbuf_push(buf, 0); write_buffers.ringbuf_push(buf, 0);
return condvarWakeOne(std::addressof(can_write)); return condvarWakeOne(std::addressof(can_write));
} }
Result ThreadData::GetWriteBuf(std::vector<u8>& buf_out, s64& off_out) { 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 (!write_buffers.ringbuf_size()) {
if (!read_running) { if (!decompress_running) {
buf_out.resize(0); buf_out.resize(0);
R_SUCCEED(); 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()); R_TRY(GetResults());
write_buffers.ringbuf_pop(buf_out, off_out); 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) { 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())) { while (this->read_offset < this->write_size && R_SUCCEEDED(this->GetResults())) {
// read more data // read more data
const auto buffer_offset = this->read_offset.load();
s64 read_size = this->read_buffer_size; s64 read_size = this->read_buffer_size;
u64 bytes_read{}; u64 bytes_read{};
@@ -306,13 +380,82 @@ Result ThreadData::readFuncInternal() {
} }
auto buf_size = bytes_read; 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"); log_write("finished read thread success!\n");
R_SUCCEED(); 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. // write thread writes data to wfunc.
Result ThreadData::writeFuncInternal() { Result ThreadData::writeFuncInternal() {
ON_SCOPE_EXIT( write_running = false; ); ON_SCOPE_EXIT( write_running = false; );
@@ -349,17 +492,20 @@ void readFunc(void* d) {
log_write("read thread returned now\n"); 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) { void writeFunc(void* d) {
auto t = static_cast<ThreadData*>(d); auto t = static_cast<ThreadData*>(d);
t->SetWriteResult(t->writeFuncInternal()); t->SetWriteResult(t->writeFuncInternal());
log_write("write thread returned now\n"); log_write("write thread returned now\n");
} }
auto GetAlternateCore(int id) { 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) {
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) {
const auto is_file_based_emummc = App::IsFileBaseEmummc(); const auto is_file_based_emummc = App::IsFileBaseEmummc();
if (is_file_based_emummc) { if (is_file_based_emummc) {
@@ -403,27 +549,30 @@ Result TransferInternal(ui::ProgressBox* pbox, s64 size, const ReadCallback& rfu
R_SUCCEED(); R_SUCCEED();
} }
else { else {
const auto WRITE_THREAD_CORE = sfunc ? pbox->GetCpuId() : GetAlternateCore(pbox->GetCpuId()); ThreadData t_data{pbox, size, rfunc, dfunc, wfunc, buffer_size};
const auto READ_THREAD_CORE = GetAlternateCore(WRITE_THREAD_CORE);
ThreadData t_data{pbox, size, rfunc, wfunc, buffer_size};
Thread t_read{}; 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)); 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{}; 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)); ON_SCOPE_EXIT(threadClose(&t_write));
const auto start_threads = [&]() -> Result { const auto start_threads = [&]() -> Result {
log_write("starting threads\n"); log_write("starting threads\n");
R_TRY(threadStart(std::addressof(t_read))); R_TRY(threadStart(std::addressof(t_read)));
R_TRY(threadStart(std::addressof(t_decompress)));
R_TRY(threadStart(std::addressof(t_write))); R_TRY(threadStart(std::addressof(t_write)));
R_SUCCEED(); R_SUCCEED();
}; };
ON_SCOPE_EXIT(threadWaitForExit(std::addressof(t_read))); ON_SCOPE_EXIT(threadWaitForExit(std::addressof(t_read)));
ON_SCOPE_EXIT(threadWaitForExit(std::addressof(t_decompress)));
ON_SCOPE_EXIT(threadWaitForExit(std::addressof(t_write))); ON_SCOPE_EXIT(threadWaitForExit(std::addressof(t_write)));
if (sfunc) { if (sfunc) {
@@ -463,6 +612,8 @@ Result TransferInternal(ui::ProgressBox* pbox, s64 size, const ReadCallback& rfu
if (R_FAILED(waitSingleHandle(t_read.handle, 1000))) { if (R_FAILED(waitSingleHandle(t_read.handle, 1000))) {
continue; continue;
} else if (R_FAILED(waitSingleHandle(t_decompress.handle, 1000))) {
continue;
} else if (R_FAILED(waitSingleHandle(t_write.handle, 1000))) { } else if (R_FAILED(waitSingleHandle(t_write.handle, 1000))) {
continue; continue;
} }
@@ -485,18 +636,22 @@ Result TransferInternal(ui::ProgressBox* pbox, s64 size, const ReadCallback& rfu
} // namespace } // namespace
Result Transfer(ui::ProgressBox* pbox, s64 size, const ReadCallback& rfunc, const WriteCallback& wfunc, Mode mode) { 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) { 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()); R_TRY(start());
return sfunc(pull); return sfunc(pull);
}, mode); }, mode);
} }
Result TransferPull(ui::ProgressBox* pbox, s64 size, const ReadCallback& rfunc, const StartCallback2& sfunc, Mode 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) { 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; *bytes_read = result;
R_SUCCEED(); R_SUCCEED();
}, },
nullptr,
[&](const void* data, s64 off, s64 size) -> Result { [&](const void* data, s64 off, s64 size) -> Result {
return f.Write(off, data, size, FsWriteOption_None); 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; return rc;
}, },
nullptr,
[&](const void* data, s64 off, s64 size) -> Result { [&](const void* data, s64 off, s64 size) -> Result {
if (ZIP_OK != zipWriteInFileInZip(zfile, data, size)) { if (ZIP_OK != zipWriteInFileInZip(zfile, data, size)) {
log_write("failed to write zip file: %s\n", path.s); 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/nca.hpp"
#include "yati/nx/ncm.hpp" #include "yati/nx/ncm.hpp"
#include "utils/thread.hpp"
#include <cstring> #include <cstring>
#include <atomic> #include <atomic>
#include <ranges> #include <ranges>
@@ -18,9 +20,6 @@
namespace sphaira::title { namespace sphaira::title {
namespace { namespace {
constexpr int THREAD_PRIO = PRIO_PREEMPTIVE;
constexpr int THREAD_CORE = 1;
struct ThreadData { struct ThreadData {
ThreadData(bool title_cache); ThreadData(bool title_cache);
@@ -383,8 +382,7 @@ Result Init() {
} }
g_thread_data = std::make_unique<ThreadData>(true); 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)); R_TRY(utils::CreateThread(&g_thread, ThreadFunc, g_thread_data.get(), 1024*32));
svcSetThreadCoreMask(g_thread.handle, THREAD_CORE, THREAD_AFFINITY_DEFAULT(THREAD_CORE));
R_TRY(threadStart(&g_thread)); 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_ThemezerFailedToDownloadTheme: return "SphairaError_ThemezerFailedToDownloadTheme";
case Result_MainFailedToDownloadUpdate: return "SphairaError_MainFailedToDownloadUpdate"; case Result_MainFailedToDownloadUpdate: return "SphairaError_MainFailedToDownloadUpdate";
case Result_UsbDsBadDeviceSpeed: return "SphairaError_UsbDsBadDeviceSpeed"; case Result_UsbDsBadDeviceSpeed: return "SphairaError_UsbDsBadDeviceSpeed";
case Result_NcaBadMagic: return "SphairaError_NcaBadMagic";
case Result_NspBadMagic: return "SphairaError_NspBadMagic"; case Result_NspBadMagic: return "SphairaError_NspBadMagic";
case Result_XciBadMagic: return "SphairaError_XciBadMagic"; case Result_XciBadMagic: return "SphairaError_XciBadMagic";
case Result_XciSecurePartitionNotFound: return "SphairaError_XciSecurePartitionNotFound"; case Result_XciSecurePartitionNotFound: return "SphairaError_XciSecurePartitionNotFound";
@@ -147,6 +148,16 @@ auto GetCodeMessage(Result rc) -> const char* {
case Result_YatiCertNotFound: return "SphairaError_YatiCertNotFound"; case Result_YatiCertNotFound: return "SphairaError_YatiCertNotFound";
case Result_YatiNcmDbCorruptHeader: return "SphairaError_YatiNcmDbCorruptHeader"; case Result_YatiNcmDbCorruptHeader: return "SphairaError_YatiNcmDbCorruptHeader";
case Result_YatiNcmDbCorruptInfos: return "SphairaError_YatiNcmDbCorruptInfos"; 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 ""; return "";
@@ -166,7 +177,7 @@ ErrorBox::ErrorBox(const std::string& message) : m_message{message} {
SetPop(); SetPop();
}}); }});
App::PlaySoundEffect(SoundEffect::SoundEffect_Error); App::PlaySoundEffect(SoundEffect::Error);
} }
ErrorBox::ErrorBox(Result code, const std::string& message) : ErrorBox{message} { 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) { if (index != old_index) {
App::PlaySoundEffect(SoundEffect_Scroll); App::PlaySoundEffect(SoundEffect::Scroll);
s64 delta = index - old_index; s64 delta = index - old_index;
s64 start = m_yoff / max * m_row; 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) { if (index != old_index) {
App::PlaySoundEffect(SoundEffect_Scroll); App::PlaySoundEffect(SoundEffect::Scroll);
s64 start = m_yoff / max * m_row; s64 start = m_yoff / max * m_row;
while (index < start) { 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](){ std::make_pair(Button::DPAD_DOWN | Button::RS_DOWN, Action{[this](){
if (m_index < (m_options.size() - 1)) { if (m_index < (m_options.size() - 1)) {
SetIndex(m_index + 1); SetIndex(m_index + 1);
App::PlaySoundEffect(SoundEffect_Focus); App::PlaySoundEffect(SoundEffect::Focus);
} }
}}), }}),
std::make_pair(Button::DPAD_UP | Button::RS_UP, Action{[this](){ std::make_pair(Button::DPAD_UP | Button::RS_UP, Action{[this](){
if (m_index != 0) { if (m_index != 0) {
SetIndex(m_index - 1); SetIndex(m_index - 1);
App::PlaySoundEffect(SoundEffect_Focus); App::PlaySoundEffect(SoundEffect::Focus);
} }
}}), }}),
std::make_pair(Button::X, Action{"Options"_i18n, [this](){ 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) { if (touch && m_index == i) {
FireAction(Button::A); FireAction(Button::A);
} else { } else {
App::PlaySoundEffect(SoundEffect_Focus); App::PlaySoundEffect(SoundEffect::Focus);
SetIndex(i); SetIndex(i);
} }
}); });

View File

@@ -6,13 +6,16 @@ namespace {
} // 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](){ SetAction(Button::B, Action{"Back"_i18n, [this](){
SetPop(); SetPop();
}}); }});
std::string buf; 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); m_file.GetSize(&m_file_size);
buf.resize(m_file_size + 1); buf.resize(m_file_size + 1);

View File

@@ -1,11 +1,14 @@
#include "ui/menus/filebrowser.hpp" #include "ui/menus/filebrowser.hpp"
#include "ui/menus/homebrew.hpp" #include "ui/menus/homebrew.hpp"
#include "ui/menus/file_viewer.hpp"
#include "ui/menus/image_viewer.hpp"
#include "ui/sidebar.hpp" #include "ui/sidebar.hpp"
#include "ui/option_box.hpp" #include "ui/option_box.hpp"
#include "ui/popup_list.hpp" #include "ui/popup_list.hpp"
#include "ui/progress_box.hpp" #include "ui/progress_box.hpp"
#include "ui/error_box.hpp" #include "ui/error_box.hpp"
#include "ui/menus/file_viewer.hpp" #include "ui/music_player.hpp"
#include "utils/devoptab.hpp" #include "utils/devoptab.hpp"
@@ -28,6 +31,7 @@
#include "yati/yati.hpp" #include "yati/yati.hpp"
#include "yati/source/file.hpp" #include "yati/source/file.hpp"
#include <usbdvd.h>
#include <minIni.h> #include <minIni.h>
#include <minizip/zip.h> #include <minizip/zip.h>
#include <minizip/unzip.h> #include <minizip/unzip.h>
@@ -80,10 +84,10 @@ constexpr FsEntry FS_ENTRIES[]{
constexpr std::string_view AUDIO_EXTENSIONS[] = { constexpr std::string_view AUDIO_EXTENSIONS[] = {
"mp3", "ogg", "flac", "wav", "aac" "ac3", "aif", "asf", "bfwav", "mp3", "ogg", "flac", "wav", "aac" "ac3", "aif", "asf", "bfwav",
"bfsar", "bfstm", "bfsar", "bfstm", "bwav",
}; };
constexpr std::string_view VIDEO_EXTENSIONS[] = { 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", "m2v", "m4a", "mov", "mpeg", "mpg", "mts", "swf", "ts", "vob", "wma", "wmv",
}; };
constexpr std::string_view IMAGE_EXTENSIONS[] = { constexpr std::string_view IMAGE_EXTENSIONS[] = {
@@ -98,6 +102,9 @@ constexpr std::string_view NSP_EXTENSIONS[] = {
constexpr std::string_view XCI_EXTENSIONS[] = { constexpr std::string_view XCI_EXTENSIONS[] = {
"xci", "xcz", "xci", "xcz",
}; };
constexpr std::string_view NCA_EXTENSIONS[] = {
"nca", "ncz",
};
// these are files that are already compressed or encrypted and should // these are files that are already compressed or encrypted and should
// be stored raw in a zip file. // be stored raw in a zip file.
constexpr std::string_view COMPRESSED_EXTENSIONS[] = { constexpr std::string_view COMPRESSED_EXTENSIONS[] = {
@@ -106,6 +113,16 @@ constexpr std::string_view COMPRESSED_EXTENSIONS[] = {
constexpr std::string_view ZIP_EXTENSIONS[] = { constexpr std::string_view ZIP_EXTENSIONS[] = {
"zip", "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 { struct RomDatabaseEntry {
// uses the naming scheme from retropie. // 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); SetSide(m_side);
log_write("getting path\n");
auto buf = path; auto buf = path;
if (path.empty() && entry.IsSd()) { if (path.empty() && entry.IsSd()) {
ini_gets("paths", "last_path", entry.root, buf, sizeof(buf), App::CONFIG_PATH); 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; buf = entry.root;
} }
log_write("setting fs\n");
SetFs(fs, buf, entry); 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} { 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) { if (touch && m_index == i) {
FireAction(Button::A); FireAction(Button::A);
} else { } else {
App::PlaySoundEffect(SoundEffect_Focus); App::PlaySoundEffect(SoundEffect::Focus);
SetIndex(i); SetIndex(i);
} }
}); });
@@ -574,8 +595,9 @@ void FsView::Draw(NVGcontext* vg, Theme* theme) {
if (e.IsDir()) { if (e.IsDir()) {
// NOTE: this takes longer than 16ms when opening a new folder due to it // NOTE: this takes longer than 16ms when opening a new folder due to it
// checking all 9 folders at once. // 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; got_dir_count = true;
e.done_stat = true;
m_fs->DirGetEntryCount(GetNewPath(e), &e.file_count, &e.dir_count); 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); 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()) { } 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); const auto path = GetNewPath(e);
if (m_fs->IsNative()) { if (m_fs->IsNative()) {
m_fs->GetFileTimeStampRaw(path, &e.time_stamp); m_fs->GetFileTimeStampRaw(path, &e.time_stamp);
@@ -686,12 +709,44 @@ void FsView::OnClick() {
nro_launch(GetNewPathCurrent()); nro_launch(GetNewPathCurrent());
} }
}); });
} else if (IsExtension(entry.GetExtension(), NCA_EXTENSIONS)) {
MountFileFs(devoptab::MountNca, devoptab::UmountNca);
} else if (IsExtension(entry.GetExtension(), NSP_EXTENSIONS)) { } else if (IsExtension(entry.GetExtension(), NSP_EXTENSIONS)) {
MountNspFs(); MountFileFs(devoptab::MountNsp, devoptab::UmountNsp);
} else if (IsExtension(entry.GetExtension(), XCI_EXTENSIONS)) { } else if (IsExtension(entry.GetExtension(), XCI_EXTENSIONS)) {
MountXciFs(); MountFileFs(devoptab::MountXci, devoptab::UmountXci);
} else if (IsExtension(entry.GetExtension(), "zip")) { } 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)) { } else if (IsExtension(entry.GetExtension(), INSTALL_EXTENSIONS)) {
InstallFiles(); InstallFiles();
} else if (IsSd()) { } 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](){ 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()) { if (m_entries_current.size() && !m_selected_count && GetEntry().IsFile()) {
options->Add<SidebarEntryCallback>("Hash"_i18n, [this](){ options->Add<SidebarEntryCallback>("Hash"_i18n, [this](){
auto options = std::make_unique<Sidebar>("Hash Options"_i18n, Sidebar::Side::RIGHT); 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](){ options->Add<SidebarEntryCallback>("SHA256"_i18n, [this](){
DisplayHash(hash::Type::Sha256); 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; fs::FsPath mount;
const auto rc = devoptab::MountNsp(GetFs(), GetNewPathCurrent(), mount); const auto rc = mount_func(GetFs(), GetNewPathCurrent(), mount);
App::PushErrorBox(rc, "Failed to mount NSP."_i18n); App::PushErrorBox(rc, "Failed to mount FS."_i18n);
if (R_SUCCEEDED(rc)) { if (R_SUCCEEDED(rc)) {
auto fs = std::make_shared<FsStdioWrapper>(mount, [mount](){ auto fs = std::make_shared<FsStdioWrapper>(mount, [mount, umount_func](){
devoptab::UmountNsp(mount); umount_func(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);
}); });
MountFsHelper(fs, GetEntry().GetName()); MountFsHelper(fs, GetEntry().GetName());
@@ -2007,32 +2050,15 @@ void FsView::MountZipFs() {
} }
Base::Base(u32 flags, u32 options) 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) 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} : MenuBase{"FileBrowser"_i18n, flags}
, m_options{options} { , m_options{options} {
if (m_options & FsOption_CanSplit) { Init(fs, fs_entry, path, is_custom);
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);
} }
void Base::Update(Controller* controller, TouchInfo* touch) { 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(); 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) { void MountFsHelper(const std::shared_ptr<fs::Fs>& fs, const fs::FsPath& name) {
const filebrowser::FsEntry fs_entry{ const filebrowser::FsEntry fs_entry{
.name = name, .name = name,

View File

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

View File

@@ -7,9 +7,13 @@
#include "image.hpp" #include "image.hpp"
#include "swkbd.hpp" #include "swkbd.hpp"
#include "utils/utils.hpp"
#include "utils/nsz_dumper.hpp"
#include "ui/menus/game_menu.hpp" #include "ui/menus/game_menu.hpp"
#include "ui/menus/game_meta_menu.hpp" #include "ui/menus/game_meta_menu.hpp"
#include "ui/menus/save_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/sidebar.hpp"
#include "ui/error_box.hpp" #include "ui/error_box.hpp"
#include "ui/option_box.hpp" #include "ui/option_box.hpp"
@@ -50,6 +54,11 @@ struct NspSource final : dump::BaseSource {
return rc; 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 { auto GetName(const std::string& path) const -> std::string {
const auto it = std::ranges::find_if(m_entries, [&path](auto& e){ const auto it = std::ranges::find_if(m_entries, [&path](auto& e){
return path.find(e.path.s) != path.npos; return path.find(e.path.s) != path.npos;
@@ -86,11 +95,57 @@ struct NspSource final : dump::BaseSource {
return App::GetDefaultImage(); 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: private:
std::vector<NspEntry> m_entries{}; std::vector<NspEntry> m_entries{};
bool m_is_file_based_emummc{}; 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) { Result Notify(Result rc, const std::string& error_message) {
if (R_FAILED(rc)) { if (R_FAILED(rc)) {
App::Push<ui::ErrorBox>(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) { void FreeEntry(NVGcontext* vg, Entry& e) {
nvgDeleteImage(vg, e.image); nvgDeleteImage(vg, e.image);
e.image = 0; e.image = 0;
@@ -324,26 +367,15 @@ Menu::Menu(u32 flags) : grid::Menu{"Games"_i18n, flags} {
); );
}); });
options->Add<SidebarEntryCallback>("Export NSP"_i18n, [this](){ auto export_nsp = options->Add<SidebarEntryCallback>("Export NSP"_i18n, [this](){
auto options = std::make_unique<Sidebar>("Select content to export"_i18n, Sidebar::Side::RIGHT); ExportOptions(false);
ON_SCOPE_EXIT(App::Push(std::move(options))); });
export_nsp->Depends(App::IsApplication, "Not supported in Applet Mode"_i18n);
options->Add<SidebarEntryCallback>("Export All"_i18n, [this](){ auto export_nsz = options->Add<SidebarEntryCallback>("Export NSZ"_i18n, [this](){
DumpGames(title::ContentFlag_All); ExportOptions(true);
}, true); }, "Exports to NSZ (compressed NSP)"_i18n);
options->Add<SidebarEntryCallback>("Export Application"_i18n, [this](){ export_nsz->Depends(App::IsApplication, "Not supported in Applet Mode"_i18n);
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);
options->Add<SidebarEntryCallback>("Export options"_i18n, [this](){ options->Add<SidebarEntryCallback>("Export options"_i18n, [this](){
App::DisplayDumpOptions(false); App::DisplayDumpOptions(false);
@@ -418,6 +450,9 @@ Menu::Menu(u32 flags) : grid::Menu{"Games"_i18n, flags} {
ns::Initialize(); ns::Initialize();
es::Initialize(); es::Initialize();
title::Init(); title::Init();
fsOpenGameCardDetectionEventNotifier(std::addressof(m_gc_event_notifier));
fsEventNotifierGetEventHandle(std::addressof(m_gc_event_notifier), std::addressof(m_gc_event), true);
} }
Menu::~Menu() { Menu::~Menu() {
@@ -426,9 +461,15 @@ Menu::~Menu() {
FreeEntries(); FreeEntries();
ns::Exit(); ns::Exit();
es::Exit(); es::Exit();
eventClose(std::addressof(m_gc_event));
fsEventNotifierClose(std::addressof(m_gc_event_notifier));
} }
void Menu::Update(Controller* controller, TouchInfo* touch) { 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) { if (m_dirty) {
App::Notify("Updating application record list"_i18n); App::Notify("Updating application record list"_i18n);
SortAndFindLastFile(true); SortAndFindLastFile(true);
@@ -439,7 +480,7 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
if (touch && m_index == i) { if (touch && m_index == i) {
FireAction(Button::A); FireAction(Button::A);
} else { } else {
App::PlaySoundEffect(SoundEffect_Focus); App::PlaySoundEffect(SoundEffect::Focus);
SetIndex(i); SetIndex(i);
} }
}); });
@@ -538,7 +579,7 @@ void Menu::ScanHomebrew() {
continue; continue;
} }
m_entries.emplace_back(e.application_id, e.type); m_entries.emplace_back(e.application_id, e.last_event);
} }
offset += record_count; 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(); auto targets = GetSelectedEntries();
ClearSelection(); ClearSelection();
std::vector<NspEntry> nsp_entries; std::vector<NspEntry> nsp_entries;
for (auto& e : targets) { 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) { 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(); fs::FsPath name_buf = e.GetName();
title::utilsReplaceIllegalCharacters(name_buf, true); 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; fs::FsPath path;
if (App::GetApp()->m_dump_app_folder.Get()) { 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 { } 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; return path;
} }
Result BuildContentEntry(const NsApplicationContentMetaStatus& status, ContentInfoEntry& out) { Result BuildContentEntry(const NsApplicationContentMetaStatus& status, ContentInfoEntry& out, bool to_nsz) {
NcmMetaData meta; NcmMetaData meta;
R_TRY(GetNcmMetaFromMetaStatus(status, meta)); R_TRY(GetNcmMetaFromMetaStatus(status, meta));
@@ -824,14 +888,14 @@ Result BuildContentEntry(const NsApplicationContentMetaStatus& status, ContentIn
R_SUCCEED(); 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.application_name = e.GetName();
out.path = BuildNspPath(e, info.status); out.path = BuildNspPath(e, info.status, to_nsz);
s64 offset{}; s64 offset{};
for (auto& e : info.content_infos) { for (auto& e : info.content_infos) {
char nca_name[64]; 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; u64 size;
ncmContentInfoSizeToU64(std::addressof(e), std::addressof(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())); 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]; 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]; 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()); out.collections.emplace_back(tik_name, offset, entry.tik_data.size());
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(); 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); LoadControlEntry(e);
keys::Keys keys; keys::Keys keys;
@@ -887,7 +951,7 @@ Result BuildNspEntries(Entry& e, const title::MetaEntries& meta_entries, std::ve
R_TRY(BuildContentEntry(status, info)); R_TRY(BuildContentEntry(status, info));
NspEntry nsp; 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; out.emplace_back(nsp).icon = e.image;
} }
@@ -895,21 +959,36 @@ Result BuildNspEntries(Entry& e, const title::MetaEntries& meta_entries, std::ve
R_SUCCEED(); 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; title::MetaEntries meta_entries;
R_TRY(GetMetaEntries(e, meta_entries, flags)); 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; std::vector<fs::FsPath> paths;
for (auto& e : entries) { for (auto& e : entries) {
if (to_nsz) {
paths.emplace_back(fs::AppendPath("/dumps/NSZ", e.path));
} else {
paths.emplace_back(fs::AppendPath("/dumps/NSP", e.path)); paths.emplace_back(fs::AppendPath("/dumps/NSP", e.path));
} }
}
auto source = std::make_shared<NspSource>(entries); auto source = std::make_shared<NspSource>(entries);
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); dump::Dump(source, paths);
} }
}
} // namespace sphaira::ui::menu::game } // 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()) { if (!m_entries.empty()) {
options->Add<SidebarEntryCallback>("Export NSP"_i18n, [this](){ 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](){ options->Add<SidebarEntryCallback>("Export options"_i18n, [this](){
@@ -153,7 +157,7 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
if (touch && m_index == i) { if (touch && m_index == i) {
FireAction(Button::A); FireAction(Button::A);
} else { } else {
App::PlaySoundEffect(SoundEffect_Focus); App::PlaySoundEffect(SoundEffect::Focus);
SetIndex(i); SetIndex(i);
} }
}); });
@@ -345,14 +349,14 @@ Result Menu::GetNcmSizeOfMetaStatus(MetaEntry& entry) const {
R_SUCCEED(); R_SUCCEED();
} }
void Menu::DumpGames() { void Menu::DumpGames(bool to_nsz) {
const auto entries = GetSelectedEntries(); const auto entries = GetSelectedEntries();
App::PopToMenu(); App::PopToMenu();
std::vector<NspEntry> nsps; 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() { void Menu::DeleteGames() {

View File

@@ -11,6 +11,9 @@
#include "yati/nx/keys.hpp" #include "yati/nx/keys.hpp"
#include "yati/nx/crypto.hpp" #include "yati/nx/crypto.hpp"
#include "utils/utils.hpp"
#include "utils/devoptab.hpp"
#include "title_info.hpp" #include "title_info.hpp"
#include "app.hpp" #include "app.hpp"
#include "dumper.hpp" #include "dumper.hpp"
@@ -26,18 +29,6 @@
namespace sphaira::ui::menu::game::meta_nca { namespace sphaira::ui::menu::game::meta_nca {
namespace { 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 { struct NcaHashSource final : hash::BaseSource {
NcaHashSource(NcmContentStorage* cs, const NcaEntry& entry) : m_cs{cs}, m_entry{entry} { 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 { 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){ 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); 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 { auto GetName(const std::string& path) const -> std::string {
const auto it = std::ranges::find_if(m_entries, [&path](auto& e){ 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()) { if (it != m_entries.end()) {
return hexIdToStr(it->content_id).str; return utils::hexIdToStr(it->content_id).str;
} }
return {}; return {};
@@ -97,7 +88,7 @@ struct NcaSource final : dump::BaseSource {
auto GetSize(const std::string& path) const -> s64 { auto GetSize(const std::string& path) const -> s64 {
const auto it = std::ranges::find_if(m_entries, [&path](auto& e){ 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()) { 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](){ std::make_pair(Button::A, Action{"Mount Fs"_i18n, [this](){
// todo: handle error here. // todo: handle error here.
if (!m_entries.empty() && !GetEntry().missing) { 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](){ std::make_pair(Button::B, Action{"Back"_i18n, [this](){
@@ -187,16 +179,23 @@ Menu::Menu(Entry& entry, const meta::MetaEntry& meta_entry)
DumpNcas(); 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](){ options->Add<SidebarEntryCallback>("Verify NCA 256 hash"_i18n, [this](){
static std::string hash_out; static std::string hash_out;
hash_out.clear(); 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()); auto source = std::make_unique<NcaHashSource>(m_meta.cs, GetEntry());
return hash::Hash(pbox, hash::Type::Sha256, source.get(), hash_out); return hash::Hash(pbox, hash::Type::Sha256, source.get(), hash_out);
}, [this](Result rc){ }, [this](Result rc){
App::PushErrorBox(rc, "Failed to hash file..."_i18n); 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 (R_SUCCEEDED(rc)) {
if (std::strncmp(hash_out.c_str(), str.str, std::strlen(str.str))) { 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); parse_keys(keys, false);
if (R_FAILED(GetNcmMetaFromMetaStatus(m_meta_entry.status, m_meta))) { if (R_FAILED(GetNcmMetaFromMetaStatus(m_meta_entry.status, m_meta))) {
log_write("[NCA-MENU] failed to GetNcmMetaFromMetaStatus()\n");
SetPop(); SetPop();
return; return;
} }
@@ -234,6 +234,7 @@ Menu::Menu(Entry& entry, const meta::MetaEntry& meta_entry)
// get the content meta header. // get the content meta header.
ncm::ContentMeta content_meta; ncm::ContentMeta content_meta;
if (R_FAILED(ncm::GetContentMeta(m_meta.db, &m_meta.key, 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(); SetPop();
return; return;
} }
@@ -241,6 +242,7 @@ Menu::Menu(Entry& entry, const meta::MetaEntry& meta_entry)
// fetch all the content infos. // fetch all the content infos.
std::vector<NcmContentInfo> infos; std::vector<NcmContentInfo> infos;
if (R_FAILED(ncm::GetContentInfos(m_meta.db, &m_meta.key, content_meta.header, 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(); SetPop();
return; return;
} }
@@ -252,12 +254,17 @@ Menu::Menu(Entry& entry, const meta::MetaEntry& meta_entry)
ncmContentInfoSizeToU64(&info, &entry.size); ncmContentInfoSizeToU64(&info, &entry.size);
bool has = false; 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; entry.missing = !has;
if (has && R_SUCCEEDED(ncmContentStorageReadContentIdFile(m_meta.cs, &entry.header, sizeof(entry.header), &info.content_id, 0))) {
// decrypt header. // decrypt header.
if (has && R_SUCCEEDED(ncmContentStorageReadContentIdFile(m_meta.cs, &entry.header, sizeof(entry.header), &info.content_id, 0))) {
log_write("[NCA-MENU] reading to decrypt header\n");
crypto::cryptoAes128Xts(&entry.header, &entry.header, keys.header_key, 0, 0x200, sizeof(entry.header), false); 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); m_entries.emplace_back(entry);
@@ -283,7 +290,7 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
if (touch && m_index == i) { if (touch && m_index == i) {
FireAction(Button::A); FireAction(Button::A);
} else { } else {
App::PlaySoundEffect(SoundEffect_Focus); App::PlaySoundEffect(SoundEffect::Focus);
SetIndex(i); 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, 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) { 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); 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; std::vector<fs::FsPath> paths;
for (auto& e : entries) { for (auto& e : entries) {
char nca_name[64]; 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; 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); 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,15 +400,24 @@ void Menu::DumpNcas() {
} }
auto source = std::make_shared<NcaSource>(m_meta.cs, m_entry.image, entries); 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() { Result Menu::MountNcaFs() {
const auto& e = GetEntry(); const auto& e = GetEntry();
// mount using devoptab instead if fails.
FsFileSystemType type; 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));
auto fs = std::make_shared<filebrowser::FsStdioWrapper>(root, [root](){
devoptab::UmountNca(root);
});
filebrowser::MountFsHelper(fs, utils::hexIdToStr(e.content_id).str);
} else {
// get fs path from ncm. // get fs path from ncm.
u64 program_id; u64 program_id;
fs::FsPath path; fs::FsPath path;
@@ -411,7 +427,9 @@ Result Menu::MountNcaFs() {
auto fs = std::make_shared<fs::FsNativeId>(program_id, type, path); auto fs = std::make_shared<fs::FsNativeId>(program_id, type, path);
R_TRY(fs->GetFsOpenResult()); R_TRY(fs->GetFsOpenResult());
filebrowser::MountFsHelper(fs, hexIdToStr(e.content_id).str); filebrowser::MountFsHelper(fs, utils::hexIdToStr(e.content_id).str);
}
R_SUCCEED(); R_SUCCEED();
} }

View File

@@ -8,6 +8,11 @@
#include "yati/yati.hpp" #include "yati/yati.hpp"
#include "yati/nx/nca.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 "app.hpp"
#include "defines.hpp" #include "defines.hpp"
@@ -17,16 +22,39 @@
#include "dumper.hpp" #include "dumper.hpp"
#include "image.hpp" #include "image.hpp"
#include "title_info.hpp" #include "title_info.hpp"
#include "threaded_file_transfer.hpp"
#include <cstring> #include <cstring>
#include <algorithm> #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 sphaira::ui::menu::gc {
namespace { namespace {
constexpr u32 XCI_MAGIC = std::byteswap(0x48454144); constexpr u32 XCI_MAGIC = std::byteswap(0x48454144);
constexpr u32 REMOUNT_ATTEMPT_MAX = 8; // same as nxdumptool. 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 { enum DumpFileType {
DumpFileType_XCI, DumpFileType_XCI,
@@ -35,6 +63,7 @@ enum DumpFileType {
DumpFileType_UID, DumpFileType_UID,
DumpFileType_Cert, DumpFileType_Cert,
DumpFileType_Initial, DumpFileType_Initial,
DumpFileType_XCZ,
}; };
enum DumpFileFlag { enum DumpFileFlag {
@@ -50,9 +79,9 @@ enum DumpFileFlag {
const char *g_option_list[] = { const char *g_option_list[] = {
"Install", "Install",
"Export", "Export XCI (Gamecard)",
"Mount", "Export XCZ (Compressed XCI)",
"Exit", "Mount Fs",
}; };
auto GetXciSizeFromRomSize(u8 rom_size) -> s64 { 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_UID: return " (Card UID).bin";
case DumpFileType_Cert: return " (Certificate).bin"; case DumpFileType_Cert: return " (Certificate).bin";
case DumpFileType_Initial: return " (Initial Data).bin"; case DumpFileType_Initial: return " (Initial Data).bin";
case DumpFileType_XCZ: return ".xcz";
} }
return ""; return "";
@@ -128,6 +158,10 @@ auto BuildFullDumpPath(DumpFileType type, std::span<const ApplicationEntry> entr
const auto base_path = BuildXciBasePath(entries); const auto base_path = BuildXciBasePath(entries);
fs::FsPath out; fs::FsPath out;
if (type == DumpFileType_XCZ) {
out = base_path + GetDumpTypeStr(type);
return fs::AppendPath(DUMP_XCZ_BASE_PATH, out);
} else {
if (use_folder) { if (use_folder) {
if (App::GetApp()->m_dump_append_folder_with_xci.Get()) { if (App::GetApp()->m_dump_append_folder_with_xci.Get()) {
out = base_path + ".xci/" + base_path + GetDumpTypeStr(type); out = base_path + ".xci/" + base_path + GetDumpTypeStr(type);
@@ -138,12 +172,13 @@ auto BuildFullDumpPath(DumpFileType type, std::span<const ApplicationEntry> entr
out = base_path + GetDumpTypeStr(type); 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 { auto BuildFullDumpPath(DumpFileType type, std::span<const ApplicationEntry> entries) -> fs::FsPath {
// check if the base path is too long. // 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(); auto use_folder = App::GetApp()->m_dump_app_folder.Get();
for (;;) { 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 // @Gc is the mount point, S is for secure partion, the remaining is the
// the gamecard handle value in lower-case hex. // the gamecard handle value in lower-case hex.
auto BuildGcPath(const char* name, const FsGameCardHandle* handle, FsGameCardPartition partiton = FsGameCardPartition_Secure) -> fs::FsPath { 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_Update] = 'U',
[FsGameCardPartition_Normal] = 'N', [FsGameCardPartition_Normal] = 'N',
[FsGameCardPartition_Secure] = 'S', [FsGameCardPartition_Secure] = 'S',
@@ -182,7 +217,7 @@ auto BuildGcPath(const char* name, const FsGameCardHandle* handle, FsGameCardPar
}; };
fs::FsPath path; 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; return path;
} }
@@ -200,7 +235,7 @@ struct XciSource final : dump::BaseSource {
int icon{}; int icon{};
Result Read(const std::string& path, void* buf, s64 off, s64 size, u64* bytes_read) override { 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); size = ClipSize(off, size, xci_size);
*bytes_read = size; *bytes_read = size;
return menu->GcStorageRead(buf, off, 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 { 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; return xci_size;
} else if (path.ends_with(GetDumpTypeStr(DumpFileType_Set))) { } else if (path.ends_with(GetDumpTypeStr(DumpFileType_Set))) {
return id_set.size(); return id_set.size();
@@ -259,33 +294,175 @@ private:
} }
}; };
struct HashStr { struct Test final : yati::source::Base {
char str[0x21]; 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) { struct NcaReader final : yati::source::Base {
HashStr str{}; NcaReader(Test* source, s64 offset) : m_source{source}, m_offset{offset} {
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;
} }
// from Gamecard-Installer-NX Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override {
Result fsOpenGameCardStorage(FsStorage* out, const FsGameCardHandle* handle, FsGameCardPartitionRaw partition) { return m_source->Read(buf, m_offset + off, size, bytes_read);
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) { private:
return serviceDispatch(fsGetServiceSession(), 501, Test* m_source;
.out_num_objects = 1, const s64 m_offset;
.out_objects = &out->s };
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 { struct GcSource final : yati::source::Base {
@@ -401,9 +578,6 @@ auto ApplicationEntry::GetSize() const -> s64 {
Menu::Menu(u32 flags) : MenuBase{"GameCard"_i18n, flags} { Menu::Menu(u32 flags) : MenuBase{"GameCard"_i18n, flags} {
this->SetActions( this->SetActions(
std::make_pair(Button::A, Action{"OK"_i18n, [this](){ std::make_pair(Button::A, Action{"OK"_i18n, [this](){
if (m_option_index == 3) {
SetPop();
} else {
if (!m_mounted) { if (!m_mounted) {
return; return;
} }
@@ -443,10 +617,11 @@ Menu::Menu(u32 flags) : MenuBase{"GameCard"_i18n, flags} {
add("Export Certificate"_i18n, DumpFileFlag_Cert); add("Export Certificate"_i18n, DumpFileFlag_Cert);
add("Export Initial Data"_i18n, DumpFileFlag_Initial); add("Export Initial Data"_i18n, DumpFileFlag_Initial);
} else if (m_option_index == 2) { } else if (m_option_index == 2) {
DumpXcz(0);
} else if (m_option_index == 3) {
const auto rc = MountGcFs(); const auto rc = MountGcFs();
App::PushErrorBox(rc, "Failed to mount GameCard filesystem"_i18n); App::PushErrorBox(rc, "Failed to mount GameCard filesystem"_i18n);
} }
}
}}), }}),
std::make_pair(Button::B, Action{"Back"_i18n, [this](){ std::make_pair(Button::B, Action{"Back"_i18n, [this](){
SetPop(); SetPop();
@@ -497,7 +672,7 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
if (touch && m_option_index == i) { if (touch && m_option_index == i) {
FireAction(Button::A); FireAction(Button::A);
} else { } else {
App::PlaySoundEffect(SoundEffect_Focus); App::PlaySoundEffect(SoundEffect::Focus);
m_option_index = i; 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)); gfx::drawRect(vg, 490, text_y - 45.f / 2.f, 2, 45, theme->GetColour(ThemeEntryID_TEXT_SELECTED));
colour = ThemeEntryID_TEXT_SELECTED; colour = ThemeEntryID_TEXT_SELECTED;
} }
if (i != 3 && !m_mounted) { if (!m_mounted) {
colour = ThemeEntryID_TEXT_INFO; colour = ThemeEntryID_TEXT_INFO;
} }
@@ -632,7 +807,7 @@ Result Menu::GcMount() {
} }
// find the nca file, this will never fail for gamecards, see above comment. // 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){ const auto it = std::find_if(buf.cbegin(), buf.cend(), [str](auto& e){
return !std::strncmp(str.str, e.name, std::strlen(str.str)); 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); 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_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(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_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; m_storage_mounted = true;
log_write("[GC] m_storage_trimmed_size: %zd\n", m_storage_trimmed_size); 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) { Result Menu::GcStorageReadInternal(void* buf, s64 off, s64 size, u64* bytes_read) {
if (off < m_parition_normal_size) { if (off < m_partition_normal_size) {
size = std::min<s64>(size, m_parition_normal_size - off); size = std::min<s64>(size, m_partition_normal_size - off);
R_TRY(GcMountPartition(FsGameCardPartitionRaw_Normal)); R_TRY(GcMountPartition(FsGameCardPartitionRaw_Normal));
} else { } else {
off = off - m_parition_normal_size; off = off - m_partition_normal_size;
R_TRY(GcMountPartition(FsGameCardPartitionRaw_Secure)); R_TRY(GcMountPartition(FsGameCardPartitionRaw_Secure));
} }
@@ -833,9 +1008,7 @@ Result Menu::GcStorageRead(void* _buf, s64 off, s64 size) {
if (unaligned_size) { if (unaligned_size) {
R_TRY(GcStorageReadInternal(data, off, sizeof(data), &bytes_read)); R_TRY(GcStorageReadInternal(data, off, sizeof(data), &bytes_read));
std::memcpy(buf, data, unaligned_size);
const auto csize = std::min<s64>(size, 0x200 - unaligned_size);
std::memcpy(buf, data + unaligned_size, csize);
} }
R_SUCCEED(); R_SUCCEED();
@@ -867,7 +1040,7 @@ Result Menu::GcOnEvent(bool force) {
log_write("trying to mount\n"); log_write("trying to mount\n");
m_mounted = R_SUCCEEDED(GcMount()); m_mounted = R_SUCCEEDED(GcMount());
if (m_mounted) { if (m_mounted) {
App::PlaySoundEffect(SoundEffect::SoundEffect_Startup); App::PlaySoundEffect(SoundEffect::Startup);
} }
} else { } else {
log_write("trying to unmount\n"); 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) { Result Menu::DumpGames(u32 flags) {
// first, try and mount the storage. // first, try and mount the storage.
// this will fill out the xci header, verify and get sizes. // 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)); 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(); R_SUCCEED();
}; };
@@ -1105,6 +1312,23 @@ Result Menu::GcGetSecurityInfo(GameCardSecurityInformation& out) {
} }
Result Menu::MountGcFs() { 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]; const auto& e = m_entries[m_entry_index];
auto fs = std::make_shared<fs::FsNative>(&m_fs->m_fs, false); 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, "/"); App::Push<filebrowser::Menu>(fs, fs_entry, "/");
#endif
R_SUCCEED(); R_SUCCEED();
} }

View File

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

View File

@@ -1,13 +1,19 @@
#include "app.hpp" #include "app.hpp"
#include "log.hpp" #include "log.hpp"
#include "fs.hpp" #include "fs.hpp"
#include "ui/menus/homebrew.hpp" #include "ui/menus/homebrew.hpp"
#include "ui/menus/filebrowser.hpp" #include "ui/menus/filebrowser.hpp"
#include "ui/sidebar.hpp" #include "ui/sidebar.hpp"
#include "ui/error_box.hpp" #include "ui/error_box.hpp"
#include "ui/option_box.hpp" #include "ui/option_box.hpp"
#include "ui/progress_box.hpp" #include "ui/progress_box.hpp"
#include "ui/nvg_util.hpp" #include "ui/nvg_util.hpp"
#include "utils/devoptab.hpp"
#include "utils/profile.hpp"
#include "owo.hpp" #include "owo.hpp"
#include "defines.hpp" #include "defines.hpp"
#include "i18n.hpp" #include "i18n.hpp"
@@ -84,7 +90,7 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
if (touch && m_index == i) { if (touch && m_index == i) {
FireAction(Button::A); FireAction(Button::A);
} else { } else {
App::PlaySoundEffect(SoundEffect_Focus); App::PlaySoundEffect(SoundEffect::Focus);
SetIndex(i); SetIndex(i);
} }
}); });
@@ -192,10 +198,11 @@ void Menu::InstallHomebrew() {
} }
void Menu::ScanHomebrew() { void Menu::ScanHomebrew() {
TimeStamp ts;
FreeEntries(); FreeEntries();
{
SCOPED_TIMESTAMP("nro scan");
nro_scan("/switch", m_entries); nro_scan("/switch", m_entries);
log_write("nros found: %zu time_taken: %.2f\n", m_entries.size(), ts.GetSecondsD()); }
struct IniUser { struct IniUser {
std::vector<NroEntry>& entires; std::vector<NroEntry>& entires;
@@ -458,6 +465,13 @@ void Menu::DisplayOptions() {
}, "Shows all hidden homebrew."_i18n); }, "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 (!m_entries_current.empty()) {
#if 0 #if 0
options->Add<SidebarEntryCallback>("Info"_i18n, [this](){ options->Add<SidebarEntryCallback>("Info"_i18n, [this](){
@@ -472,14 +486,10 @@ void Menu::DisplayOptions() {
}, "Hides the selected homebrew.\n\n" }, "Hides the selected homebrew.\n\n"
"To unhide homebrew, enable \"Show hidden\" in the sort options."_i18n); "To unhide homebrew, enable \"Show hidden\" in the sort options."_i18n);
auto mount_option = options->Add<SidebarEntryCallback>("Mount RomFS"_i18n, [this](){ options->Add<SidebarEntryCallback>("Mount NRO Fs"_i18n, [this](){
const auto rc = MountRomfsFs(); const auto rc = MountNroFs();
App::PushErrorBox(rc, "Failed to mount NRO RomFS"_i18n); App::PushErrorBox(rc, "Failed to mount NRO FileSystem"_i18n);
}, "Mounts the homebrew RomFS"_i18n); }, "Mounts the NRO FileSystem (icon, nacp and RomFS)."_i18n);
mount_option->Depends([this](){
return GetEntry().romfs_offset && GetEntry().romfs_size;
}, "This homebrew does not have a RomFS"_i18n);
options->Add<SidebarEntryCallback>("Delete"_i18n, [this](){ options->Add<SidebarEntryCallback>("Delete"_i18n, [this](){
const auto buf = "Are you sure you want to delete "_i18n + GetEntry().path.toString() + "?"; 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 { Result Menu::MountNroFs() {
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:/";
const auto& e = GetEntry(); const auto& e = GetEntry();
// todo: add errors for when nro doesn't have romfs. fs::FsPath root;
R_UNLESS(e.romfs_offset, 0x1); R_TRY(devoptab::MountNro(App::GetApp()->m_fs.get(), e.path, root));
R_UNLESS(e.romfs_size, 0x1);
FsFile file; auto fs = std::make_shared<filebrowser::FsStdioWrapper>(root, [root](){
R_TRY(fsFsOpenFile(fsdevGetDeviceFileSystem("sdmc"), e.path, FsOpenMode_Read, &file)); devoptab::UmountNro(root);
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);
}); });
filebrowser::MountFsHelper(fs, e.GetName()); filebrowser::MountFsHelper(fs, root);
R_SUCCEED(); 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 "ui/menus/install_stream_menu_base.hpp"
#include "yati/yati.hpp" #include "yati/yati.hpp"
#include "app.hpp" #include "app.hpp"
@@ -275,5 +273,3 @@ void Menu::OnInstallClose() {
} }
} // namespace sphaira::ui::menu::stream } // 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"\ "You can backup and restore saves.\n\n"\
"Experimental support for backing up system saves is possible." }, "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 = { .name = "Themezer", .title = "Themezer", .func = MiscMenuFuncGenerator<ui::menu::themezer::Menu>, .flag = MiscMenuFlag_Shortcut, .info =
"Download themes from themezer.net. "\ "Download themes from themezer.net. "\
"Themes are downloaded to /themes/sphaira\n"\ "Themes are downloaded to /themes/sphaira\n"\
"To install the themes, NXThemesInstaller needs to be installed (can be downloaded via the AppStore)." }, "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 = { .name = "GitHub", .title = "GitHub", .func = MiscMenuFuncGenerator<ui::menu::gh::Menu>, .flag = MiscMenuFlag_Shortcut, .info =
"Download releases directly from GitHub. "\ "Download releases directly from GitHub. "\
"Custom entries can be added to /config/sphaira/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 = { .name = "FTP", .title = "FTP Install", .func = MiscMenuFuncGenerator<ui::menu::ftp::Menu>, .flag = MiscMenuFlag_Install, .info =
"Install apps via FTP.\n\n"\ "Install apps via FTP.\n\n"\
"NOTE: This feature does not always work, use at your own risk. "\ "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. "\ "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." }, "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 = { .name = "GameCard", .title = "GameCard", .func = MiscMenuFuncGenerator<ui::menu::gc::Menu>, .flag = MiscMenuFlag_Shortcut, .info =
"View info on the inserted Game Card (GC). "\ "View info on the inserted Game Card (GC). "\
"You can backup and install the inserted 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 "ui/menus/mtp_menu.hpp"
#include "usb/usbds.hpp" #include "usb/usbds.hpp"
#include "app.hpp" #include "app.hpp"
@@ -12,10 +10,10 @@
namespace sphaira::ui::menu::mtp { namespace sphaira::ui::menu::mtp {
Menu::Menu(u32 flags) : stream::Menu{"MTP Install"_i18n, flags} { 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) { if (!m_was_mtp_enabled) {
log_write("[MTP] wasn't enabled, forcefully enabling\n"); log_write("[MTP] wasn't enabled, forcefully enabling\n");
App::SetMtpEnable(true); haze::Init();
} }
haze::InitInstallMode( haze::InitInstallMode(
@@ -31,7 +29,7 @@ Menu::~Menu() {
if (!m_was_mtp_enabled) { if (!m_was_mtp_enabled) {
log_write("[MTP] disabling on exit\n"); log_write("[MTP] disabling on exit\n");
App::SetMtpEnable(false); haze::Exit();
} }
} }
@@ -59,5 +57,3 @@ void Menu::OnDisableInstallMode() {
} }
} // namespace sphaira::ui::menu::mtp } // 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; 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 // https://github.com/J-D-K/JKSV/issues/264#issuecomment-2618962807
struct NXSaveMeta { struct NXSaveMeta {
u32 magic{}; // NX_SAVE_META_MAGIC 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 { auto BuildSaveName(const Entry& e) -> fs::FsPath {
fs::FsPath name_buf = e.GetName(); fs::FsPath name_buf = e.GetName();
title::utilsReplaceIllegalCharacters(name_buf, true); title::utilsReplaceIllegalCharacters(name_buf, true);
@@ -389,7 +413,7 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
if (touch && m_index == i) { if (touch && m_index == i) {
FireAction(Button::A); FireAction(Button::A);
} else { } else {
App::PlaySoundEffect(SoundEffect_Focus); App::PlaySoundEffect(SoundEffect::Focus);
SetIndex(i); SetIndex(i);
} }
}); });
@@ -691,14 +715,9 @@ void Menu::DisplayOptions() {
} }
void Menu::BackupSaves(std::vector<std::reference_wrapper<Entry>>& entries) { 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 { App::Push<ProgressBox>(0, "Backup"_i18n, "", [this, entries, location](auto pbox) -> Result {
for (auto& e : entries) { return BackupSaveInternal(pbox, location, entries, m_compress_save_backup.Get());
// the entry may not have loaded yet.
LoadControlEntry(e);
R_TRY(BackupSaveInternal(pbox, location, e, m_compress_save_backup.Get()));
}
R_SUCCEED();
}, [](Result rc){ }, [](Result rc){
App::PushErrorBox(rc, "Backup failed!"_i18n); App::PushErrorBox(rc, "Backup failed!"_i18n);
@@ -936,16 +955,20 @@ Result Menu::RestoreSaveInternal(ProgressBox* pbox, const Entry& e, const fs::Fs
R_SUCCEED(); R_SUCCEED();
} }
Result Menu::BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& location, const Entry& e, bool compressed, bool is_auto) const { Result Menu::BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& location, std::span<const std::reference_wrapper<Entry>> entries, bool compressed, bool is_auto) const {
std::unique_ptr<fs::Fs> fs; std::vector<fs::FsPath> paths;
if (location.entry.type == dump::DumpLocationType_Stdio) { for (auto& e : entries) {
fs = std::make_unique<fs::FsStdio>(true, location.stdio[location.entry.index].mount); // ensure that we have title name and icon loaded.
} else if (location.entry.type == dump::DumpLocationType_SdCard) { LoadControlEntry(e);
fs = std::make_unique<fs::FsNativeSd>(); paths.emplace_back(BuildSavePath(e, is_auto));
} else {
std::unreachable();
} }
auto source = std::make_shared<DumpSource>(entries, paths);
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);
pbox->SetTitle(e.GetName()); pbox->SetTitle(e.GetName());
if (e.image) { if (e.image) {
pbox->SetImage(e.image); pbox->SetImage(e.image);
@@ -992,12 +1015,6 @@ Result Menu::BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& loc
zip_info_default.tmz_date.tm_mon = tm->tm_mon; zip_info_default.tmz_date.tm_mon = tm->tm_mon;
zip_info_default.tmz_date.tm_year = tm->tm_year; zip_info_default.tmz_date.tm_year = tm->tm_year;
const auto path = fs::AppendPath(fs->Root(), BuildSavePath(e, is_auto));
const auto temp_path = path + ".temp";
fs->CreateDirectoryRecursivelyWithPath(temp_path);
ON_SCOPE_EXIT(fs->DeleteFile(temp_path));
// zip to memory if less than 1GB and not applet mode. // zip to memory if less than 1GB and not applet mode.
// TODO: use my mmz code from ftpsrv to stream zip creation. // TODO: use my mmz code from ftpsrv to stream zip creation.
// this will allow for zipping to memory and flushing every X bytes // this will allow for zipping to memory and flushing every X bytes
@@ -1009,11 +1026,11 @@ Result Menu::BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& loc
if (!file_download) { if (!file_download) {
mz::FileFuncMem(&mz_mem, &file_func); mz::FileFuncMem(&mz_mem, &file_func);
} else { } else {
mz::FileFuncStdio(&file_func); dump::FileFuncWriter(writer, &file_func);
} }
{ {
auto zfile = zipOpen2_64(temp_path, APPEND_STATUS_CREATE, nullptr, &file_func); auto zfile = zipOpen2_64(path, APPEND_STATUS_CREATE, nullptr, &file_func);
R_UNLESS(zfile, Result_ZipOpen2_64); R_UNLESS(zfile, Result_ZipOpen2_64);
ON_SCOPE_EXIT(zipClose(zfile, "sphaira v" APP_VERSION_HASH)); ON_SCOPE_EXIT(zipClose(zfile, "sphaira v" APP_VERSION_HASH));
@@ -1073,13 +1090,9 @@ Result Menu::BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& loc
} }
// if we dumped the save to ram, flush the data to file. // if we dumped the save to ram, flush the data to file.
const auto is_file_based_emummc = App::IsFileBaseEmummc();
if (!file_download) { if (!file_download) {
pbox->NewTransfer("Flushing zip to file"); pbox->NewTransfer("Flushing zip to file");
R_TRY(fs->CreateFile(temp_path, mz_mem.buf.size(), 0)); R_TRY(writer->SetSize(mz_mem.buf.size()));
fs::File file;
R_TRY(fs->OpenFile(temp_path, FsOpenMode_Write, &file));
R_TRY(thread::Transfer(pbox, mz_mem.buf.size(), R_TRY(thread::Transfer(pbox, mz_mem.buf.size(),
[&](void* data, s64 off, s64 size, u64* bytes_read) -> Result { [&](void* data, s64 off, s64 size, u64* bytes_read) -> Result {
@@ -1089,19 +1102,20 @@ Result Menu::BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& loc
R_SUCCEED(); R_SUCCEED();
}, },
[&](const void* data, s64 off, s64 size) -> Result { [&](const void* data, s64 off, s64 size) -> Result {
const auto rc = file.Write(off, data, size, FsWriteOption_None); return writer->Write(data, off, size);
if (is_file_based_emummc) {
svcSleepThread(2e+6); // 2ms
}
return rc;
} }
)); ));
} }
fs->DeleteFile(path);
R_TRY(fs->RenameFile(temp_path, path));
R_SUCCEED(); 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() { Result Menu::MountSaveFs() {
@@ -1109,7 +1123,7 @@ Result Menu::MountSaveFs() {
if (e.system_save_data_id) { if (e.system_save_data_id) {
fs::FsPath root; 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](){ auto fs = std::make_shared<filebrowser::FsStdioWrapper>(root, [&e](){
devoptab::UnmountSave(e.system_save_data_id); devoptab::UnmountSave(e.system_save_data_id);

View File

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

View File

@@ -1,5 +1,3 @@
#if ENABLE_NETWORK_INSTALL
#include "ui/menus/usb_menu.hpp" #include "ui/menus/usb_menu.hpp"
#include "yati/yati.hpp" #include "yati/yati.hpp"
#include "app.hpp" #include "app.hpp"
@@ -7,14 +5,18 @@
#include "log.hpp" #include "log.hpp"
#include "ui/nvg_util.hpp" #include "ui/nvg_util.hpp"
#include "i18n.hpp" #include "i18n.hpp"
#include "haze_helper.hpp"
#include "utils/thread.hpp"
#include <cstring> #include <cstring>
namespace sphaira::ui::menu::usb { namespace sphaira::ui::menu::usb {
namespace { namespace {
constexpr u64 CONNECTION_TIMEOUT = UINT64_MAX; constexpr u64 CONNECTION_TIMEOUT = 3e+9;
constexpr u64 TRANSFER_TIMEOUT = UINT64_MAX; constexpr u64 TRANSFER_TIMEOUT = 3e+9;
constexpr u64 FINISHED_TIMEOUT = 1e+9 * 3; // 3 seconds. constexpr u64 FINISHED_TIMEOUT = 3e+9; // 3 seconds.
void thread_func(void* user) { void thread_func(void* user) {
auto app = static_cast<Menu*>(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. // 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) { if (m_was_mtp_enabled) {
App::Notify("Disable MTP for usb install"_i18n); App::Notify("Disable MTP for usb install"_i18n);
App::SetMtpEnable(false); haze::Exit();
} }
// 3 second timeout for transfers. // 3 second timeout for transfers.
@@ -47,15 +49,18 @@ Menu::Menu(u32 flags) : MenuBase{"USB"_i18n, flags} {
} }
if (m_state != State::Failed) { 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); threadStart(&m_thread);
} }
} }
Menu::~Menu() { Menu::~Menu() {
// signal for thread to exit and wait. // signal for thread to exit and wait.
m_stop_source.request_stop(); if (R_FAILED(waitSingleHandle(m_thread.handle, 0))) {
m_usb_source->SignalCancel(); m_usb_source->SignalCancel();
m_stop_source.request_stop();
}
threadWaitForExit(&m_thread); threadWaitForExit(&m_thread);
threadClose(&m_thread); threadClose(&m_thread);
@@ -65,7 +70,7 @@ Menu::~Menu() {
if (m_was_mtp_enabled) { if (m_was_mtp_enabled) {
App::Notify("Re-enabled MTP"_i18n); App::Notify("Re-enabled MTP"_i18n);
App::SetMtpEnable(true); haze::Init();
} }
} }
@@ -92,7 +97,13 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
m_state = State::Progress; m_state = State::Progress;
log_write("got connection\n"); log_write("got connection\n");
App::Push<ui::ProgressBox>(0, "Installing "_i18n, "", [this](auto pbox) -> Result { App::Push<ui::ProgressBox>(0, "Installing "_i18n, "", [this](auto pbox) -> Result {
ON_SCOPE_EXIT(m_usb_source->Finished(FINISHED_TIMEOUT)); log_write("inside progress box\n");
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. // if we are doing s2s install, skip verifying the nca contents.
yati::ConfigOverride config_override{}; yati::ConfigOverride config_override{};
@@ -102,10 +113,7 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
config_override.skip_rsa_npdm_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) {
pbox->SetTitle(file_name); pbox->SetTitle(file_name);
m_usb_source->SetFileNameForTranfser(file_name);
const auto rc = yati::InstallFromSource(pbox, m_usb_source.get(), file_name, config_override); const auto rc = yati::InstallFromSource(pbox, m_usb_source.get(), file_name, config_override);
if (R_FAILED(rc)) { if (R_FAILED(rc)) {
m_usb_source->SignalCancel(); m_usb_source->SignalCancel();
@@ -191,5 +199,3 @@ void Menu::ThreadFunction() {
} }
} // namespace sphaira::ui::menu::usb } // 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 "log.hpp"
#include "threaded_file_transfer.hpp" #include "threaded_file_transfer.hpp"
#include "i18n.hpp" #include "i18n.hpp"
#include "utils/thread.hpp"
#include <cstring> #include <cstring>
#include <cmath>
namespace sphaira::ui { namespace sphaira::ui {
namespace { namespace {
@@ -17,13 +21,44 @@ void threadFunc(void* arg) {
d->pbox->RequestExit(); 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 } // 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_done{done}
, m_action{action} , m_action{action}
, m_title{title} , m_title{title}
, m_image{image} { , m_image{image} {
App::SetAutoSleepDisabled(true);
if (App::GetApp()->m_progress_boost_mode.Get()) { if (App::GetApp()->m_progress_boost_mode.Get()) {
App::SetBoostMode(true); App::SetBoostMode(true);
} }
@@ -45,10 +80,9 @@ ProgressBox::ProgressBox(int image, const std::string& action, const std::string
// create cancel event. // create cancel event.
ueventCreate(&m_uevent, false); ueventCreate(&m_uevent, false);
m_cpuid = cpuid;
m_thread_data.pbox = this; m_thread_data.pbox = this;
m_thread_data.callback = callback; 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"); log_write("failed to create thead\n");
} }
if (R_FAILED(threadStart(&m_thread))) { if (R_FAILED(threadStart(&m_thread))) {
@@ -68,9 +102,12 @@ ProgressBox::~ProgressBox() {
} }
FreeImage(); FreeImage();
if (m_done) {
m_done(m_thread_data.result); m_done(m_thread_data.result);
}
App::SetBoostMode(false); App::SetBoostMode(false);
App::SetAutoSleepDisabled(false);
} }
auto ProgressBox::Update(Controller* controller, TouchInfo* touch) -> void { 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 center_x = m_pos.x + m_pos.w/2;
const auto end_y = m_pos.y + m_pos.h; const auto end_y = m_pos.y + m_pos.h;
const auto progress_bar_w = m_pos.w - 230; 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); nvgSave(vg);
nvgIntersectScissor(vg, GetX(), GetY(), GetW(), GetH()); 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); gfx::drawRect(vg, prog_bar, theme->GetColour(ThemeEntryID_PROGRESSBAR_BACKGROUND), rounding);
const u32 percentage = ((double)offset / (double)size) * 100.0; 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::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_mb = (double)speed / (1024.0 * 1024.0);
const double speed_kb = (double)speed / (1024.0); const double speed_kb = (double)speed / (1024.0);
@@ -219,6 +259,17 @@ auto ProgressBox::NewTransfer(const std::string& transfer) -> ProgressBox& {
return *this; 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& { auto ProgressBox::UpdateTransfer(s64 offset, s64 size) -> ProgressBox& {
mutexLock(&m_mutex); mutexLock(&m_mutex);
m_size = size; 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_y_off -= m_step;
m_index++; m_index++;
App::PlaySoundEffect(SoundEffect_Scroll); App::PlaySoundEffect(SoundEffect::Scroll);
}}), }}),
std::make_pair(Button::LS_UP, Action{[this](){ std::make_pair(Button::LS_UP, Action{[this](){
if (m_y_off == m_y_off_base) { 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_y_off += m_step;
m_index--; 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); 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) SidebarEntryCallback::SidebarEntryCallback(const std::string& title, const Callback& cb, bool pop_on_click, const std::string& info)
: SidebarEntryBase{title, info} : SidebarEntryBase{title, info}
, m_callback{cb} , m_callback{cb}
@@ -422,6 +468,13 @@ void Sidebar::SetupButtons() {
SetPop(); 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 } // namespace sphaira::ui

View File

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

View File

@@ -16,8 +16,6 @@
// The USB transfer code was taken from Haze (part of Atmosphere). // The USB transfer code was taken from Haze (part of Atmosphere).
#if ENABLE_NETWORK_INSTALL
#include "usb/base.hpp" #include "usb/base.hpp"
#include "log.hpp" #include "log.hpp"
#include "defines.hpp" #include "defines.hpp"
@@ -100,5 +98,3 @@ Result Base::TransferAll(bool read, void *data, u32 size, u64 timeout) {
} }
} // namespace sphaira::usb } // 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/usb_uploader.hpp"
#include "usb/tinfoil.hpp" #include "usb/usb_api.hpp"
#include "log.hpp" #include "log.hpp"
#include "defines.hpp" #include "defines.hpp"
namespace sphaira::usb::upload { namespace sphaira::usb::upload {
namespace { namespace {
namespace tinfoil = usb::tinfoil;
const UsbHsInterfaceFilter FILTER{ const UsbHsInterfaceFilter FILTER{
.Flags = UsbHsInterfaceFilterFlags_idVendor | .Flags = UsbHsInterfaceFilterFlags_idVendor |
UsbHsInterfaceFilterFlags_idProduct | UsbHsInterfaceFilterFlags_idProduct |
@@ -36,6 +31,8 @@ const UsbHsInterfaceFilter FILTER{
constexpr u8 INDEX = 0; constexpr u8 INDEX = 0;
using namespace usb::api;
} // namespace } // namespace
Usb::Usb(u64 transfer_timeout) { Usb::Usb(u64 transfer_timeout) {
@@ -46,69 +43,90 @@ Usb::Usb(u64 transfer_timeout) {
Usb::~Usb() { 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)); R_TRY(m_usb->IsUsbConnected(timeout));
// build name table.
std::string names_list; std::string names_list;
for (auto& name : names) { for (auto& name : names) {
names_list += name + '\n'; names_list += name + '\n';
} }
tinfoil::TUSHeader header{}; // send.
header.magic = tinfoil::Magic_List0; SendHeader send_header;
header.nspListSize = names_list.length(); R_TRY(m_usb->TransferAll(true, &send_header, sizeof(send_header), timeout));
header.flags = flags; 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_TRY(m_usb->TransferAll(false, names_list.data(), names_list.length(), timeout));
R_SUCCEED(); R_SUCCEED();
} }
Result Usb::PollCommands() { Result Usb::PollCommands() {
tinfoil::USBCmdHeader header; SendHeader send_header;
R_TRY(m_usb->TransferAll(true, &header, sizeof(header))); R_TRY(m_usb->TransferAll(true, &send_header, sizeof(send_header)));
R_UNLESS(header.magic == tinfoil::Magic_Command0, Result_UsbUploadBadMagic); 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); R_THROW(Result_UsbUploadExit);
} else if (header.cmdId == tinfoil::USBCmdId::FILE_RANGE) { } else if (send_header.cmd == CMD_OPEN) {
return FileRangeCmd(header.dataSize); 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 { } else {
R_TRY(SendResult(RESULT_ERROR));
R_THROW(Result_UsbUploadBadCommand); R_THROW(Result_UsbUploadBadCommand);
} }
} }
Result Usb::FileRangeCmd(u64 data_size) { Result Usb::file_transfer_loop() {
tinfoil::FileRangeCmdHeader header; log_write("doing file transfer\n");
R_TRY(m_usb->TransferAll(true, &header, sizeof(header)));
std::string path(header.nspNameLen, '\0'); // get offset + size.
R_TRY(m_usb->TransferAll(true, path.data(), header.nspNameLen)); SendDataHeader send_header;
R_TRY(m_usb->TransferAll(true, &send_header, sizeof(send_header)));
// send response header. // check if we should finish now.
R_TRY(m_usb->TransferAll(false, &header, sizeof(header))); if (send_header.offset == 0 && send_header.size == 0) {
log_write("finished\n");
s64 curr_off = 0x0; R_TRY(SendResult(RESULT_OK));
s64 end_off = header.size; return Result_UsbUploadExit;
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;
} }
// read file and calculate the hash.
u64 bytes_read; u64 bytes_read;
R_TRY(Read(path, m_buf.data(), header.offset + curr_off, read_size, &bytes_read)); m_buf.resize(send_header.size);
R_TRY(m_usb->TransferAll(false, m_buf.data(), bytes_read)); log_write("reading buffer: %zu\n", m_buf.size());
curr_off += bytes_read;
} 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(); 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 "usb/usbds.hpp"
#include "log.hpp" #include "log.hpp"
#include "defines.hpp" #include "defines.hpp"
#include <ranges> #include <ranges>
#include <cstring> #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* { auto GetUsbDsStateStr(UsbState state) -> const char* {
switch (state) { switch (state) {
case UsbState_Detached: return "Detached"; case UsbState_Detached: return "Detached";
@@ -30,8 +19,7 @@ auto GetUsbDsStateStr(UsbState state) -> const char* {
} }
auto GetUsbDsSpeedStr(UsbDeviceSpeed speed) -> const char* { auto GetUsbDsSpeedStr(UsbDeviceSpeed speed) -> const char* {
// todo: remove this cast when libnx pr is merged. switch (speed) {
switch ((u32)speed) {
case UsbDeviceSpeed_None: return "None"; case UsbDeviceSpeed_None: return "None";
case UsbDeviceSpeed_Low: return "USB 1.0 Low Speed"; case UsbDeviceSpeed_Low: return "USB 1.0 Low Speed";
case UsbDeviceSpeed_Full: return "USB 1.1 Full 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(usbDsInterface_EnableInterface(m_interface));
R_TRY(usbDsEnable()); 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"); log_write("success USB init\n");
R_SUCCEED(); 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) { 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); 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 } // namespace sphaira::usb
#endif

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