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:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -25,3 +25,5 @@ compile_commands.json
|
||||
out
|
||||
|
||||
usb_test/
|
||||
__pycache__
|
||||
usb_*.spec
|
||||
|
||||
@@ -18,17 +18,6 @@
|
||||
"inherits":["core"],
|
||||
"cacheVariables": {
|
||||
"CMAKE_BUILD_TYPE": "MinSizeRel",
|
||||
"ENABLE_NETWORK_INSTALL": false,
|
||||
"LTO": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ReleaseWithInstall",
|
||||
"displayName": "ReleaseWithInstall",
|
||||
"inherits":["core"],
|
||||
"cacheVariables": {
|
||||
"CMAKE_BUILD_TYPE": "MinSizeRel",
|
||||
"ENABLE_NETWORK_INSTALL": true,
|
||||
"LTO": true
|
||||
}
|
||||
},
|
||||
@@ -38,8 +27,8 @@
|
||||
"inherits":["core"],
|
||||
"cacheVariables": {
|
||||
"CMAKE_BUILD_TYPE": "MinSizeRel",
|
||||
"ENABLE_NETWORK_INSTALL": true,
|
||||
"LTO": false
|
||||
"LTO": false,
|
||||
"DEV_BUILD": true
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -49,11 +38,6 @@
|
||||
"configurePreset": "Release",
|
||||
"jobs": 16
|
||||
},
|
||||
{
|
||||
"name": "ReleaseWithInstall",
|
||||
"configurePreset": "ReleaseWithInstall",
|
||||
"jobs": 16
|
||||
},
|
||||
{
|
||||
"name": "Dev",
|
||||
"configurePreset": "Dev",
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"url": "https://github.com/ITotalJustice/ftpsrv",
|
||||
"assets": [
|
||||
{
|
||||
"name": "switch"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"url": "https://github.com/ITotalJustice/untitled"
|
||||
}
|
||||
@@ -35,6 +35,7 @@ set(sphaira_VERSION_HASH "${sphaira_VERSION} [${GIT_COMMIT}]")
|
||||
add_executable(sphaira
|
||||
source/ui/menus/appstore.cpp
|
||||
source/ui/menus/file_viewer.cpp
|
||||
source/ui/menus/image_viewer.cpp
|
||||
source/ui/menus/filebrowser.cpp
|
||||
source/ui/menus/file_picker.cpp
|
||||
source/ui/menus/homebrew.cpp
|
||||
@@ -65,6 +66,7 @@ add_executable(sphaira
|
||||
source/ui/widget.cpp
|
||||
source/ui/list.cpp
|
||||
source/ui/scrolling_text.cpp
|
||||
source/ui/music_player.cpp
|
||||
|
||||
source/app.cpp
|
||||
source/download.cpp
|
||||
@@ -89,29 +91,39 @@ add_executable(sphaira
|
||||
source/title_info.cpp
|
||||
source/minizip_helper.cpp
|
||||
source/fatfs.cpp
|
||||
source/usbdvd.cpp
|
||||
|
||||
source/utils/utils.cpp
|
||||
source/utils/audio.cpp
|
||||
source/utils/nsz_dumper.cpp
|
||||
source/utils/devoptab_common.cpp
|
||||
source/utils/devoptab_romfs.cpp
|
||||
source/utils/devoptab_save.cpp
|
||||
source/utils/devoptab_nro.cpp
|
||||
source/utils/devoptab_nca.cpp
|
||||
source/utils/devoptab_nsp.cpp
|
||||
source/utils/devoptab_xci.cpp
|
||||
source/utils/devoptab_zip.cpp
|
||||
source/utils/devoptab_bfsar.cpp
|
||||
|
||||
source/usb/base.cpp
|
||||
source/usb/usbds.cpp
|
||||
source/usb/usbhs.cpp
|
||||
source/usb/usb_uploader.cpp
|
||||
source/usb/usb_installer.cpp
|
||||
source/usb/usb_dumper.cpp
|
||||
|
||||
source/yati/yati.cpp
|
||||
source/yati/container/nsp.cpp
|
||||
source/yati/container/xci.cpp
|
||||
source/yati/source/file.cpp
|
||||
source/yati/source/usb.cpp
|
||||
source/yati/source/stream.cpp
|
||||
source/yati/source/stream_file.cpp
|
||||
|
||||
source/yati/nx/es.cpp
|
||||
source/yati/nx/keys.cpp
|
||||
source/yati/nx/nca.cpp
|
||||
source/yati/nx/ncz.cpp
|
||||
source/yati/nx/ncm.cpp
|
||||
source/yati/nx/ns.cpp
|
||||
|
||||
@@ -123,7 +135,8 @@ target_compile_definitions(sphaira PRIVATE
|
||||
-DAPP_VERSION="${sphaira_VERSION}"
|
||||
-DAPP_VERSION_HASH="${sphaira_VERSION_HASH}"
|
||||
-DCURL_NO_OLDIES=1
|
||||
-DENABLE_NETWORK_INSTALL=$<BOOL:${ENABLE_NETWORK_INSTALL}>
|
||||
-DDEV_BUILD=$<BOOL:${DEV_BUILD}>
|
||||
-DZSTD_STATIC_LINKING_ONLY=1
|
||||
)
|
||||
|
||||
target_compile_options(sphaira PRIVATE
|
||||
@@ -177,12 +190,12 @@ FetchContent_Declare(ftpsrv
|
||||
|
||||
FetchContent_Declare(libhaze
|
||||
GIT_REPOSITORY https://github.com/ITotalJustice/libhaze.git
|
||||
GIT_TAG 0be1523
|
||||
GIT_TAG f0b2a14
|
||||
)
|
||||
|
||||
FetchContent_Declare(libpulsar
|
||||
GIT_REPOSITORY https://github.com/ITotalJustice/switch-libpulsar.git
|
||||
GIT_TAG de656e4
|
||||
GIT_TAG ac7bc97
|
||||
)
|
||||
|
||||
FetchContent_Declare(nanovg
|
||||
@@ -197,7 +210,7 @@ FetchContent_Declare(stb
|
||||
|
||||
FetchContent_Declare(yyjson
|
||||
GIT_REPOSITORY https://github.com/ibireme/yyjson.git
|
||||
GIT_TAG 0.11.1
|
||||
GIT_TAG 0.12.0
|
||||
)
|
||||
|
||||
FetchContent_Declare(minIni
|
||||
@@ -213,7 +226,7 @@ FetchContent_Declare(zstd
|
||||
|
||||
FetchContent_Declare(libusbhsfs
|
||||
GIT_REPOSITORY https://github.com/ITotalJustice/libusbhsfs.git
|
||||
GIT_TAG d0a973e
|
||||
GIT_TAG 625269b
|
||||
)
|
||||
|
||||
FetchContent_Declare(libnxtc
|
||||
@@ -226,31 +239,47 @@ FetchContent_Declare(nvjpg
|
||||
GIT_TAG 45680e7
|
||||
)
|
||||
|
||||
FetchContent_Declare(dr_libs
|
||||
GIT_REPOSITORY https://github.com/mackron/dr_libs.git
|
||||
GIT_TAG b962384
|
||||
SOURCE_SUBDIR NONE
|
||||
)
|
||||
|
||||
FetchContent_Declare(id3v2lib
|
||||
GIT_REPOSITORY https://github.com/larsbs/id3v2lib.git
|
||||
GIT_TAG 141ffb8
|
||||
)
|
||||
|
||||
FetchContent_Declare(libusbdvd
|
||||
GIT_REPOSITORY https://github.com/proconsule/libusbdvd.git
|
||||
GIT_TAG 3cb0613
|
||||
)
|
||||
|
||||
set(USE_NEW_ZSTD ON)
|
||||
# has issues with some homebrew and game icons (oxenfree, overwatch2).
|
||||
set(USE_NVJPG ON)
|
||||
set(USE_NVJPG OFF)
|
||||
|
||||
set(ZSTD_BUILD_STATIC ON)
|
||||
set(ZSTD_BUILD_SHARED OFF)
|
||||
set(ZSTD_BUILD_COMPRESSION OFF)
|
||||
set(ZSTD_BUILD_COMPRESSION ON)
|
||||
set(ZSTD_MULTITHREAD_SUPPORT ON)
|
||||
set(ZSTD_BUILD_DECOMPRESSION ON)
|
||||
set(ZSTD_BUILD_DICTBUILDER OFF)
|
||||
set(ZSTD_LEGACY_SUPPORT OFF)
|
||||
set(ZSTD_MULTITHREAD_SUPPORT OFF)
|
||||
set(ZSTD_BUILD_PROGRAMS OFF)
|
||||
set(ZSTD_BUILD_TESTS OFF)
|
||||
|
||||
set(MININI_LIB_NAME minIni)
|
||||
set(MININI_USE_STDIO ON)
|
||||
set(MININI_USE_NX OFF)
|
||||
set(MININI_USE_FLOAT OFF)
|
||||
set(MININI_USE_FLOAT ON)
|
||||
|
||||
if (CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")
|
||||
set(NANOVG_DEBUG ON)
|
||||
endif()
|
||||
set(NANOVG_NO_JPEG OFF)
|
||||
set(NANOVG_NO_PNG OFF)
|
||||
set(NANOVG_NO_BMP ON)
|
||||
set(NANOVG_NO_BMP OFF)
|
||||
set(NANOVG_NO_PSD ON)
|
||||
set(NANOVG_NO_TGA ON)
|
||||
set(NANOVG_NO_GIF ON)
|
||||
@@ -283,6 +312,9 @@ FetchContent_MakeAvailable(
|
||||
libusbhsfs
|
||||
libnxtc
|
||||
nvjpg
|
||||
dr_libs
|
||||
id3v2lib
|
||||
libusbdvd
|
||||
)
|
||||
|
||||
set(FTPSRV_LIB_BUILD TRUE)
|
||||
@@ -324,6 +356,9 @@ target_include_directories(ftpsrv_helper PUBLIC ${ftpsrv_SOURCE_DIR}/src/platfor
|
||||
add_library(stb INTERFACE)
|
||||
target_include_directories(stb INTERFACE ${stb_SOURCE_DIR})
|
||||
|
||||
add_library(dr_libs INTERFACE)
|
||||
target_include_directories(dr_libs INTERFACE ${dr_libs_SOURCE_DIR})
|
||||
|
||||
add_library(libnxtc
|
||||
${libnxtc_SOURCE_DIR}/source/nxtc.c
|
||||
${libnxtc_SOURCE_DIR}/source/nxtc_log.c
|
||||
@@ -331,6 +366,28 @@ add_library(libnxtc
|
||||
)
|
||||
target_include_directories(libnxtc PUBLIC ${libnxtc_SOURCE_DIR}/include)
|
||||
|
||||
add_library(libusbdvd
|
||||
${libusbdvd_SOURCE_DIR}/source/usbdvd.cpp
|
||||
${libusbdvd_SOURCE_DIR}/source/usbdvd_scsi.cpp
|
||||
${libusbdvd_SOURCE_DIR}/source/usbdvd_utils.cpp
|
||||
${libusbdvd_SOURCE_DIR}/source/fs/usbdvd_datadisc.cpp
|
||||
${libusbdvd_SOURCE_DIR}/source/fs/audiocdfs/audiocdfs.cpp
|
||||
${libusbdvd_SOURCE_DIR}/source/fs/audiocdfs/cdaudio_devoptab.cpp
|
||||
${libusbdvd_SOURCE_DIR}/source/fs/iso9660/usbdvd_iso9660.cpp
|
||||
${libusbdvd_SOURCE_DIR}/source/fs/iso9660/iso9660_devoptab.cpp
|
||||
${libusbdvd_SOURCE_DIR}/source/fs/udf/usbdvd_udf.cpp
|
||||
${libusbdvd_SOURCE_DIR}/source/fs/udf/udf_devoptab.cpp
|
||||
${libusbdvd_SOURCE_DIR}/source/os/switch/switch_usb.cpp
|
||||
|
||||
)
|
||||
target_include_directories(libusbdvd PRIVATE ${libusbdvd_SOURCE_DIR}/source/)
|
||||
target_include_directories(libusbdvd PRIVATE ${libusbdvd_SOURCE_DIR}/source/os/switch)
|
||||
target_include_directories(libusbdvd PRIVATE ${libusbdvd_SOURCE_DIR}/source/fs)
|
||||
target_include_directories(libusbdvd PRIVATE ${libusbdvd_SOURCE_DIR}/source/fs/audiocdfs)
|
||||
target_include_directories(libusbdvd PRIVATE ${libusbdvd_SOURCE_DIR}/source/fs/iso9660)
|
||||
target_include_directories(libusbdvd PRIVATE ${libusbdvd_SOURCE_DIR}/source/fs/udf)
|
||||
target_include_directories(libusbdvd PUBLIC ${libusbdvd_SOURCE_DIR}/include)
|
||||
|
||||
if (USE_NVJPG)
|
||||
add_library(nvjpg
|
||||
${nvjpg_SOURCE_DIR}/lib/decoder.cpp
|
||||
@@ -357,7 +414,6 @@ endif()
|
||||
add_library(fatfs
|
||||
source/ff16/diskio.c
|
||||
source/ff16/ff.c
|
||||
source/ff16/ffunicode.c
|
||||
)
|
||||
|
||||
target_include_directories(fatfs PUBLIC source/ff16)
|
||||
@@ -380,6 +436,9 @@ target_link_libraries(sphaira PRIVATE
|
||||
# libusbhsfs
|
||||
libnxtc
|
||||
fatfs
|
||||
dr_libs
|
||||
id3v2lib
|
||||
libusbdvd
|
||||
|
||||
${minizip_lib}
|
||||
ZLIB::ZLIB
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
#include "nanovg.h"
|
||||
#include "nanovg/dk_renderer.hpp"
|
||||
#include "pulsar.h"
|
||||
#include "ui/widget.hpp"
|
||||
#include "ui/notification.hpp"
|
||||
#include "owo.hpp"
|
||||
#include "option.hpp"
|
||||
#include "fs.hpp"
|
||||
#include "log.hpp"
|
||||
#include "utils/audio.hpp"
|
||||
|
||||
#ifdef USE_NVJPG
|
||||
#include <nvjpg.hpp>
|
||||
@@ -22,16 +22,7 @@
|
||||
|
||||
namespace sphaira {
|
||||
|
||||
enum SoundEffect {
|
||||
SoundEffect_Music,
|
||||
SoundEffect_Focus,
|
||||
SoundEffect_Scroll,
|
||||
SoundEffect_Limit,
|
||||
SoundEffect_Startup,
|
||||
SoundEffect_Install,
|
||||
SoundEffect_Error,
|
||||
SoundEffect_MAX,
|
||||
};
|
||||
using SoundEffect = audio::SoundEffect;
|
||||
|
||||
enum class LaunchType {
|
||||
Normal,
|
||||
@@ -108,6 +99,10 @@ public:
|
||||
static auto GetLanguage() -> long;
|
||||
static auto GetTextScrollSpeed() -> long;
|
||||
|
||||
static auto GetNszCompressLevel() -> u8;
|
||||
static auto GetNszThreadCount() -> u8;
|
||||
static auto GetNszBlockExponent() -> u8;
|
||||
|
||||
static void SetMtpEnable(bool enable);
|
||||
static void SetFtpEnable(bool enable);
|
||||
static void SetNxlinkEnable(bool enable);
|
||||
@@ -153,8 +148,12 @@ public:
|
||||
|
||||
void LoadTheme(const ThemeMeta& meta);
|
||||
void CloseTheme();
|
||||
void CloseThemeBackgroundMusic();
|
||||
void ScanThemes(const std::string& path);
|
||||
void ScanThemeEntries();
|
||||
void LoadAndPlayThemeMusic();
|
||||
static Result SetDefaultBackgroundMusic(fs::Fs* fs, const fs::FsPath& path);
|
||||
static void SetBackgroundMusicPause(bool pause);
|
||||
|
||||
// helper that converts 1.2.3 to a u32 used for comparisons.
|
||||
static auto GetVersionFromString(const char* str) -> u32;
|
||||
@@ -294,6 +293,7 @@ public:
|
||||
|
||||
option::OptionBool m_log_enabled{INI_SECTION, "log_enabled", false};
|
||||
option::OptionBool m_replace_hbmenu{INI_SECTION, "replace_hbmenu", false};
|
||||
option::OptionString m_default_music{INI_SECTION, "default_music", "/config/sphaira/themes/default_music.bfstm"};
|
||||
option::OptionString m_theme_path{INI_SECTION, "theme", DEFAULT_THEME_PATH};
|
||||
option::OptionBool m_theme_music{INI_SECTION, "theme_music", true};
|
||||
option::OptionBool m_12hour_time{INI_SECTION, "12hour_time", false};
|
||||
@@ -328,13 +328,18 @@ public:
|
||||
option::OptionBool m_dump_append_folder_with_xci{"dump", "append_folder_with_xci", true};
|
||||
option::OptionBool m_dump_trim_xci{"dump", "trim_xci", false};
|
||||
option::OptionBool m_dump_label_trim_xci{"dump", "label_trim_xci", false};
|
||||
option::OptionBool m_dump_usb_transfer_stream{"dump", "usb_transfer_stream", true, false};
|
||||
option::OptionBool m_dump_convert_to_common_ticket{"dump", "convert_to_common_ticket", true};
|
||||
option::OptionLong m_nsz_compress_level{"dump", "nsz_compress_level", 3};
|
||||
option::OptionLong m_nsz_compress_threads{"dump", "nsz_compress_threads", 3};
|
||||
option::OptionBool m_nsz_compress_ldm{"dump", "nsz_compress_ldm", true};
|
||||
option::OptionBool m_nsz_compress_block{"dump", "nsz_compress_block", false};
|
||||
option::OptionLong m_nsz_compress_block_exponent{"dump", "nsz_compress_block_exponent", 6};
|
||||
|
||||
// todo: move this into it's own menu
|
||||
option::OptionLong m_text_scroll_speed{"accessibility", "text_scroll_speed", 1}; // normal
|
||||
|
||||
PLSR_PlayerSoundId m_sound_ids[SoundEffect_MAX]{};
|
||||
std::shared_ptr<fs::FsNativeSd> m_fs{};
|
||||
audio::SongID m_background_music{};
|
||||
|
||||
#ifdef USE_NVJPG
|
||||
nj::Decoder m_decoder;
|
||||
|
||||
@@ -577,6 +577,7 @@ enum class SphairaResult : Result {
|
||||
|
||||
UsbDsBadDeviceSpeed,
|
||||
|
||||
NcaBadMagic,
|
||||
NspBadMagic,
|
||||
XciBadMagic,
|
||||
XciSecurePartitionNotFound,
|
||||
@@ -648,6 +649,17 @@ enum class SphairaResult : Result {
|
||||
YatiNcmDbCorruptHeader,
|
||||
// unable to total infos from ncm database.
|
||||
YatiNcmDbCorruptInfos,
|
||||
|
||||
NszFailedCreateCctx,
|
||||
NszFailedSetCompressionLevel,
|
||||
NszFailedSetThreadCount,
|
||||
NszFailedSetLongDistanceMode,
|
||||
NszFailedResetCctx,
|
||||
NszFailedCompress2,
|
||||
NszFailedCompressStream2,
|
||||
NszTooManyBlocks,
|
||||
// set when nca finished but not all blocks were handled.
|
||||
NszMissingBlocks,
|
||||
};
|
||||
|
||||
#define MAKE_SPHAIRA_RESULT_ENUM(x) Result_##x = MAKERESULT(Module_Sphaira, (Result)SphairaResult::x)
|
||||
@@ -717,8 +729,11 @@ enum : Result {
|
||||
MAKE_SPHAIRA_RESULT_ENUM(ThemezerFailedToDownloadTheme),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(MainFailedToDownloadUpdate),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(UsbDsBadDeviceSpeed),
|
||||
|
||||
MAKE_SPHAIRA_RESULT_ENUM(NspBadMagic),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(XciBadMagic),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(NcaBadMagic),
|
||||
|
||||
MAKE_SPHAIRA_RESULT_ENUM(XciSecurePartitionNotFound),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(EsBadTitleKeyType),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(EsPersonalisedTicketDeviceIdMissmatch),
|
||||
@@ -729,7 +744,9 @@ enum : Result {
|
||||
MAKE_SPHAIRA_RESULT_ENUM(EsInvalidTicketFromatVersion),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(EsInvalidTicketKeyType),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(EsInvalidTicketKeyRevision),
|
||||
|
||||
MAKE_SPHAIRA_RESULT_ENUM(OwoBadArgs),
|
||||
|
||||
MAKE_SPHAIRA_RESULT_ENUM(UsbCancelled),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(UsbBadMagic),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(UsbBadVersion),
|
||||
@@ -744,6 +761,7 @@ enum : Result {
|
||||
MAKE_SPHAIRA_RESULT_ENUM(UsbUploadBadTransferSize),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(UsbUploadBadTotalSize),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(UsbUploadBadCommand),
|
||||
|
||||
MAKE_SPHAIRA_RESULT_ENUM(YatiContainerNotFound),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(YatiNcaNotFound),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(YatiInvalidNcaReadSize),
|
||||
@@ -765,6 +783,16 @@ enum : Result {
|
||||
MAKE_SPHAIRA_RESULT_ENUM(YatiCertNotFound),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(YatiNcmDbCorruptHeader),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(YatiNcmDbCorruptInfos),
|
||||
|
||||
MAKE_SPHAIRA_RESULT_ENUM(NszFailedCreateCctx),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(NszFailedSetCompressionLevel),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(NszFailedSetThreadCount),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(NszFailedSetLongDistanceMode),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(NszFailedResetCctx),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(NszFailedCompress2),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(NszFailedCompressStream2),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(NszTooManyBlocks),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(NszMissingBlocks),
|
||||
};
|
||||
|
||||
#undef MAKE_SPHAIRA_RESULT_ENUM
|
||||
@@ -799,16 +827,6 @@ enum : Result {
|
||||
// #define ON_SCOPE_FAIL(_f) std::experimental::scope_exit ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE_){[&] { if (R_FAILED(rc)) { _f; } }};
|
||||
// #define ON_SCOPE_SUCCESS(_f) std::experimental::scope_exit ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE_){[&] { if (R_SUCCEEDED(rc)) { _f; } }};
|
||||
|
||||
// threading helpers.
|
||||
#define PRIO_PREEMPTIVE 0x3B
|
||||
|
||||
// threading affinity, use with svcSetThreadCoreMask().
|
||||
#define THREAD_AFFINITY_CORE0 BIT(0)
|
||||
#define THREAD_AFFINITY_CORE1 BIT(1)
|
||||
#define THREAD_AFFINITY_CORE2 BIT(2)
|
||||
#define THREAD_AFFINITY_DEFAULT(core) (BIT(core)|THREAD_AFFINITY_CORE1|THREAD_AFFINITY_CORE2)
|
||||
#define THREAD_AFFINITY_ALL (THREAD_AFFINITY_CORE0|THREAD_AFFINITY_CORE1|THREAD_AFFINITY_CORE2)
|
||||
|
||||
// mutex helpers.
|
||||
#define SCOPED_MUTEX(mutex) \
|
||||
mutexLock(mutex); \
|
||||
|
||||
@@ -142,6 +142,7 @@ struct DownloadEventData {
|
||||
|
||||
auto Init() -> bool;
|
||||
void Exit();
|
||||
void ExitSignal();
|
||||
|
||||
// sync functions
|
||||
auto ToMemory(const Api& e) -> ApiResult;
|
||||
|
||||
@@ -2,16 +2,21 @@
|
||||
|
||||
#include "fs.hpp"
|
||||
#include "location.hpp"
|
||||
#include "ui/progress_box.hpp"
|
||||
|
||||
#include <switch.h>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <functional>
|
||||
#include <minizip/ioapi.h>
|
||||
|
||||
namespace sphaira::dump {
|
||||
|
||||
enum DumpLocationType {
|
||||
// dump using native fs.
|
||||
DumpLocationType_SdCard,
|
||||
// dump to usb pc.
|
||||
DumpLocationType_Usb,
|
||||
// dump to usb using tinfoil protocol.
|
||||
DumpLocationType_UsbS2S,
|
||||
// speed test, only reads the data, doesn't write anything.
|
||||
@@ -24,11 +29,12 @@ enum DumpLocationType {
|
||||
|
||||
enum DumpLocationFlag {
|
||||
DumpLocationFlag_SdCard = 1 << DumpLocationType_SdCard,
|
||||
DumpLocationFlag_Usb = 1 << DumpLocationType_Usb,
|
||||
DumpLocationFlag_UsbS2S = 1 << DumpLocationType_UsbS2S,
|
||||
DumpLocationFlag_DevNull = 1 << DumpLocationType_DevNull,
|
||||
DumpLocationFlag_Stdio = 1 << DumpLocationType_Stdio,
|
||||
DumpLocationFlag_Network = 1 << DumpLocationType_Network,
|
||||
DumpLocationFlag_All = DumpLocationFlag_SdCard | DumpLocationFlag_UsbS2S | DumpLocationFlag_DevNull | DumpLocationFlag_Stdio | DumpLocationFlag_Network,
|
||||
DumpLocationFlag_All = DumpLocationFlag_SdCard | DumpLocationFlag_Usb | DumpLocationFlag_UsbS2S | DumpLocationFlag_DevNull | DumpLocationFlag_Stdio | DumpLocationFlag_Network,
|
||||
};
|
||||
|
||||
struct DumpEntry {
|
||||
@@ -48,17 +54,36 @@ struct BaseSource {
|
||||
virtual auto GetName(const std::string& path) const -> std::string = 0;
|
||||
virtual auto GetSize(const std::string& path) const -> s64 = 0;
|
||||
virtual auto GetIcon(const std::string& path) const -> int { return 0; }
|
||||
|
||||
Result Read(const std::string& path, void* buf, s64 off, s64 size) {
|
||||
u64 bytes_read;
|
||||
return Read(path, buf, off, size, &bytes_read);
|
||||
}
|
||||
};
|
||||
|
||||
struct WriteSource {
|
||||
virtual ~WriteSource() = default;
|
||||
virtual Result Write(const void* buf, s64 off, s64 size) = 0;
|
||||
virtual Result SetSize(s64 size) = 0;
|
||||
};
|
||||
|
||||
// called after dump has finished.
|
||||
using OnExit = std::function<void(Result rc)>;
|
||||
using OnLocation = std::function<void(const DumpLocation& loc)>;
|
||||
|
||||
using CustomTransfer = std::function<Result(ui::ProgressBox* pbox, BaseSource* source, WriteSource* writer, const fs::FsPath& path)>;
|
||||
|
||||
// prompts the user to select dump location, calls on_loc on success with the selected location.
|
||||
void DumpGetLocation(const std::string& title, u32 location_flags, const OnLocation& on_loc);
|
||||
void DumpGetLocation(const std::string& title, u32 location_flags, const OnLocation& on_loc, const CustomTransfer& custom_transfer = nullptr);
|
||||
|
||||
Result Dump(ui::ProgressBox* pbox, const std::shared_ptr<BaseSource>& source, const DumpLocation& location, const std::vector<fs::FsPath>& paths, const CustomTransfer& custom_transfer = nullptr);
|
||||
|
||||
// dumps to a fetched location using DumpGetLocation().
|
||||
void Dump(const std::shared_ptr<BaseSource>& source, const DumpLocation& location, const std::vector<fs::FsPath>& paths, const OnExit& on_exit);
|
||||
void Dump(const std::shared_ptr<BaseSource>& source, const DumpLocation& location, const std::vector<fs::FsPath>& paths, const OnExit& on_exit, const CustomTransfer& custom_transfer = nullptr);
|
||||
// DumpGetLocation() + Dump() all in one.
|
||||
void Dump(const std::shared_ptr<BaseSource>& source, const std::vector<fs::FsPath>& paths, const OnExit& on_exit = [](Result){}, u32 location_flags = DumpLocationFlag_All);
|
||||
void Dump(const std::shared_ptr<BaseSource>& source, const std::vector<fs::FsPath>& paths, const OnExit& on_exit = nullptr, u32 location_flags = DumpLocationFlag_All);
|
||||
void Dump(const std::shared_ptr<BaseSource>& source, const std::vector<fs::FsPath>& paths, const CustomTransfer& custom_transfer, const OnExit& on_exit = nullptr, u32 location_flags = DumpLocationFlag_All);
|
||||
|
||||
void FileFuncWriter(WriteSource* writer, zlib_filefunc64_def* funcs);
|
||||
|
||||
} // namespace sphaira::dump
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <switch.h>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
namespace sphaira::fatfs {
|
||||
|
||||
|
||||
@@ -322,6 +322,7 @@ struct Fs {
|
||||
virtual bool FileExists(const FsPath& path) = 0;
|
||||
virtual bool DirExists(const FsPath& path) = 0;
|
||||
virtual bool IsNative() const = 0;
|
||||
virtual bool IsSd() const { return false; }
|
||||
virtual FsPath Root() const { return "/"; }
|
||||
|
||||
Result OpenFile(const fs::FsPath& path, u32 mode, File* f) {
|
||||
@@ -510,6 +511,8 @@ struct FsNativeSd final : FsNative {
|
||||
FsNativeSd(bool ignore_read_only = true) : FsNative{fsdevGetDeviceFileSystem("sdmc:"), false, ignore_read_only} {
|
||||
m_open_result = 0;
|
||||
}
|
||||
|
||||
bool IsSd() const override { return true; }
|
||||
};
|
||||
#endif
|
||||
|
||||
|
||||
@@ -14,6 +14,9 @@ enum class Type {
|
||||
Md5,
|
||||
Sha1,
|
||||
Sha256,
|
||||
Null,
|
||||
Deflate,
|
||||
Zstd,
|
||||
};
|
||||
|
||||
struct BaseSource {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
namespace sphaira::haze {
|
||||
|
||||
bool Init();
|
||||
bool IsInit();
|
||||
void Exit();
|
||||
|
||||
using OnInstallStart = std::function<bool(const char* path)>;
|
||||
|
||||
@@ -9,6 +9,11 @@
|
||||
|
||||
namespace sphaira {
|
||||
|
||||
struct NroData {
|
||||
NroStart start;
|
||||
NroHeader header;
|
||||
};
|
||||
|
||||
struct Hbini {
|
||||
u64 timestamp{}; // timestamp of last launch
|
||||
bool hidden{};
|
||||
@@ -27,9 +32,6 @@ struct NroEntry {
|
||||
u64 icon_size{};
|
||||
u64 icon_offset{};
|
||||
|
||||
u64 romfs_size{};
|
||||
u64 romfs_offset{};
|
||||
|
||||
FsTimeStampRaw timestamp{};
|
||||
Hbini hbini{};
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ private:
|
||||
|
||||
using OptionBool = OptionBase<bool>;
|
||||
using OptionLong = OptionBase<long>;
|
||||
using OptionFloat = OptionBase<float>;
|
||||
using OptionString = OptionBase<std::string>;
|
||||
|
||||
} // namespace sphaira::option
|
||||
|
||||
@@ -15,7 +15,10 @@ enum class Mode {
|
||||
SingleThreadedIfSmaller,
|
||||
};
|
||||
|
||||
using DecompressWriteCallback = std::function<Result(const void* data, s64 size)>;
|
||||
|
||||
using ReadCallback = std::function<Result(void* data, s64 off, s64 size, u64* bytes_read)>;
|
||||
using DecompressCallback = std::function<Result(void* data, s64 off, s64 size, const DecompressWriteCallback& callback)>;
|
||||
using WriteCallback = std::function<Result(const void* data, s64 off, s64 size)>;
|
||||
|
||||
// used for pull api
|
||||
@@ -33,6 +36,7 @@ using StartCallback2 = std::function<Result(StartThreadCallback start, PullCallb
|
||||
|
||||
// reads data from rfunc into wfunc.
|
||||
Result Transfer(ui::ProgressBox* pbox, s64 size, const ReadCallback& rfunc, const WriteCallback& wfunc, Mode mode = Mode::MultiThreaded);
|
||||
Result Transfer(ui::ProgressBox* pbox, s64 size, const ReadCallback& rfunc, const DecompressCallback& dfunc, const WriteCallback& wfunc, Mode mode = Mode::MultiThreaded);
|
||||
|
||||
// reads data from rfunc, pull data from provided pull() callback.
|
||||
Result TransferPull(ui::ProgressBox* pbox, s64 size, const ReadCallback& rfunc, const StartCallback& sfunc, Mode mode = Mode::MultiThreaded);
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
namespace sphaira::ui::menu::fileview {
|
||||
|
||||
struct Menu final : MenuBase {
|
||||
Menu(const fs::FsPath& path);
|
||||
Menu(fs::Fs* fs, const fs::FsPath& path);
|
||||
|
||||
auto GetShortTitle() const -> const char* override { return "File"; };
|
||||
void Update(Controller* controller, TouchInfo* touch) override;
|
||||
@@ -15,8 +15,8 @@ struct Menu final : MenuBase {
|
||||
void OnFocusGained() override;
|
||||
|
||||
private:
|
||||
fs::Fs* const m_fs;
|
||||
const fs::FsPath m_path;
|
||||
fs::FsNativeSd m_fs{};
|
||||
fs::File m_file{};
|
||||
s64 m_file_size{};
|
||||
s64 m_file_offset{};
|
||||
|
||||
@@ -106,6 +106,7 @@ struct FileEntry final : FsDirectoryEntry {
|
||||
bool checked_extension{}; // did we already search for an ext?
|
||||
bool checked_internal_extension{}; // did we already search for an ext?
|
||||
bool selected{}; // is this file selected?
|
||||
bool done_stat{}; // have we checked file_size / count.
|
||||
|
||||
auto IsFile() const -> bool {
|
||||
return type == FsDirEntryType_File;
|
||||
@@ -305,9 +306,11 @@ struct FsView final : Widget {
|
||||
void DisplayOptions();
|
||||
void DisplayAdvancedOptions();
|
||||
|
||||
void MountNspFs();
|
||||
void MountXciFs();
|
||||
void MountZipFs();
|
||||
using MountFsFunc = Result(*)(fs::Fs *fs, const fs::FsPath &path, fs::FsPath &out_path);
|
||||
// using MountFsFunc = std::function<Result(fs::Fs *fs, const fs::FsPath &path, fs::FsPath &out_path)>;
|
||||
using UmountFsFunc = std::function<void(const fs::FsPath &mount)>;
|
||||
|
||||
void MountFileFs(const MountFsFunc& mount_func, const UmountFsFunc& umount_func);
|
||||
|
||||
// private:
|
||||
Base* m_menu{};
|
||||
@@ -437,6 +440,9 @@ protected:
|
||||
|
||||
auto CreateFs(const FsEntry& fs_entry) -> std::shared_ptr<fs::Fs>;
|
||||
|
||||
private:
|
||||
void Init(const std::shared_ptr<fs::Fs>& fs, const FsEntry& fs_entry, const fs::FsPath& path, bool is_custom);
|
||||
|
||||
protected:
|
||||
static constexpr inline const char* INI_SECTION = "filebrowser";
|
||||
|
||||
@@ -480,7 +486,6 @@ auto IsSamePath(std::string_view a, std::string_view b) -> bool;
|
||||
auto IsExtension(std::string_view ext1, std::string_view ext2) -> bool;
|
||||
auto IsExtension(std::string_view ext, std::span<const std::string_view> list) -> bool;
|
||||
|
||||
|
||||
struct FsStdioWrapper final : fs::FsStdio {
|
||||
using OnExit = std::function<void(void)>;
|
||||
FsStdioWrapper(const fs::FsPath& root, const OnExit& on_exit) : fs::FsStdio{true, root}, m_on_exit{on_exit} {
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace sphaira::ui::menu::game {
|
||||
|
||||
struct Entry {
|
||||
u64 app_id{};
|
||||
u8 type{};
|
||||
u8 last_event{};
|
||||
NacpLanguageEntry lang{};
|
||||
int image{};
|
||||
bool selected{};
|
||||
@@ -84,7 +84,8 @@ private:
|
||||
}
|
||||
|
||||
void DeleteGames();
|
||||
void DumpGames(u32 flags);
|
||||
void ExportOptions(bool to_nsz);
|
||||
void DumpGames(u32 flags, bool to_nsz);
|
||||
void CreateSaves(AccountUid uid);
|
||||
|
||||
private:
|
||||
@@ -98,6 +99,10 @@ private:
|
||||
bool m_is_reversed{};
|
||||
bool m_dirty{};
|
||||
|
||||
// use for detection game card removal to force a refresh.
|
||||
Event m_gc_event{};
|
||||
FsEventNotifier m_gc_event_notifier{};
|
||||
|
||||
option::OptionLong m_sort{INI_SECTION, "sort", SortType::SortType_Updated};
|
||||
option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Descending};
|
||||
option::OptionLong m_layout{INI_SECTION, "layout", LayoutType::LayoutType_Grid};
|
||||
@@ -160,13 +165,13 @@ struct ContentInfoEntry {
|
||||
std::vector<NcmRightsId> ncm_rights_id{};
|
||||
};
|
||||
|
||||
auto BuildNspPath(const Entry& e, const NsApplicationContentMetaStatus& status) -> fs::FsPath;
|
||||
Result BuildContentEntry(const NsApplicationContentMetaStatus& status, ContentInfoEntry& out);
|
||||
Result BuildNspEntry(const Entry& e, const ContentInfoEntry& info, const keys::Keys& keys, NspEntry& out);
|
||||
Result BuildNspEntries(Entry& e, const title::MetaEntries& meta_entries, std::vector<NspEntry>& out);
|
||||
Result BuildNspEntries(Entry& e, u32 flags, std::vector<NspEntry>& out);
|
||||
auto BuildNspPath(const Entry& e, const NsApplicationContentMetaStatus& status, bool to_nsz = false) -> fs::FsPath;
|
||||
Result BuildContentEntry(const NsApplicationContentMetaStatus& status, ContentInfoEntry& out, bool to_nsz = false);
|
||||
Result BuildNspEntry(const Entry& e, const ContentInfoEntry& info, const keys::Keys& keys, NspEntry& out, bool to_nsz = false);
|
||||
Result BuildNspEntries(Entry& e, const title::MetaEntries& meta_entries, std::vector<NspEntry>& out, bool to_nsz = false);
|
||||
Result BuildNspEntries(Entry& e, u32 flags, std::vector<NspEntry>& out, bool to_nsz = false);
|
||||
|
||||
// dumps the array of nsp entries.
|
||||
void DumpNsp(const std::vector<NspEntry>& entries);
|
||||
void DumpNsp(const std::vector<NspEntry>& entries, bool to_nsz);
|
||||
|
||||
} // namespace sphaira::ui::menu::game
|
||||
|
||||
@@ -91,7 +91,7 @@ private:
|
||||
return GetEntry(m_index);
|
||||
}
|
||||
|
||||
void DumpGames();
|
||||
void DumpGames(bool to_nsz);
|
||||
void DeleteGames();
|
||||
Result ResetRequiredSystemVersion(MetaEntry& entry) const;
|
||||
Result GetNcmSizeOfMetaStatus(MetaEntry& entry) const;
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
#include <span>
|
||||
#include <memory>
|
||||
|
||||
namespace sphaira::ui::menu::gc {
|
||||
// todo: pr to libnx
|
||||
extern "C" {
|
||||
|
||||
typedef enum {
|
||||
FsGameCardPartitionRaw_None = -1,
|
||||
@@ -15,6 +16,13 @@ typedef enum {
|
||||
FsGameCardPartitionRaw_Secure = 1,
|
||||
} FsGameCardPartitionRaw;
|
||||
|
||||
Result fsOpenGameCardStorage(FsStorage* out, const FsGameCardHandle* handle, FsGameCardPartitionRaw partition);
|
||||
Result fsOpenGameCardDetectionEventNotifier(FsEventNotifier* out);
|
||||
|
||||
}
|
||||
|
||||
namespace sphaira::ui::menu::gc {
|
||||
|
||||
////////////////////////////////////////////////
|
||||
// The below structs are taken from nxdumptool./
|
||||
////////////////////////////////////////////////
|
||||
@@ -88,6 +96,33 @@ typedef struct {
|
||||
|
||||
static_assert(sizeof(GameCardInitialData) == 0x200);
|
||||
|
||||
/// Encrypted using AES-128-CTR with the key and IV/counter from the `GameCardTitleKeyAreaEncryption` section. Assumed to be all zeroes in retail gamecards.
|
||||
typedef struct {
|
||||
u8 titlekey[0x10]; ///< Decrypted titlekey from the `GameCardInitialData` section.
|
||||
u8 reserved[0xCF0];
|
||||
} GameCardTitleKeyArea;
|
||||
|
||||
static_assert(sizeof(GameCardTitleKeyArea) == 0xD00);
|
||||
|
||||
/// Encrypted using RSA-2048-OAEP and a private OAEP key from AuthoringTool. Assumed to be all zeroes in retail gamecards.
|
||||
typedef struct {
|
||||
u8 titlekey_encryption_key[0x10]; ///< Used as the AES-128-CTR key for the `GameCardTitleKeyArea` section. Randomly generated during XCI creation by AuthoringTool.
|
||||
u8 titlekey_encryption_iv[0x10]; ///< Used as the AES-128-CTR IV/counter for the `GameCardTitleKeyArea` section. Randomly generated during XCI creation by AuthoringTool.
|
||||
u8 reserved[0xE0];
|
||||
} GameCardTitleKeyAreaEncryption;
|
||||
|
||||
static_assert(sizeof(GameCardTitleKeyAreaEncryption) == 0x100);
|
||||
|
||||
/// Used to secure communications between the Lotus and the inserted gamecard.
|
||||
/// Supposedly precedes the gamecard header.
|
||||
typedef struct {
|
||||
GameCardInitialData initial_data;
|
||||
GameCardTitleKeyArea titlekey_area;
|
||||
GameCardTitleKeyAreaEncryption titlekey_area_encryption;
|
||||
} GameCardKeyArea;
|
||||
|
||||
static_assert(sizeof(GameCardKeyArea) == 0x1000);
|
||||
|
||||
typedef struct {
|
||||
u8 maker_code; ///< GameCardUidMakerCode.
|
||||
u8 version; ///< TODO: determine whether this matches GameCardVersion or not.
|
||||
@@ -198,6 +233,7 @@ private:
|
||||
void FreeImage();
|
||||
void OnChangeIndex(s64 new_index);
|
||||
Result DumpGames(u32 flags);
|
||||
Result DumpXcz(u32 flags);
|
||||
|
||||
Result MountGcFs();
|
||||
|
||||
@@ -222,12 +258,12 @@ private:
|
||||
|
||||
FsStorage m_storage{};
|
||||
// size of normal partition.
|
||||
s64 m_parition_normal_size{};
|
||||
s64 m_partition_normal_size{};
|
||||
// size of secure partition.
|
||||
s64 m_parition_secure_size{};
|
||||
s64 m_partition_secure_size{};
|
||||
// used size reported in the xci header.
|
||||
s64 m_storage_trimmed_size{};
|
||||
// total size of m_parition_normal_size + m_parition_secure_size.
|
||||
// total size of m_partition_normal_size + m_partition_secure_size.
|
||||
s64 m_storage_total_size{};
|
||||
// reported size via rom_size in the xci header.
|
||||
s64 m_storage_full_size{};
|
||||
|
||||
@@ -71,7 +71,7 @@ private:
|
||||
return m_sort.Get() >= SortType_UpdatedStar;
|
||||
}
|
||||
|
||||
Result MountRomfsFs();
|
||||
Result MountNroFs();
|
||||
|
||||
private:
|
||||
static constexpr inline const char* INI_SECTION = "homebrew";
|
||||
|
||||
36
sphaira/include/ui/menus/image_viewer.hpp
Normal file
36
sphaira/include/ui/menus/image_viewer.hpp
Normal 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
|
||||
@@ -87,7 +87,8 @@ private:
|
||||
|
||||
auto BuildSavePath(const Entry& e, bool is_auto) const -> fs::FsPath;
|
||||
Result RestoreSaveInternal(ProgressBox* pbox, const Entry& e, const fs::FsPath& path) const;
|
||||
Result BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& location, const Entry& e, bool compressed, bool is_auto = false) const;
|
||||
Result BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& location, Entry& e, bool compressed, bool is_auto = false) const;
|
||||
Result BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& location, std::span<const std::reference_wrapper<Entry>> entries, bool compressed, bool is_auto = false) const;
|
||||
|
||||
Result MountSaveFs();
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#if 0
|
||||
#pragma once
|
||||
|
||||
#include "ui/menus/menu_base.hpp"
|
||||
@@ -175,3 +176,4 @@ private:
|
||||
};
|
||||
|
||||
} // namespace sphaira::ui::menu::themezer
|
||||
#endif
|
||||
|
||||
45
sphaira/include/ui/music_player.hpp
Normal file
45
sphaira/include/ui/music_player.hpp
Normal 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
|
||||
@@ -17,8 +17,7 @@ struct ProgressBox final : Widget {
|
||||
int image,
|
||||
const std::string& action,
|
||||
const std::string& title,
|
||||
const ProgressBoxCallback& callback, const ProgressBoxDoneCallback& done = [](Result rc){},
|
||||
int cpuid = 1, int prio = PRIO_PREEMPTIVE, int stack_size = 1024*128
|
||||
const ProgressBoxCallback& callback, const ProgressBoxDoneCallback& done = nullptr
|
||||
);
|
||||
~ProgressBox();
|
||||
|
||||
@@ -28,6 +27,8 @@ struct ProgressBox final : Widget {
|
||||
auto SetActionName(const std::string& action) -> ProgressBox&;
|
||||
auto SetTitle(const std::string& title) -> ProgressBox&;
|
||||
auto NewTransfer(const std::string& transfer) -> ProgressBox&;
|
||||
// zeros the saved offset.
|
||||
auto ResetTranfser() -> ProgressBox&;
|
||||
auto UpdateTransfer(s64 offset, s64 size) -> ProgressBox&;
|
||||
// not const in order to avoid copy by using std::swap
|
||||
auto SetImage(int image) -> ProgressBox&;
|
||||
@@ -44,10 +45,6 @@ struct ProgressBox final : Widget {
|
||||
auto CopyFile(const fs::FsPath& src, const fs::FsPath& dst, bool single_threaded = false) -> Result;
|
||||
void Yield();
|
||||
|
||||
auto GetCpuId() const {
|
||||
return m_cpuid;
|
||||
}
|
||||
|
||||
auto OnDownloadProgressCallback() {
|
||||
return [this](s64 dltotal, s64 dlnow, s64 ultotal, s64 ulnow){
|
||||
if (this->ShouldExit()) {
|
||||
@@ -103,7 +100,6 @@ private:
|
||||
ScrollingText m_scroll_title{};
|
||||
ScrollingText m_scroll_transfer{};
|
||||
|
||||
int m_cpuid{};
|
||||
int m_image{};
|
||||
bool m_own_image{};
|
||||
};
|
||||
|
||||
@@ -91,6 +91,25 @@ private:
|
||||
std::string m_false_str;
|
||||
};
|
||||
|
||||
class SidebarEntrySlider final : public SidebarEntryBase {
|
||||
public:
|
||||
using Callback = std::function<void(float&)>;
|
||||
|
||||
public:
|
||||
explicit SidebarEntrySlider(const std::string& title, float value, float min, float max, int steps, const Callback& cb, const std::string& info = "");
|
||||
void Draw(NVGcontext* vg, Theme* theme, const Vec4& root_pos, bool left) override;
|
||||
|
||||
private:
|
||||
float m_value;
|
||||
float m_min;
|
||||
float m_max;
|
||||
int m_steps;
|
||||
Callback m_callback;
|
||||
|
||||
float m_duration;
|
||||
float m_inc;
|
||||
};
|
||||
|
||||
class SidebarEntryCallback final : public SidebarEntryBase {
|
||||
public:
|
||||
using Callback = std::function<void()>;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "nanovg.h"
|
||||
#include "pulsar.h"
|
||||
#include "fs.hpp"
|
||||
|
||||
#include <switch.h>
|
||||
@@ -229,6 +228,7 @@ struct ThemeMeta {
|
||||
struct Theme {
|
||||
ThemeMeta meta;
|
||||
ElementEntry elements[ThemeEntryID_MAX];
|
||||
fs::FsPath music_path;
|
||||
|
||||
auto GetColour(ThemeEntryID id) const {
|
||||
return elements[id].colour;
|
||||
@@ -291,11 +291,13 @@ enum class Button : u64 {
|
||||
LS_RIGHT = static_cast<u64>(HidNpadButton_StickLRight),
|
||||
LS_UP = static_cast<u64>(HidNpadButton_StickLUp),
|
||||
LS_DOWN = static_cast<u64>(HidNpadButton_StickLDown),
|
||||
LS_ANY = LS_LEFT | LS_RIGHT | LS_UP | LS_DOWN,
|
||||
|
||||
RS_LEFT = static_cast<u64>(HidNpadButton_StickRLeft),
|
||||
RS_RIGHT = static_cast<u64>(HidNpadButton_StickRRight),
|
||||
RS_UP = static_cast<u64>(HidNpadButton_StickRUp),
|
||||
RS_DOWN = static_cast<u64>(HidNpadButton_StickRDown),
|
||||
RS_ANY = RS_LEFT | RS_RIGHT | RS_UP | RS_DOWN,
|
||||
|
||||
ANY_LEFT = static_cast<u64>(HidNpadButton_AnyLeft),
|
||||
ANY_RIGHT = static_cast<u64>(HidNpadButton_AnyRight),
|
||||
|
||||
@@ -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
|
||||
64
sphaira/include/usb/usb_api.hpp
Normal file
64
sphaira/include/usb/usb_api.hpp
Normal 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
|
||||
37
sphaira/include/usb/usb_dumper.hpp
Normal file
37
sphaira/include/usb/usb_dumper.hpp
Normal 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
|
||||
39
sphaira/include/usb/usb_installer.hpp
Normal file
39
sphaira/include/usb/usb_installer.hpp
Normal 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
|
||||
@@ -13,25 +13,30 @@ struct Usb {
|
||||
Usb(u64 transfer_timeout);
|
||||
virtual ~Usb();
|
||||
|
||||
virtual Result Read(const std::string& path, void* buf, s64 off, s64 size, u64* bytes_read) = 0;
|
||||
virtual Result Read(void* buf, u64 off, u32 size, u64* bytes_read) = 0;
|
||||
virtual Result Open(u32 index, s64& out_size, u16& out_flags) = 0;
|
||||
|
||||
Result IsUsbConnected(u64 timeout) {
|
||||
return m_usb->IsUsbConnected(timeout);
|
||||
}
|
||||
|
||||
// waits for connection and then sends file list.
|
||||
Result WaitForConnection(u64 timeout, u8 flags, std::span<const std::string> names);
|
||||
Result WaitForConnection(u64 timeout, std::span<const std::string> names);
|
||||
|
||||
// polls for command, executes transfer if possible.
|
||||
// will return Result_Exit if exit command is recieved.
|
||||
Result PollCommands();
|
||||
|
||||
private:
|
||||
Result FileRangeCmd(u64 data_size);
|
||||
Result file_transfer_loop();
|
||||
|
||||
private:
|
||||
std::unique_ptr<usb::UsbHs> m_usb;
|
||||
std::vector<u8> m_buf;
|
||||
Result SendResult(u32 result, u32 arg3 = 0, u32 arg4 = 0);
|
||||
|
||||
private:
|
||||
std::unique_ptr<usb::UsbHs> m_usb{};
|
||||
std::vector<u8> m_buf{};
|
||||
Result m_open_result{};
|
||||
bool m_was_connected{};
|
||||
};
|
||||
|
||||
} // namespace sphaira::usb::upload
|
||||
|
||||
@@ -2,11 +2,6 @@
|
||||
|
||||
#include "base.hpp"
|
||||
|
||||
// TODO: remove these when libnx pr is merged.
|
||||
enum { UsbDeviceSpeed_None = 0x0 };
|
||||
enum { UsbDeviceSpeed_Low = 0x1 };
|
||||
Result usbDsGetSpeed(UsbDeviceSpeed *out);
|
||||
|
||||
auto GetUsbDsStateStr(UsbState state) -> const char*;
|
||||
auto GetUsbDsSpeedStr(UsbDeviceSpeed speed) -> const char*;
|
||||
|
||||
|
||||
13
sphaira/include/usbdvd.hpp
Normal file
13
sphaira/include/usbdvd.hpp
Normal 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
|
||||
81
sphaira/include/utils/audio.hpp
Normal file
81
sphaira/include/utils/audio.hpp
Normal 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
|
||||
@@ -1,15 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <switch.h>
|
||||
#include "fs.hpp"
|
||||
#include "yati/source/base.hpp"
|
||||
|
||||
#include <switch.h>
|
||||
#include <memory>
|
||||
|
||||
namespace sphaira::devoptab {
|
||||
|
||||
// mounts to "lower_case_hex_id:/"
|
||||
Result MountFromSavePath(u64 id, fs::FsPath& out_path);
|
||||
Result MountSaveSystem(u64 id, fs::FsPath& out_path);
|
||||
void UnmountSave(u64 id);
|
||||
|
||||
// todo:
|
||||
Result MountZip(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path);
|
||||
void UmountZip(const fs::FsPath& mount);
|
||||
|
||||
@@ -17,6 +19,17 @@ Result MountNsp(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path);
|
||||
void UmountNsp(const fs::FsPath& mount);
|
||||
|
||||
Result MountXci(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path);
|
||||
Result MountXciSource(const std::shared_ptr<sphaira::yati::source::Base>& source, s64 size, const fs::FsPath& path, fs::FsPath& out_path);
|
||||
void UmountXci(const fs::FsPath& mount);
|
||||
|
||||
Result MountNca(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path);
|
||||
Result MountNcaNcm(NcmContentStorage* cs, const NcmContentId* id, fs::FsPath& out_path);
|
||||
void UmountNca(const fs::FsPath& mount);
|
||||
|
||||
Result MountBfsar(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path);
|
||||
void UmountBfsar(const fs::FsPath& mount);
|
||||
|
||||
Result MountNro(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path);
|
||||
void UmountNro(const fs::FsPath& mount);
|
||||
|
||||
} // namespace sphaira::devoptab
|
||||
|
||||
@@ -1,35 +1,84 @@
|
||||
#pragma once
|
||||
|
||||
#include "yati/source/file.hpp"
|
||||
#include "utils/lru.hpp"
|
||||
#include <memory>
|
||||
#include <span>
|
||||
|
||||
namespace sphaira::devoptab::common {
|
||||
|
||||
// max entries per devoptab, should be enough.
|
||||
enum { MAX_ENTRIES = 4 };
|
||||
|
||||
// buffers data in 512k chunks to maximise throughput.
|
||||
// not suitable if random access >= 512k is common.
|
||||
// if that is needed, see the LRU cache varient used for fatfs.
|
||||
struct BufferedData final : yati::source::Base {
|
||||
static constexpr inline u64 CHUNK_SIZE = 1024 * 512;
|
||||
|
||||
BufferedData(std::unique_ptr<yati::source::Base>&& _source, u64 _size)
|
||||
: source{std::forward<decltype(_source)>(_source)}
|
||||
struct BufferedDataBase : yati::source::Base {
|
||||
BufferedDataBase(const std::shared_ptr<yati::source::Base>& _source, u64 _size)
|
||||
: source{_source}
|
||||
, capacity{_size} {
|
||||
|
||||
}
|
||||
|
||||
Result Read(void *buf, s64 off, s64 size);
|
||||
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override;
|
||||
virtual Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override {
|
||||
return source->Read(buf, off, size, bytes_read);
|
||||
}
|
||||
|
||||
protected:
|
||||
std::shared_ptr<yati::source::Base> source;
|
||||
const u64 capacity;
|
||||
};
|
||||
|
||||
// buffers data in 512k chunks to maximise throughput.
|
||||
// not suitable if random access >= 512k is common.
|
||||
// if that is needed, see the LRU cache varient used for fatfs.
|
||||
struct BufferedData : BufferedDataBase {
|
||||
BufferedData(const std::shared_ptr<yati::source::Base>& _source, u64 _size, u64 _alloc = 1024 * 512)
|
||||
: BufferedDataBase{_source, _size} {
|
||||
m_data.resize(_alloc);
|
||||
}
|
||||
|
||||
virtual Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override;
|
||||
|
||||
private:
|
||||
std::unique_ptr<yati::source::Base> source;
|
||||
const u64 capacity;
|
||||
|
||||
u64 m_off{};
|
||||
u64 m_size{};
|
||||
u8 m_data[CHUNK_SIZE]{};
|
||||
std::vector<u8> m_data{};
|
||||
};
|
||||
|
||||
struct BufferedFileData {
|
||||
u8* data{};
|
||||
u64 off{};
|
||||
u64 size{};
|
||||
|
||||
~BufferedFileData() {
|
||||
if (data) {
|
||||
free(data);
|
||||
}
|
||||
}
|
||||
|
||||
void Allocate(u64 new_size) {
|
||||
data = (u8*)realloc(data, new_size * sizeof(*data));
|
||||
off = 0;
|
||||
size = 0;
|
||||
}
|
||||
};
|
||||
|
||||
constexpr u64 CACHE_LARGE_ALLOC_SIZE = 1024 * 512;
|
||||
constexpr u64 CACHE_LARGE_SIZE = 1024 * 16;
|
||||
|
||||
struct LruBufferedData : BufferedDataBase {
|
||||
LruBufferedData(const std::shared_ptr<yati::source::Base>& _source, u64 _size, u32 small = 1024, u32 large = 2)
|
||||
: BufferedDataBase{_source, _size} {
|
||||
buffered_small.resize(small);
|
||||
buffered_large.resize(large);
|
||||
lru_cache[0].Init(buffered_small);
|
||||
lru_cache[1].Init(buffered_large);
|
||||
}
|
||||
|
||||
virtual Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override;
|
||||
|
||||
private:
|
||||
utils::Lru<BufferedFileData> lru_cache[2]{};
|
||||
std::vector<BufferedFileData> buffered_small{}; // 1MiB (usually).
|
||||
std::vector<BufferedFileData> buffered_large{}; // 1MiB
|
||||
};
|
||||
|
||||
bool fix_path(const char* str, char* out);
|
||||
|
||||
41
sphaira/include/utils/devoptab_romfs.hpp
Normal file
41
sphaira/include/utils/devoptab_romfs.hpp
Normal 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
|
||||
76
sphaira/include/utils/lru.hpp
Normal file
76
sphaira/include/utils/lru.hpp
Normal 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
|
||||
23
sphaira/include/utils/nsz_dumper.hpp
Normal file
23
sphaira/include/utils/nsz_dumper.hpp
Normal 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
|
||||
@@ -5,12 +5,16 @@
|
||||
|
||||
namespace sphaira::utils {
|
||||
|
||||
struct ScopedTimestampProfile {
|
||||
struct ScopedTimestampProfile final {
|
||||
ScopedTimestampProfile(const std::string& name) : m_name{name} {
|
||||
|
||||
}
|
||||
|
||||
~ScopedTimestampProfile() {
|
||||
Log();
|
||||
}
|
||||
|
||||
void Log() {
|
||||
log_write("\t[%s] time taken: %.2fs %.2fms\n", m_name.c_str(), m_ts.GetSecondsD(), m_ts.GetMsD());
|
||||
}
|
||||
|
||||
|
||||
58
sphaira/include/utils/thread.hpp
Normal file
58
sphaira/include/utils/thread.hpp
Normal 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
|
||||
25
sphaira/include/utils/utils.hpp
Normal file
25
sphaira/include/utils/utils.hpp
Normal 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
|
||||
@@ -9,9 +9,10 @@ namespace sphaira::yati::container {
|
||||
struct Nsp final : Base {
|
||||
using Base::Base;
|
||||
Result GetCollections(Collections& out) override;
|
||||
Result GetCollections(Collections& out, s64 off);
|
||||
|
||||
// builds nsp meta data and the size of the entier nsp.
|
||||
static auto Build(std::span<CollectionEntry> collections, s64& size) -> std::vector<u8>;
|
||||
static auto Build(std::span<const CollectionEntry> collections, s64& size) -> std::vector<u8>;
|
||||
};
|
||||
|
||||
} // namespace sphaira::yati::container
|
||||
|
||||
@@ -9,18 +9,62 @@ namespace sphaira::yati::container {
|
||||
|
||||
struct Xci final : Base {
|
||||
|
||||
struct Hfs0Header {
|
||||
u32 magic;
|
||||
u32 total_files;
|
||||
u32 string_table_size;
|
||||
u32 padding;
|
||||
};
|
||||
|
||||
struct Hfs0FileTableEntry {
|
||||
u64 data_offset;
|
||||
u64 data_size;
|
||||
u32 name_offset;
|
||||
u32 hash_size;
|
||||
u64 padding;
|
||||
u8 hash[0x20];
|
||||
};
|
||||
|
||||
struct Hfs0 {
|
||||
Hfs0Header header{};
|
||||
std::vector<Hfs0FileTableEntry> file_table{};
|
||||
std::vector<std::string> string_table{};
|
||||
s64 data_offset{};
|
||||
|
||||
auto GetHfs0Size() const {
|
||||
return sizeof(header) + file_table.size() * sizeof(Hfs0FileTableEntry) + header.string_table_size;
|
||||
}
|
||||
|
||||
auto GetHfs0Data() const -> std::vector<u8>;
|
||||
};
|
||||
|
||||
struct Partition {
|
||||
// name of the partition.
|
||||
std::string name;
|
||||
// offset of this hfs0.
|
||||
s64 hfs0_offset;
|
||||
s64 hfs0_size;
|
||||
Hfs0 hfs0;
|
||||
// all the collections for this partition, may be empty.
|
||||
Collections collections;
|
||||
};
|
||||
|
||||
struct Root {
|
||||
// offset of this hfs0.
|
||||
s64 hfs0_offset;
|
||||
Hfs0 hfs0;
|
||||
std::vector<Partition> partitions;
|
||||
};
|
||||
|
||||
using Partitions = std::vector<Partition>;
|
||||
|
||||
using Base::Base;
|
||||
Result GetCollections(Collections& out) override;
|
||||
Result GetPartitions(Partitions& out);
|
||||
Result GetRoot(Root& out);
|
||||
|
||||
private:
|
||||
Result Hfs0GetPartition(source::Base* source, s64 off, Hfs0& out);
|
||||
Result ReadPartitionFromHfs0(source::Base* source, const Hfs0& root, u32 index, Partition& out);
|
||||
};
|
||||
|
||||
} // namespace sphaira::yati::container
|
||||
|
||||
@@ -54,4 +54,15 @@ static inline void cryptoAes128Xts(const void* in, void* out, const u8* key, u64
|
||||
Aes128Xts(key, is_encryptor).Run(out, in, sector, sector_size, data_size);
|
||||
}
|
||||
|
||||
static inline void UpdateCtr(u8* counter, u64 offset) {
|
||||
const u64 swp = __bswap64(offset >> 4);
|
||||
std::memcpy(&counter[0x8], &swp, 0x8);
|
||||
}
|
||||
|
||||
static inline void SetCtr(u8* counter, u64 ctr, u64 offset = 0) {
|
||||
const u64 swp = __bswap64(ctr);
|
||||
std::memcpy(&counter[0x0], &swp, 0x8);
|
||||
UpdateCtr(counter, offset);
|
||||
}
|
||||
|
||||
} // namespace sphaira::crypto
|
||||
|
||||
@@ -201,7 +201,8 @@ Result EncryptTitleKey(keys::KeyEntry& out, u8 key_gen, const keys::Keys& keys);
|
||||
|
||||
Result ShouldPatchTicket(const TicketData& data, std::span<const u8> ticket, std::span<const u8> cert_chain, bool patch_personalised, bool& should_patch);
|
||||
Result ShouldPatchTicket(std::span<const u8> ticket, std::span<const u8> cert_chain, bool patch_personalised, bool& should_patch);
|
||||
Result PatchTicket(std::vector<u8>& ticket, std::span<const u8> cert_chain, u8 key_gen, const keys::Keys& keys, bool patch_personalised);
|
||||
// cert chain may be modified if the ticket is converted to common ticket.
|
||||
Result PatchTicket(std::vector<u8>& ticket, std::vector<u8>& cert_chain, u8 key_gen, const keys::Keys& keys, bool patch_personalised);
|
||||
|
||||
// fills out with the list of common / personalised rights ids.
|
||||
Result GetCommonTickets(std::vector<FsRightsId>& out);
|
||||
@@ -218,6 +219,11 @@ bool IsRightsIdFound(const FsRightsId& id, std::span<const FsRightsId> ids);
|
||||
// wrapper around ipc.
|
||||
Result GetCommonTicketAndCertificate(const FsRightsId& rights_id, std::vector<u8>& tik_out, std::vector<u8>& cert_out);
|
||||
// fetches data from system es save.
|
||||
Result GetPersonalisedTicketData(u64 *size_out, void *tik_data, u64 tik_size, const FsRightsId* rightsId);
|
||||
Result GetPersonalisedTicketAndCertificate(const FsRightsId& rights_id, std::vector<u8>& tik_out, std::vector<u8>& cert_out);
|
||||
|
||||
// fills out with the decrypted title key.
|
||||
Result GetTitleKeyDecrypted(const FsRightsId& rights_id, u8 key_gen, const keys::Keys& keys, keys::KeyEntry& out);
|
||||
Result GetTitleKeyDecrypted(std::span<const u8> ticket, const FsRightsId& rights_id, u8 key_gen, const keys::Keys& keys, keys::KeyEntry& out);
|
||||
|
||||
} // namespace sphaira::es
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
#include "fs.hpp"
|
||||
#include "keys.hpp"
|
||||
#include "ncm.hpp"
|
||||
#include "yati/source/base.hpp"
|
||||
|
||||
#include <switch.h>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
|
||||
namespace sphaira::nca {
|
||||
|
||||
@@ -15,7 +18,7 @@ namespace sphaira::nca {
|
||||
#define NCA_SECTOR_SIZE 0x200
|
||||
#define NCA_XTS_SECTION_SIZE 0xC00
|
||||
#define NCA_SECTION_TOTAL 0x4
|
||||
#define NCA_MEDIA_REAL(x)((x * 0x200))
|
||||
#define NCA_MEDIA_REAL(x)((u64(x) * 0x200))
|
||||
|
||||
#define NCA_PROGRAM_LOGO_OFFSET 0x8000
|
||||
#define NCA_META_CNMT_OFFSET 0xC20
|
||||
@@ -94,6 +97,22 @@ struct SectionTableEntry {
|
||||
u32 media_end_offset; // divided by 0x200.
|
||||
u8 _0x8[0x4]; // unknown.
|
||||
u8 _0xC[0x4]; // unknown.
|
||||
|
||||
auto IsValid() const -> bool {
|
||||
return media_start_offset && media_end_offset;
|
||||
}
|
||||
|
||||
auto GetOffset() const -> u64 {
|
||||
return NCA_MEDIA_REAL(media_start_offset);
|
||||
}
|
||||
|
||||
auto GetOffsetEnd() const -> u64 {
|
||||
return NCA_MEDIA_REAL(media_end_offset);
|
||||
}
|
||||
|
||||
auto GetSize() const -> u64 {
|
||||
return GetOffsetEnd() - GetOffset();
|
||||
}
|
||||
};
|
||||
|
||||
struct LayerRegion {
|
||||
@@ -139,6 +158,55 @@ static_assert(sizeof(HierarchicalSha256Data) == 0xF8);
|
||||
static_assert(sizeof(IntegrityMetaInfo) == 0xF8);
|
||||
static_assert(sizeof(HierarchicalSha256Data) == sizeof(IntegrityMetaInfo));
|
||||
|
||||
struct BucketTreeHeader {
|
||||
u32 magic; // BKTR
|
||||
u32 version;
|
||||
u32 count;
|
||||
u8 _0xC[0x4];
|
||||
};
|
||||
|
||||
struct PatchInfo {
|
||||
u64 indirect_offset;
|
||||
u64 indirect_size;
|
||||
BucketTreeHeader indirect_header;
|
||||
u64 aes_ctr_offset;
|
||||
u64 aes_ctr_size;
|
||||
BucketTreeHeader aes_ctr_header;
|
||||
};
|
||||
static_assert(sizeof(PatchInfo) == 0x40);
|
||||
|
||||
struct CompressionInfo {
|
||||
u64 table_offset;
|
||||
u64 table_size;
|
||||
BucketTreeHeader table_header;
|
||||
u8 _0x20[0x8];
|
||||
};
|
||||
static_assert(sizeof(CompressionInfo) == 0x28);
|
||||
|
||||
struct BktrEntry {
|
||||
u8 _0x0[0x4];
|
||||
u32 count;
|
||||
u64 size;
|
||||
u64 offsets[0x3FF0 / sizeof(u64)];
|
||||
};
|
||||
static_assert(sizeof(BktrEntry) == 0x4000);
|
||||
|
||||
struct NX_PACKED BktrRelocationEntry {
|
||||
u64 patched_addr;
|
||||
u64 source_addr;
|
||||
u32 flag;
|
||||
};
|
||||
static_assert(sizeof(BktrRelocationEntry) == 0x14);
|
||||
|
||||
struct BktrRelocationBucket {
|
||||
u8 _0x0[0x4];
|
||||
u32 count;
|
||||
u64 end_offset;
|
||||
BktrRelocationEntry entries[0x3FF0 / sizeof(BktrRelocationEntry)];
|
||||
u8 _[0x3FF0 % sizeof(BktrRelocationEntry)];
|
||||
};
|
||||
static_assert(sizeof(BktrRelocationBucket) == 0x4000);
|
||||
|
||||
struct FsHeader {
|
||||
u16 version; // always 2.
|
||||
u8 fs_type; // see FileSystemType.
|
||||
@@ -152,12 +220,16 @@ struct FsHeader {
|
||||
IntegrityMetaInfo integrity_meta_info; // used for romfs
|
||||
} hash_data;
|
||||
|
||||
u8 patch_info[0x40];
|
||||
PatchInfo patch_info;
|
||||
u64 section_ctr;
|
||||
u8 spares_info[0x30];
|
||||
u8 compression_info[0x28];
|
||||
CompressionInfo compression_info;
|
||||
u8 meta_data_hash_data_info[0x30];
|
||||
u8 reserved[0x30];
|
||||
|
||||
auto IsValid() const -> bool {
|
||||
return version == 2;
|
||||
}
|
||||
};
|
||||
static_assert(sizeof(FsHeader) == 0x200);
|
||||
static_assert(sizeof(FsHeader::hash_data) == 0xF8);
|
||||
@@ -203,6 +275,10 @@ struct Header {
|
||||
|
||||
FsHeader fs_header[NCA_SECTION_TOTAL];
|
||||
|
||||
auto IsValid() const -> bool {
|
||||
return magic == NCA3_MAGIC;
|
||||
}
|
||||
|
||||
auto GetKeyGeneration() const -> u8 {
|
||||
if (old_key_gen < key_gen) {
|
||||
return key_gen;
|
||||
@@ -220,12 +296,25 @@ struct Header {
|
||||
key_gen = key_generation;
|
||||
}
|
||||
}
|
||||
|
||||
auto GetSectionCount() const -> u8 {
|
||||
u8 count = 0;
|
||||
for (u32 i = 0; i < NCA_SECTION_TOTAL; i++) {
|
||||
if (!fs_header[i].IsValid() || !fs_table[i].IsValid()) {
|
||||
break;
|
||||
}
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
};
|
||||
static_assert(sizeof(Header) == 0xC00);
|
||||
|
||||
auto GetContentTypeStr(u8 content_type) -> const char*;
|
||||
auto GetDistributionTypeStr(u8 distribution_type) -> const char*;
|
||||
|
||||
Result DecryptHeader(const void* in, const keys::Keys& keys, Header& out);
|
||||
|
||||
Result DecryptKeak(const keys::Keys& keys, Header& header);
|
||||
Result EncryptKeak(const keys::Keys& keys, Header& header, u8 key_generation);
|
||||
Result VerifyFixedKey(const Header& header);
|
||||
@@ -236,4 +325,52 @@ Result ParseControl(const fs::FsPath& path, u64 program_id, void* nacp_out = nul
|
||||
|
||||
auto GetKeyGenStr(u8 key_gen) -> const char*;
|
||||
|
||||
// finds and decrypts the title key, also decrypts header key area if needed.
|
||||
Result GetDecryptedTitleKey(Header& header, const keys::Keys& keys, keys::KeyEntry& out);
|
||||
// same as above but also checks the path for ticket.
|
||||
Result GetDecryptedTitleKey(fs::Fs* fs, const fs::FsPath& path, Header& header, const keys::Keys& keys, keys::KeyEntry& out);
|
||||
|
||||
// helpers.
|
||||
struct DecyptedData : yati::source::Base {
|
||||
DecyptedData(u64 align, const std::shared_ptr<yati::source::Base>& source);
|
||||
Result Read(void *_buf, s64 _off, s64 _size, u64* _bytes_read) override;
|
||||
virtual Result SetCtr(u64 ctr) = 0;
|
||||
|
||||
private:
|
||||
virtual Result Decrypt(void* buf, s64 off, s64 size) = 0;
|
||||
|
||||
private:
|
||||
std::shared_ptr<yati::source::Base> m_source;
|
||||
const u64 m_align;
|
||||
};
|
||||
|
||||
// todo: add support for xts sections.
|
||||
struct DecyptedDataCtr final : DecyptedData {
|
||||
DecyptedDataCtr(const void* key, u64 ctr, const std::shared_ptr<yati::source::Base>& source);
|
||||
Result SetCtr(u64 ctr) override;
|
||||
|
||||
private:
|
||||
Result Decrypt(void* buf, s64 off, s64 size) override;
|
||||
|
||||
private:
|
||||
Aes128CtrContext m_ctx{};
|
||||
u8 m_ctr[AES_BLOCK_SIZE]{};
|
||||
};
|
||||
|
||||
struct NcaReader final : yati::source::Base {
|
||||
NcaReader(const nca::Header& decrypted_header, const void* key, u64 size, const std::shared_ptr<yati::source::Base>& source);
|
||||
Result Read(void *_buf, s64 off, s64 size, u64* bytes_read) override;
|
||||
Result ReadEncrypted(void *_buf, s64 off, s64 size, u64* bytes_read);
|
||||
|
||||
private:
|
||||
Result ReadInternal(void *_buf, s64 off, s64 size, u64* bytes_read, bool decrypt);
|
||||
|
||||
private:
|
||||
const nca::Header m_header;
|
||||
const u64 m_capacity;
|
||||
std::shared_ptr<yati::source::Base> m_source;
|
||||
std::unique_ptr<DecyptedData> m_decryptor{};
|
||||
u8 m_key[0x10]{};
|
||||
};
|
||||
|
||||
} // namespace sphaira::nca
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "fs.hpp"
|
||||
#include "yati/source/base.hpp"
|
||||
|
||||
#include <switch.h>
|
||||
#include <vector>
|
||||
@@ -82,4 +83,16 @@ static constexpr inline bool HasRequiredSystemVersion(const NcmContentMetaKey *k
|
||||
// fills program id and out path of the control nca.
|
||||
Result GetFsPathFromContentId(NcmContentStorage* cs, const NcmContentMetaKey& key, const NcmContentId& id, u64* out_program_id, fs::FsPath* out_path);
|
||||
|
||||
// helper for reading nca from ncm.
|
||||
struct NcmSource final : yati::source::Base {
|
||||
NcmSource(NcmContentStorage* cs, const NcmContentId* id);
|
||||
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override;
|
||||
Result GetSize(s64* size);
|
||||
|
||||
private:
|
||||
NcmContentStorage m_cs;
|
||||
NcmContentId m_id;
|
||||
s64 m_size{};
|
||||
};
|
||||
|
||||
} // namespace sphaira::ncm
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include "yati/source/base.hpp"
|
||||
#include "utils/lru.hpp"
|
||||
#include "defines.hpp"
|
||||
|
||||
#include <switch.h>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <zstd.h>
|
||||
|
||||
namespace sphaira::ncz {
|
||||
|
||||
@@ -8,7 +15,11 @@ namespace sphaira::ncz {
|
||||
// todo: byteswap this
|
||||
#define NCZ_BLOCK_MAGIC std::byteswap(0x4E435A424C4F434BUL)
|
||||
|
||||
#define NCZ_SECTION_OFFSET (0x4000 + sizeof(ncz::Header))
|
||||
#define NCZ_BLOCK_VERSION (2)
|
||||
#define NCZ_BLOCK_TYPE (1)
|
||||
|
||||
#define NCZ_NORMAL_SIZE (0x4000)
|
||||
#define NCZ_SECTION_OFFSET (NCZ_NORMAL_SIZE + sizeof(ncz::Header))
|
||||
|
||||
struct Header {
|
||||
u64 magic; // NCZ_SECTION_MAGIC
|
||||
@@ -23,11 +34,21 @@ struct BlockHeader {
|
||||
u8 block_size_exponent;
|
||||
u32 total_blocks;
|
||||
u64 decompressed_size;
|
||||
|
||||
Result IsValid() const {
|
||||
R_UNLESS(magic == NCZ_BLOCK_MAGIC, 9);
|
||||
R_UNLESS(version == NCZ_BLOCK_VERSION, Result_YatiInvalidNczBlockVersion);
|
||||
R_UNLESS(type == NCZ_BLOCK_TYPE, Result_YatiInvalidNczBlockType);
|
||||
R_UNLESS(total_blocks, Result_YatiInvalidNczBlockTotal);
|
||||
R_UNLESS(block_size_exponent >= 14 && block_size_exponent <= 32, Result_YatiInvalidNczBlockSizeExponent);
|
||||
R_SUCCEED();
|
||||
}
|
||||
};
|
||||
|
||||
struct Block {
|
||||
u32 size;
|
||||
};
|
||||
using Blocks = std::vector<Block>;
|
||||
|
||||
struct BlockInfo {
|
||||
u64 offset; // compressed offset.
|
||||
@@ -50,5 +71,39 @@ struct Section {
|
||||
return off < offset + size && off >= offset;
|
||||
}
|
||||
};
|
||||
using Sections = std::vector<Section>;
|
||||
|
||||
struct NczBlockReader final : yati::source::Base {
|
||||
explicit NczBlockReader(const Header& header, const Sections& sections, const BlockHeader& block_header, const Blocks& blocks, u64 offset, const std::shared_ptr<yati::source::Base>& source);
|
||||
Result Read(void *_buf, s64 off, s64 size, u64* bytes_read) override;
|
||||
|
||||
private:
|
||||
struct LruData {
|
||||
s64 offset{};
|
||||
std::vector<u8> data{};
|
||||
|
||||
auto InRange(u64 off) const -> bool {
|
||||
return off < offset + data.size() && off >= offset;
|
||||
}
|
||||
};
|
||||
|
||||
private:
|
||||
Result ReadInternal(void *_buf, s64 off, s64 size, u64* bytes_read, bool decrypt);
|
||||
|
||||
private:
|
||||
const Header m_header;
|
||||
const Sections m_sections;
|
||||
const BlockHeader m_block_header;
|
||||
const Blocks m_blocks;
|
||||
const u64 m_block_offset;
|
||||
std::shared_ptr<yati::source::Base> m_source;
|
||||
|
||||
u32 m_block_size{};
|
||||
std::vector<BlockInfo> m_block_infos{};
|
||||
|
||||
// lru cache of blocks
|
||||
std::vector<LruData> m_lru_data{};
|
||||
utils::Lru<LruData> m_lru{};
|
||||
};
|
||||
|
||||
} // namespace sphaira::ncz
|
||||
|
||||
@@ -10,6 +10,11 @@ struct Base {
|
||||
// virtual Result Read(void* buf, s64 off, s64 size, u64* bytes_read) = 0;
|
||||
virtual Result Read(void* buf, s64 off, s64 size, u64* bytes_read) = 0;
|
||||
|
||||
Result Read2(void* buf, s64 off, s64 size) {
|
||||
u64 bytes_read;
|
||||
return Read(buf, off, size, &bytes_read);
|
||||
}
|
||||
|
||||
virtual bool IsStream() const {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,42 +1,50 @@
|
||||
#pragma once
|
||||
|
||||
#include "base.hpp"
|
||||
#include "fs.hpp"
|
||||
#include "usb/usbds.hpp"
|
||||
#include "usb/usb_installer.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <switch.h>
|
||||
|
||||
namespace sphaira::yati::source {
|
||||
|
||||
struct Usb final : Base {
|
||||
Usb(u64 transfer_timeout);
|
||||
~Usb();
|
||||
Usb(u64 transfer_timeout) {
|
||||
m_usb = std::make_unique<usb::install::Usb>(transfer_timeout);
|
||||
}
|
||||
|
||||
bool IsStream() const override;
|
||||
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override;
|
||||
Result Finished(u64 timeout);
|
||||
void SignalCancel() override {
|
||||
m_usb->SignalCancel();
|
||||
}
|
||||
|
||||
bool IsStream() const override {
|
||||
return m_usb->GetFlags() & usb::api::FLAG_STREAM;
|
||||
}
|
||||
|
||||
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override {
|
||||
return m_usb->Read(buf, off, size, bytes_read);
|
||||
}
|
||||
|
||||
Result IsUsbConnected(u64 timeout) {
|
||||
return m_usb->IsUsbConnected(timeout);
|
||||
}
|
||||
|
||||
Result WaitForConnection(u64 timeout, std::vector<std::string>& out_names);
|
||||
void SetFileNameForTranfser(const std::string& name);
|
||||
Result WaitForConnection(u64 timeout, std::vector<std::string>& names) {
|
||||
return m_usb->WaitForConnection(timeout, names);
|
||||
}
|
||||
|
||||
void SignalCancel() override {
|
||||
m_usb->Cancel();
|
||||
Result OpenFile(u32 index, s64& file_size) {
|
||||
return m_usb->OpenFile(index, file_size);
|
||||
}
|
||||
|
||||
Result CloseFile() {
|
||||
return m_usb->CloseFile();
|
||||
}
|
||||
|
||||
private:
|
||||
Result SendCmdHeader(u32 cmdId, size_t dataSize, u64 timeout);
|
||||
Result SendFileRangeCmd(u64 offset, u64 size, u64 timeout);
|
||||
|
||||
private:
|
||||
std::unique_ptr<usb::UsbDs> m_usb;
|
||||
std::string m_transfer_file_name{};
|
||||
u8 m_flags{};
|
||||
std::unique_ptr<usb::install::Usb> m_usb{};
|
||||
};
|
||||
|
||||
} // namespace sphaira::yati::source
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@
|
||||
#include "evman.hpp"
|
||||
#include "fs.hpp"
|
||||
#include "app.hpp"
|
||||
#include "utils/thread.hpp"
|
||||
|
||||
#include <switch.h>
|
||||
#include <cstring>
|
||||
@@ -32,8 +33,8 @@ namespace {
|
||||
constexpr auto API_AGENT = "TotalJustice";
|
||||
constexpr u64 CHUNK_SIZE = 1024*1024;
|
||||
constexpr auto MAX_THREADS = 4;
|
||||
constexpr int THREAD_PRIO = PRIO_PREEMPTIVE;
|
||||
constexpr int THREAD_CORE = 1;
|
||||
constexpr int THREAD_PRIO = 0x2F;
|
||||
constexpr int THREAD_CORE = 2;
|
||||
|
||||
std::atomic_bool g_running{};
|
||||
CURLSH* g_curl_share{};
|
||||
@@ -262,14 +263,17 @@ struct ThreadEntry {
|
||||
R_UNLESS(m_curl != nullptr, Result_CurlFailedEasyInit);
|
||||
|
||||
ueventCreate(&m_uevent, true);
|
||||
R_TRY(threadCreate(&m_thread, ThreadFunc, this, nullptr, 1024*32, THREAD_PRIO, THREAD_CORE));
|
||||
R_TRY(svcSetThreadCoreMask(m_thread.handle, THREAD_CORE, THREAD_AFFINITY_DEFAULT(THREAD_CORE)));
|
||||
R_TRY(utils::CreateThread(&m_thread, ThreadFunc, this, 1024*32));
|
||||
R_TRY(threadStart(&m_thread));
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
void Close() {
|
||||
void SignalClose() {
|
||||
ueventSignal(&m_uevent);
|
||||
}
|
||||
|
||||
void Close() {
|
||||
SignalClose();
|
||||
threadWaitForExit(&m_thread);
|
||||
threadClose(&m_thread);
|
||||
if (m_curl) {
|
||||
@@ -320,13 +324,17 @@ struct ThreadQueue {
|
||||
|
||||
auto Create() -> Result {
|
||||
ueventCreate(&m_uevent, true);
|
||||
R_TRY(threadCreate(&m_thread, ThreadFunc, this, nullptr, 1024*32, THREAD_PRIO, THREAD_CORE));
|
||||
R_TRY(utils::CreateThread(&m_thread, ThreadFunc, this, 1024*32));
|
||||
R_TRY(threadStart(&m_thread));
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
void Close() {
|
||||
void SignalClose() {
|
||||
ueventSignal(&m_uevent);
|
||||
}
|
||||
|
||||
void Close() {
|
||||
SignalClose();
|
||||
threadWaitForExit(&m_thread);
|
||||
threadClose(&m_thread);
|
||||
}
|
||||
@@ -1049,6 +1057,12 @@ void ThreadEntry::ThreadFunc(void* p) {
|
||||
|
||||
void ThreadQueue::ThreadFunc(void* p) {
|
||||
auto data = static_cast<ThreadQueue*>(p);
|
||||
|
||||
if (!g_cache.init()) {
|
||||
log_write("failed to init json cache\n");
|
||||
}
|
||||
ON_SCOPE_EXIT(g_cache.exit());
|
||||
|
||||
while (g_running) {
|
||||
auto rc = waitSingle(waiterForUEvent(&data->m_uevent), UINT64_MAX);
|
||||
log_write("[thread queue] woke up\n");
|
||||
@@ -1141,16 +1155,22 @@ auto Init() -> bool {
|
||||
|
||||
log_write("finished creating threads\n");
|
||||
|
||||
if (!g_cache.init()) {
|
||||
log_write("failed to init json cache\n");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Exit() {
|
||||
void ExitSignal() {
|
||||
g_running = false;
|
||||
|
||||
g_thread_queue.SignalClose();
|
||||
|
||||
for (auto& entry : g_threads) {
|
||||
entry.SignalClose();
|
||||
}
|
||||
}
|
||||
|
||||
void Exit() {
|
||||
ExitSignal();
|
||||
|
||||
g_thread_queue.Close();
|
||||
|
||||
if (g_curl_single) {
|
||||
@@ -1168,7 +1188,6 @@ void Exit() {
|
||||
}
|
||||
|
||||
curl_global_cleanup();
|
||||
g_cache.exit();
|
||||
}
|
||||
|
||||
auto ToMemory(const Api& e) -> ApiResult {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "i18n.hpp"
|
||||
#include "location.hpp"
|
||||
#include "threaded_file_transfer.hpp"
|
||||
#include "haze_helper.hpp"
|
||||
|
||||
#include "ui/sidebar.hpp"
|
||||
#include "ui/error_box.hpp"
|
||||
@@ -18,29 +19,179 @@
|
||||
#include "yati/source/stream.hpp"
|
||||
|
||||
#include "usb/usb_uploader.hpp"
|
||||
#include "usb/tinfoil.hpp"
|
||||
#include "usb/usb_dumper.hpp"
|
||||
#include "usb/usbds.hpp"
|
||||
|
||||
namespace sphaira::dump {
|
||||
namespace {
|
||||
|
||||
struct ZipInternal {
|
||||
WriteSource* writer;
|
||||
s64 offset;
|
||||
s64 size;
|
||||
Result rc;
|
||||
};
|
||||
|
||||
voidpf zopen64_file(voidpf opaque, const void* filename, int mode)
|
||||
{
|
||||
ZipInternal* fs = (ZipInternal*)calloc(1, sizeof(*fs));
|
||||
fs->writer = (WriteSource*)opaque;
|
||||
return fs;
|
||||
}
|
||||
|
||||
ZPOS64_T ztell64_file(voidpf opaque, voidpf stream)
|
||||
{
|
||||
auto fs = (ZipInternal*)stream;
|
||||
return fs->offset;
|
||||
}
|
||||
|
||||
long zseek64_file(voidpf opaque, voidpf stream, ZPOS64_T offset, int origin)
|
||||
{
|
||||
auto fs = (ZipInternal*)stream;
|
||||
switch (origin) {
|
||||
case SEEK_SET: {
|
||||
fs->offset = offset;
|
||||
} break;
|
||||
case SEEK_CUR: {
|
||||
fs->offset += offset;
|
||||
} break;
|
||||
case SEEK_END: {
|
||||
fs->offset = fs->size + offset;
|
||||
} break;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
uLong zwrite_file(voidpf opaque, voidpf stream, const void* buf, uLong size) {
|
||||
auto fs = (ZipInternal*)stream;
|
||||
|
||||
if (R_FAILED(fs->rc = fs->writer->Write(buf, fs->offset, size))) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
fs->offset += size;
|
||||
fs->size = std::max(fs->size, fs->offset);
|
||||
return size;
|
||||
}
|
||||
|
||||
int zclose_file(voidpf opaque, voidpf stream) {
|
||||
if (stream) {
|
||||
auto fs = (ZipInternal*)stream;
|
||||
std::memset(fs, 0, sizeof(*fs));
|
||||
std::free(fs);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int zerror_file(voidpf opaque, voidpf stream) {
|
||||
auto fs = (ZipInternal*)stream;
|
||||
if (R_FAILED(fs->rc)) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
constexpr zlib_filefunc64_def zlib_filefunc = {
|
||||
.zopen64_file = zopen64_file,
|
||||
.zwrite_file = zwrite_file,
|
||||
.ztell64_file = ztell64_file,
|
||||
.zseek64_file = zseek64_file,
|
||||
.zclose_file = zclose_file,
|
||||
.zerror_file = zerror_file,
|
||||
};
|
||||
|
||||
struct DumpLocationEntry {
|
||||
const DumpLocationType type;
|
||||
const char* name;
|
||||
};
|
||||
|
||||
struct WriteFileSource final : WriteSource {
|
||||
WriteFileSource(fs::File* file) : m_file{file} {
|
||||
}
|
||||
|
||||
Result Write(const void* buf, s64 off, s64 size) override {
|
||||
return m_file->Write(off, buf, size, FsWriteOption_None);
|
||||
}
|
||||
|
||||
Result SetSize(s64 size) override {
|
||||
return m_file->SetSize(size);
|
||||
}
|
||||
|
||||
private:
|
||||
fs::File* m_file;
|
||||
};
|
||||
|
||||
struct WriteNullSource final : WriteSource {
|
||||
Result Write(const void* buf, s64 off, s64 size) override {
|
||||
R_SUCCEED();
|
||||
}
|
||||
Result SetSize(s64 size) override {
|
||||
R_SUCCEED();
|
||||
}
|
||||
};
|
||||
|
||||
struct WriteUsbSource final : WriteSource {
|
||||
WriteUsbSource(u64 transfer_timeout) {
|
||||
// disable mtp if enabled.
|
||||
m_was_mtp_enabled = haze::IsInit();
|
||||
if (m_was_mtp_enabled) {
|
||||
haze::Exit();
|
||||
}
|
||||
|
||||
m_usb = std::make_unique<usb::dump::Usb>(transfer_timeout);
|
||||
}
|
||||
|
||||
~WriteUsbSource() {
|
||||
m_usb.reset();
|
||||
|
||||
if (m_was_mtp_enabled) {
|
||||
haze::Init();
|
||||
}
|
||||
}
|
||||
|
||||
Result WaitForConnection(std::string_view path, u64 timeout) {
|
||||
return m_usb->WaitForConnection(path, timeout);
|
||||
}
|
||||
|
||||
Result CloseFile() {
|
||||
return m_usb->CloseFile();
|
||||
}
|
||||
|
||||
Result Write(const void* buf, s64 off, s64 size) override {
|
||||
return m_usb->Write(buf, off, size);
|
||||
}
|
||||
|
||||
Result SetSize(s64 size) override {
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
private:
|
||||
std::unique_ptr<usb::dump::Usb> m_usb{};
|
||||
bool m_was_mtp_enabled{};
|
||||
};
|
||||
|
||||
constexpr DumpLocationEntry DUMP_LOCATIONS[]{
|
||||
{ DumpLocationType_SdCard, "microSD card (/dumps/)" },
|
||||
#if ENABLE_NETWORK_INSTALL
|
||||
{ DumpLocationType_Usb, "USB export to PC (usb_export.py)" },
|
||||
{ DumpLocationType_UsbS2S, "USB transfer (Switch 2 Switch)" },
|
||||
#endif
|
||||
{ DumpLocationType_DevNull, "/dev/null (Speed Test)" },
|
||||
};
|
||||
|
||||
#if ENABLE_NETWORK_INSTALL
|
||||
struct UsbTest final : usb::upload::Usb, yati::source::Stream {
|
||||
UsbTest(ui::ProgressBox* pbox, BaseSource* source) : Usb{UINT64_MAX} {
|
||||
m_pbox = pbox;
|
||||
m_source = source;
|
||||
UsbTest(ui::ProgressBox* pbox, BaseSource* source, std::span<const fs::FsPath> paths)
|
||||
: Usb{UINT64_MAX}
|
||||
, m_pbox{pbox}
|
||||
, m_source{source}
|
||||
, m_paths{paths} {
|
||||
|
||||
}
|
||||
|
||||
auto& GetPath(u32 index) const {
|
||||
return m_paths[index];
|
||||
}
|
||||
|
||||
auto& GetPath() const {
|
||||
return GetPath(m_current_file_index);
|
||||
}
|
||||
|
||||
Result ReadChunk(void* buf, s64 size, u64* bytes_read) override {
|
||||
@@ -49,26 +200,33 @@ struct UsbTest final : usb::upload::Usb, yati::source::Stream {
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Read(const std::string& path, void* buf, s64 off, s64 size, u64* bytes_read) override {
|
||||
Result Read(void* buf, u64 off, u32 size, u64* bytes_read) override {
|
||||
if (m_pull) {
|
||||
return Stream::Read(buf, off, size, bytes_read);
|
||||
} else {
|
||||
return ReadInternal(path, buf, off, size, bytes_read);
|
||||
return ReadInternal(GetPath(), buf, off, size, bytes_read);
|
||||
}
|
||||
}
|
||||
|
||||
Result ReadInternal(const std::string& path, void* buf, s64 off, s64 size, u64* bytes_read) {
|
||||
if (m_path != path) {
|
||||
m_path = path;
|
||||
m_progress = 0;
|
||||
m_pull_offset = 0;
|
||||
Stream::Reset();
|
||||
m_size = m_source->GetSize(path);
|
||||
m_pbox->SetImage(m_source->GetIcon(path));
|
||||
m_pbox->SetTitle(m_source->GetName(path));
|
||||
m_pbox->NewTransfer(m_path);
|
||||
}
|
||||
Result Open(u32 index, s64& out_size, u16& out_flags) override {
|
||||
const auto path = m_paths[index];
|
||||
const auto size = m_source->GetSize(path);
|
||||
|
||||
m_progress = 0;
|
||||
m_pull_offset = 0;
|
||||
Stream::Reset();
|
||||
m_size = size;
|
||||
m_pbox->SetImage(m_source->GetIcon(path));
|
||||
m_pbox->SetTitle(m_source->GetName(path));
|
||||
m_pbox->NewTransfer(path);
|
||||
|
||||
m_current_file_index = index;
|
||||
out_size = size;
|
||||
out_flags = usb::api::FLAG_STREAM;
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result ReadInternal(const std::string& path, void* buf, s64 off, s64 size, u64* bytes_read) {
|
||||
R_TRY(m_source->Read(path, buf, off, size, bytes_read));
|
||||
|
||||
m_offset += *bytes_read;
|
||||
@@ -93,16 +251,55 @@ struct UsbTest final : usb::upload::Usb, yati::source::Stream {
|
||||
private:
|
||||
ui::ProgressBox* m_pbox{};
|
||||
BaseSource* m_source{};
|
||||
std::string m_path{};
|
||||
std::span<const fs::FsPath> m_paths{};
|
||||
thread::PullCallback m_pull{};
|
||||
s64 m_offset{};
|
||||
s64 m_size{};
|
||||
s64 m_progress{};
|
||||
s64 m_pull_offset{};
|
||||
u32 m_current_file_index{};
|
||||
};
|
||||
#endif
|
||||
|
||||
Result DumpToFile(ui::ProgressBox* pbox, fs::Fs* fs, const fs::FsPath& root, BaseSource* source, std::span<const fs::FsPath> paths) {
|
||||
Result DumpToUsb(ui::ProgressBox* pbox, BaseSource* source, std::span<const fs::FsPath> paths, const CustomTransfer& custom_transfer) {
|
||||
auto write_source = std::make_unique<WriteUsbSource>(3e+9);
|
||||
|
||||
for (const auto& path : paths) {
|
||||
const auto file_size = source->GetSize(path);
|
||||
pbox->SetImage(source->GetIcon(path));
|
||||
pbox->SetTitle(source->GetName(path));
|
||||
pbox->NewTransfer("Waiting for USB connection...");
|
||||
|
||||
// wait until usb is ready.
|
||||
while (true) {
|
||||
R_TRY(pbox->ShouldExitResult());
|
||||
|
||||
const auto rc = write_source->WaitForConnection(path, 3e+9);
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
pbox->NewTransfer(path);
|
||||
ON_SCOPE_EXIT(write_source->CloseFile());
|
||||
|
||||
if (custom_transfer) {
|
||||
R_TRY(custom_transfer(pbox, source, write_source.get(), path));
|
||||
} else {
|
||||
R_TRY(thread::Transfer(pbox, file_size,
|
||||
[&](void* data, s64 off, s64 size, u64* bytes_read) -> Result {
|
||||
return source->Read(path, data, off, size, bytes_read);
|
||||
},
|
||||
[&](const void* data, s64 off, s64 size) -> Result {
|
||||
return write_source->Write(data, off, size);
|
||||
}
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result DumpToFile(ui::ProgressBox* pbox, fs::Fs* fs, const fs::FsPath& root, BaseSource* source, std::span<const fs::FsPath> paths, const CustomTransfer& custom_transfer) {
|
||||
const auto is_file_based_emummc = App::IsFileBaseEmummc();
|
||||
|
||||
for (const auto& path : paths) {
|
||||
@@ -122,19 +319,24 @@ Result DumpToFile(ui::ProgressBox* pbox, fs::Fs* fs, const fs::FsPath& root, Bas
|
||||
{
|
||||
fs::File file;
|
||||
R_TRY(fs->OpenFile(temp_path, FsOpenMode_Write, &file));
|
||||
auto write_source = std::make_unique<WriteFileSource>(&file);
|
||||
|
||||
R_TRY(thread::Transfer(pbox, file_size,
|
||||
[&](void* data, s64 off, s64 size, u64* bytes_read) -> Result {
|
||||
return source->Read(path, data, off, size, bytes_read);
|
||||
},
|
||||
[&](const void* data, s64 off, s64 size) -> Result {
|
||||
const auto rc = file.Write(off, data, size, FsWriteOption_None);
|
||||
if (is_file_based_emummc) {
|
||||
svcSleepThread(2e+6); // 2ms
|
||||
if (custom_transfer) {
|
||||
R_TRY(custom_transfer(pbox, source, write_source.get(), path));
|
||||
} else {
|
||||
R_TRY(thread::Transfer(pbox, file_size,
|
||||
[&](void* data, s64 off, s64 size, u64* bytes_read) -> Result {
|
||||
return source->Read(path, data, off, size, bytes_read);
|
||||
},
|
||||
[&](const void* data, s64 off, s64 size) -> Result {
|
||||
const auto rc = write_source->Write(data, off, size);
|
||||
if (is_file_based_emummc) {
|
||||
svcSleepThread(2e+6); // 2ms
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
));
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fs->DeleteFile(base_path);
|
||||
@@ -144,53 +346,50 @@ Result DumpToFile(ui::ProgressBox* pbox, fs::Fs* fs, const fs::FsPath& root, Bas
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result DumpToFileNative(ui::ProgressBox* pbox, BaseSource* source, std::span<const fs::FsPath> paths) {
|
||||
Result DumpToFileNative(ui::ProgressBox* pbox, BaseSource* source, std::span<const fs::FsPath> paths, const CustomTransfer& custom_transfer) {
|
||||
fs::FsNativeSd fs{};
|
||||
return DumpToFile(pbox, &fs, "/", source, paths);
|
||||
return DumpToFile(pbox, &fs, "/", source, paths, custom_transfer);
|
||||
}
|
||||
|
||||
Result DumpToStdio(ui::ProgressBox* pbox, const location::StdioEntry& loc, BaseSource* source, std::span<const fs::FsPath> paths) {
|
||||
Result DumpToStdio(ui::ProgressBox* pbox, const location::StdioEntry& loc, BaseSource* source, std::span<const fs::FsPath> paths, const CustomTransfer& custom_transfer) {
|
||||
fs::FsStdio fs{};
|
||||
return DumpToFile(pbox, &fs, loc.mount, source, paths);
|
||||
return DumpToFile(pbox, &fs, loc.mount, source, paths, custom_transfer);
|
||||
}
|
||||
|
||||
#if ENABLE_NETWORK_INSTALL
|
||||
Result DumpToUsbS2SStream(ui::ProgressBox* pbox, UsbTest* usb, std::span<const fs::FsPath> paths) {
|
||||
Result DumpToUsbS2SInternal(ui::ProgressBox* pbox, UsbTest* usb) {
|
||||
auto source = usb->GetSource();
|
||||
|
||||
for (auto& path : paths) {
|
||||
while (!pbox->ShouldExit()) {
|
||||
R_TRY(usb->PollCommands());
|
||||
|
||||
const auto path = usb->GetPath();
|
||||
const auto file_size = source->GetSize(path);
|
||||
|
||||
R_TRY(thread::TransferPull(pbox, file_size,
|
||||
[&](void* data, s64 off, s64 size, u64* bytes_read) -> Result {
|
||||
return usb->ReadInternal(path, data, off, size, bytes_read);
|
||||
},
|
||||
[&](thread::StartThreadCallback start, thread::PullCallback pull) -> Result {
|
||||
[&](const thread::StartThreadCallback& start, const thread::PullCallback& pull) -> Result {
|
||||
usb->SetPullCallback(pull);
|
||||
R_TRY(start());
|
||||
|
||||
while (!pbox->ShouldExit()) {
|
||||
R_TRY(usb->PollCommands());
|
||||
|
||||
if (usb->GetPullOffset() >= file_size) {
|
||||
R_SUCCEED();
|
||||
const auto rc = usb->file_transfer_loop();
|
||||
if (R_FAILED(rc)) {
|
||||
if (rc == Result_UsbUploadExit) {
|
||||
break;
|
||||
} else {
|
||||
R_THROW(rc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
R_THROW(0xFFFF);
|
||||
return pbox->ShouldExitResult();
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result DumpToUsbS2SRandom(ui::ProgressBox* pbox, UsbTest* usb) {
|
||||
while (!pbox->ShouldExit()) {
|
||||
R_TRY(usb->PollCommands());
|
||||
}
|
||||
|
||||
R_THROW(0xFFFF);
|
||||
return pbox->ShouldExitResult();
|
||||
}
|
||||
|
||||
Result DumpToUsbS2S(ui::ProgressBox* pbox, BaseSource* source, std::span<const fs::FsPath> paths) {
|
||||
@@ -199,27 +398,16 @@ Result DumpToUsbS2S(ui::ProgressBox* pbox, BaseSource* source, std::span<const f
|
||||
file_list.emplace_back(path);
|
||||
}
|
||||
|
||||
// auto usb = std::make_unique<UsbTest>(pbox, entries);
|
||||
auto usb = std::make_unique<UsbTest>(pbox, source);
|
||||
constexpr u64 timeout = 1e+9;
|
||||
auto usb = std::make_unique<UsbTest>(pbox, source, paths);
|
||||
constexpr u64 timeout = 3e+9;
|
||||
|
||||
while (!pbox->ShouldExit()) {
|
||||
if (R_SUCCEEDED(usb->IsUsbConnected(timeout))) {
|
||||
pbox->NewTransfer("USB connected, sending file list"_i18n);
|
||||
u8 flags = usb::tinfoil::USBFlag_NONE;
|
||||
if (App::GetApp()->m_dump_usb_transfer_stream.Get()) {
|
||||
flags |= usb::tinfoil::USBFlag_STREAM;
|
||||
}
|
||||
|
||||
if (R_SUCCEEDED(usb->WaitForConnection(timeout, flags, file_list))) {
|
||||
if (R_SUCCEEDED(usb->WaitForConnection(timeout, file_list))) {
|
||||
pbox->NewTransfer("Sent file list, waiting for command..."_i18n);
|
||||
|
||||
Result rc;
|
||||
if (flags & usb::tinfoil::USBFlag_STREAM) {
|
||||
rc = DumpToUsbS2SStream(pbox, usb.get(), paths);
|
||||
} else {
|
||||
rc = DumpToUsbS2SRandom(pbox, usb.get());
|
||||
}
|
||||
Result rc = DumpToUsbS2SInternal(pbox, usb.get());
|
||||
|
||||
// wait for exit command.
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
@@ -242,11 +430,10 @@ Result DumpToUsbS2S(ui::ProgressBox* pbox, BaseSource* source, std::span<const f
|
||||
}
|
||||
}
|
||||
|
||||
R_THROW(0xFFFF);
|
||||
return pbox->ShouldExitResult();
|
||||
}
|
||||
#endif
|
||||
|
||||
Result DumpToDevNull(ui::ProgressBox* pbox, BaseSource* source, std::span<const fs::FsPath> paths) {
|
||||
Result DumpToDevNull(ui::ProgressBox* pbox, BaseSource* source, std::span<const fs::FsPath> paths, const CustomTransfer& custom_transfer) {
|
||||
for (auto path : paths) {
|
||||
R_TRY(pbox->ShouldExitResult());
|
||||
|
||||
@@ -255,14 +442,20 @@ Result DumpToDevNull(ui::ProgressBox* pbox, BaseSource* source, std::span<const
|
||||
pbox->SetTitle(source->GetName(path));
|
||||
pbox->NewTransfer(path);
|
||||
|
||||
R_TRY(thread::Transfer(pbox, file_size,
|
||||
[&](void* data, s64 off, s64 size, u64* bytes_read) -> Result {
|
||||
return source->Read(path, data, off, size, bytes_read);
|
||||
},
|
||||
[&](const void* data, s64 off, s64 size) -> Result {
|
||||
R_SUCCEED();
|
||||
}
|
||||
));
|
||||
auto write_source = std::make_unique<WriteNullSource>();
|
||||
|
||||
if (custom_transfer) {
|
||||
R_TRY(custom_transfer(pbox, source, write_source.get(), path));
|
||||
} else {
|
||||
R_TRY(thread::Transfer(pbox, file_size,
|
||||
[&](void* data, s64 off, s64 size, u64* bytes_read) -> Result {
|
||||
return source->Read(path, data, off, size, bytes_read);
|
||||
},
|
||||
[&](const void* data, s64 off, s64 size) -> Result {
|
||||
return write_source->Write(data, off, size);
|
||||
}
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
@@ -318,13 +511,13 @@ Result DumpToNetwork(ui::ProgressBox* pbox, const location::Entry& loc, BaseSour
|
||||
|
||||
} // namespace
|
||||
|
||||
void DumpGetLocation(const std::string& title, u32 location_flags, const OnLocation& on_loc) {
|
||||
void DumpGetLocation(const std::string& title, u32 location_flags, const OnLocation& on_loc, const CustomTransfer& custom_transfer) {
|
||||
DumpLocation out;
|
||||
ui::PopupList::Items items;
|
||||
std::vector<DumpEntry> dump_entries;
|
||||
|
||||
out.network = location::Load();
|
||||
if (location_flags & (1 << DumpLocationType_Network)) {
|
||||
if (!custom_transfer && location_flags & (1 << DumpLocationType_Network)) {
|
||||
for (s32 i = 0; i < std::size(out.network); i++) {
|
||||
dump_entries.emplace_back(DumpLocationType_Network, i);
|
||||
items.emplace_back(out.network[i].name);
|
||||
@@ -341,36 +534,45 @@ void DumpGetLocation(const std::string& title, u32 location_flags, const OnLocat
|
||||
|
||||
for (s32 i = 0; i < std::size(DUMP_LOCATIONS); i++) {
|
||||
if (location_flags & (1 << DUMP_LOCATIONS[i].type)) {
|
||||
dump_entries.emplace_back(DUMP_LOCATIONS[i].type, i);
|
||||
items.emplace_back(i18n::get(DUMP_LOCATIONS[i].name));
|
||||
log_write("[dump] got name: %s\n", DUMP_LOCATIONS[i].name);
|
||||
if (!custom_transfer || DUMP_LOCATIONS[i].type != DumpLocationType_UsbS2S) {
|
||||
log_write("[dump] got name 2: %s\n", DUMP_LOCATIONS[i].name);
|
||||
dump_entries.emplace_back(DUMP_LOCATIONS[i].type, i);
|
||||
items.emplace_back(i18n::get(DUMP_LOCATIONS[i].name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
App::Push<ui::PopupList>(
|
||||
title, items, [dump_entries, out, on_loc](auto op_index) mutable {
|
||||
out.entry = dump_entries[*op_index];
|
||||
log_write("got entry: %u index: %zu\n", out.entry.type, *op_index);
|
||||
on_loc(out);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
void Dump(const std::shared_ptr<BaseSource>& source, const DumpLocation& location, const std::vector<fs::FsPath>& paths, const OnExit& on_exit) {
|
||||
App::Push<ui::ProgressBox>(0, "Exporting"_i18n, "", [source, paths, location](auto pbox) -> Result {
|
||||
if (location.entry.type == DumpLocationType_Network) {
|
||||
R_TRY(DumpToNetwork(pbox, location.network[location.entry.index], source.get(), paths));
|
||||
} else if (location.entry.type == DumpLocationType_Stdio) {
|
||||
R_TRY(DumpToStdio(pbox, location.stdio[location.entry.index], source.get(), paths));
|
||||
} else if (location.entry.type == DumpLocationType_SdCard) {
|
||||
R_TRY(DumpToFileNative(pbox, source.get(), paths));
|
||||
} else if (location.entry.type == DumpLocationType_UsbS2S) {
|
||||
#if ENABLE_NETWORK_INSTALL
|
||||
R_TRY(DumpToUsbS2S(pbox, source.get(), paths));
|
||||
#endif
|
||||
} else if (location.entry.type == DumpLocationType_DevNull) {
|
||||
R_TRY(DumpToDevNull(pbox, source.get(), paths));
|
||||
}
|
||||
Result Dump(ui::ProgressBox* pbox, const std::shared_ptr<BaseSource>& source, const DumpLocation& location, const std::vector<fs::FsPath>& paths, const CustomTransfer& custom_transfer) {
|
||||
if (location.entry.type == DumpLocationType_Network) {
|
||||
R_TRY(DumpToNetwork(pbox, location.network[location.entry.index], source.get(), paths));
|
||||
} else if (location.entry.type == DumpLocationType_Stdio) {
|
||||
R_TRY(DumpToStdio(pbox, location.stdio[location.entry.index], source.get(), paths, custom_transfer));
|
||||
} else if (location.entry.type == DumpLocationType_SdCard) {
|
||||
R_TRY(DumpToFileNative(pbox, source.get(), paths, custom_transfer));
|
||||
} else if (location.entry.type == DumpLocationType_Usb) {
|
||||
R_TRY(DumpToUsb(pbox, source.get(), paths, custom_transfer));
|
||||
} else if (location.entry.type == DumpLocationType_UsbS2S) {
|
||||
R_TRY(DumpToUsbS2S(pbox, source.get(), paths));
|
||||
} else if (location.entry.type == DumpLocationType_DevNull) {
|
||||
R_TRY(DumpToDevNull(pbox, source.get(), paths, custom_transfer));
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
void Dump(const std::shared_ptr<BaseSource>& source, const DumpLocation& location, const std::vector<fs::FsPath>& paths, const OnExit& on_exit, const CustomTransfer& custom_transfer) {
|
||||
App::Push<ui::ProgressBox>(0, "Exporting"_i18n, "", [source, paths, location, custom_transfer](auto pbox) -> Result {
|
||||
return Dump(pbox, source, location, paths, custom_transfer);
|
||||
}, [on_exit](Result rc){
|
||||
App::PushErrorBox(rc, "Export failed!"_i18n);
|
||||
|
||||
@@ -379,7 +581,9 @@ void Dump(const std::shared_ptr<BaseSource>& source, const DumpLocation& locatio
|
||||
log_write("dump successfull!!!\n");
|
||||
}
|
||||
|
||||
on_exit(rc);
|
||||
if (on_exit) {
|
||||
on_exit(rc);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -389,4 +593,15 @@ void Dump(const std::shared_ptr<BaseSource>& source, const std::vector<fs::FsPat
|
||||
});
|
||||
}
|
||||
|
||||
void Dump(const std::shared_ptr<BaseSource>& source, const std::vector<fs::FsPath>& paths, const CustomTransfer& custom_transfer, const OnExit& on_exit, u32 location_flags) {
|
||||
DumpGetLocation("Select export location"_i18n, location_flags, [source, paths, on_exit, custom_transfer](const DumpLocation& loc) {
|
||||
Dump(source, loc, paths, on_exit, custom_transfer);
|
||||
}, custom_transfer);
|
||||
}
|
||||
|
||||
void FileFuncWriter(WriteSource* writer, zlib_filefunc64_def* funcs) {
|
||||
*funcs = zlib_filefunc;
|
||||
funcs->opaque = writer;
|
||||
}
|
||||
|
||||
} // namespace sphaira::dump
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#include "utils/devoptab.hpp"
|
||||
#include "utils/devoptab_common.hpp"
|
||||
#include "fatfs.hpp"
|
||||
#include "defines.hpp"
|
||||
#include "log.hpp"
|
||||
@@ -6,6 +8,7 @@
|
||||
#include <array>
|
||||
#include <algorithm>
|
||||
#include <span>
|
||||
#include <memory>
|
||||
|
||||
#include <cstring>
|
||||
#include <cstdio>
|
||||
@@ -15,98 +18,77 @@
|
||||
namespace sphaira::fatfs {
|
||||
namespace {
|
||||
|
||||
auto is_archive(BYTE attr) -> bool {
|
||||
const auto archive_attr = AM_DIR | AM_ARC;
|
||||
return (attr & archive_attr) == archive_attr;
|
||||
}
|
||||
|
||||
// todo: replace with off+size and have the data be in another struct
|
||||
// in order to be more lcache efficient.
|
||||
struct BufferedFileData {
|
||||
u8* data{};
|
||||
u64 off{};
|
||||
u64 size{};
|
||||
struct FsStorageSource final : yati::source::Base {
|
||||
FsStorageSource(FsStorage* s) : m_s{*s} {
|
||||
|
||||
~BufferedFileData() {
|
||||
if (data) {
|
||||
free(data);
|
||||
}
|
||||
}
|
||||
|
||||
void Allocate(u64 new_size) {
|
||||
data = (u8*)realloc(data, new_size * sizeof(*data));
|
||||
off = 0;
|
||||
size = 0;
|
||||
}
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
struct LinkedList {
|
||||
T* data;
|
||||
LinkedList* next;
|
||||
LinkedList* prev;
|
||||
};
|
||||
|
||||
constexpr u64 CACHE_LARGE_ALLOC_SIZE = 1024 * 512;
|
||||
constexpr u64 CACHE_LARGE_SIZE = 1024 * 16;
|
||||
|
||||
template<typename T>
|
||||
struct Lru {
|
||||
using ListEntry = LinkedList<T>;
|
||||
|
||||
// pass span of the data.
|
||||
void Init(std::span<T> data) {
|
||||
list_flat_array.clear();
|
||||
list_flat_array.resize(data.size());
|
||||
|
||||
auto list_entry = list_head = list_flat_array.data();
|
||||
|
||||
for (size_t i = 0; i < data.size(); i++) {
|
||||
list_entry = list_flat_array.data() + i;
|
||||
list_entry->data = data.data() + i;
|
||||
|
||||
if (i + 1 < data.size()) {
|
||||
list_entry->next = &list_flat_array[i + 1];
|
||||
}
|
||||
if (i) {
|
||||
list_entry->prev = &list_flat_array[i - 1];
|
||||
}
|
||||
}
|
||||
|
||||
list_tail = list_entry->prev->next;
|
||||
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override {
|
||||
R_TRY(fsStorageRead(&m_s, off, buf, size));
|
||||
*bytes_read = size;
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
// moves entry to the front of the list.
|
||||
void Update(ListEntry* entry) {
|
||||
// only update position if we are not the head.
|
||||
if (list_head != entry) {
|
||||
entry->prev->next = entry->next;
|
||||
if (entry->next) {
|
||||
entry->next->prev = entry->prev;
|
||||
} else {
|
||||
list_tail = entry->prev;
|
||||
}
|
||||
|
||||
// update head.
|
||||
auto head_temp = list_head;
|
||||
list_head = entry;
|
||||
list_head->prev = nullptr;
|
||||
list_head->next = head_temp;
|
||||
head_temp->prev = list_head;
|
||||
}
|
||||
Result GetSize(s64* size) {
|
||||
return fsStorageGetSize(&m_s, size);
|
||||
}
|
||||
|
||||
// moves last entry (tail) to the front of the list.
|
||||
auto GetNextFree() {
|
||||
Update(list_tail);
|
||||
return list_head->data;
|
||||
}
|
||||
|
||||
auto begin() const { return list_head; }
|
||||
auto end() const { return list_tail; }
|
||||
|
||||
private:
|
||||
ListEntry* list_head{};
|
||||
ListEntry* list_tail{};
|
||||
std::vector<ListEntry> list_flat_array{};
|
||||
FsStorage m_s;
|
||||
};
|
||||
|
||||
using LruBufferedData = Lru<BufferedFileData>;
|
||||
struct File {
|
||||
FIL* files;
|
||||
u32 file_count;
|
||||
size_t off;
|
||||
char path[256];
|
||||
};
|
||||
|
||||
struct Dir {
|
||||
FDIR dir;
|
||||
char path[256];
|
||||
};
|
||||
|
||||
u64 get_size_from_files(const File* file) {
|
||||
u64 size = 0;
|
||||
for (u32 i = 0; i < file->file_count; i++) {
|
||||
size += f_size(&file->files[i]);
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
FIL* get_current_file(File* file) {
|
||||
auto off = file->off;
|
||||
for (u32 i = 0; i < file->file_count; i++) {
|
||||
auto fil = &file->files[i];
|
||||
if (off <= f_size(fil)) {
|
||||
return fil;
|
||||
}
|
||||
off -= f_size(fil);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// adjusts current file pos and sets the rest of files to 0.
|
||||
void set_current_file_pos(File* file) {
|
||||
s64 off = file->off;
|
||||
for (u32 i = 0; i < file->file_count; i++) {
|
||||
auto fil = &file->files[i];
|
||||
if (off >= 0 && off < f_size(fil)) {
|
||||
f_lseek(fil, off);
|
||||
} else {
|
||||
f_rewind(fil);
|
||||
}
|
||||
off -= f_size(fil);
|
||||
}
|
||||
}
|
||||
|
||||
enum BisMountType {
|
||||
BisMountType_PRODINFOF,
|
||||
@@ -117,10 +99,7 @@ enum BisMountType {
|
||||
|
||||
struct FatStorageEntry {
|
||||
FsStorage storage;
|
||||
s64 storage_size;
|
||||
LruBufferedData lru_cache[2];
|
||||
BufferedFileData buffered_small[1024]; // 1MiB (usually).
|
||||
BufferedFileData buffered_large[2]; // 1MiB
|
||||
std::unique_ptr<devoptab::common::LruBufferedData> buffered;
|
||||
FATFS fs;
|
||||
devoptab_t devoptab;
|
||||
};
|
||||
@@ -141,93 +120,39 @@ static_assert(std::size(BIS_MOUNT_ENTRIES) == FF_VOLUMES);
|
||||
|
||||
FatStorageEntry g_fat_storage[FF_VOLUMES];
|
||||
|
||||
Result ReadStorage(FsStorage* storage, std::span<LruBufferedData> lru_cache, void *_buffer, u64 file_off, u64 read_size, u64 capacity) {
|
||||
// log_write("[FATFS] read offset: %zu size: %zu\n", file_off, read_size);
|
||||
auto dst = static_cast<u8*>(_buffer);
|
||||
size_t amount = 0;
|
||||
|
||||
R_UNLESS(file_off < capacity, FsError_UnsupportedOperateRangeForFileStorage);
|
||||
read_size = std::min(read_size, capacity - file_off);
|
||||
|
||||
// fatfs reads in max 16k chunks.
|
||||
// knowing this, it's possible to detect large file reads by simply checking if
|
||||
// the read size is 16k (or more, maybe in the furter).
|
||||
// however this would destroy random access performance, such as fetching 512 bytes.
|
||||
// the fix was to have 2 LRU caches, one for large data and the other for small (anything below 16k).
|
||||
// the results in file reads 32MB -> 184MB and directory listing is instant.
|
||||
const auto large_read = read_size >= 1024 * 16;
|
||||
auto& lru = large_read ? lru_cache[1] : lru_cache[0];
|
||||
|
||||
for (auto list = lru.begin(); list; list = list->next) {
|
||||
const auto& m_buffered = list->data;
|
||||
if (m_buffered->size) {
|
||||
// check if we can read this data into the beginning of dst.
|
||||
if (file_off < m_buffered->off + m_buffered->size && file_off >= m_buffered->off) {
|
||||
const auto off = file_off - m_buffered->off;
|
||||
const auto size = std::min<s64>(read_size, m_buffered->size - off);
|
||||
if (size) {
|
||||
// log_write("[FAT] cache HIT at: %zu\n", file_off);
|
||||
std::memcpy(dst, m_buffered->data + off, size);
|
||||
|
||||
read_size -= size;
|
||||
file_off += size;
|
||||
amount += size;
|
||||
dst += size;
|
||||
|
||||
lru.Update(list);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (read_size) {
|
||||
// log_write("[FAT] cache miss at: %zu %zu\n", file_off, read_size);
|
||||
|
||||
auto alloc_size = large_read ? CACHE_LARGE_ALLOC_SIZE : std::max<u64>(read_size, 512);
|
||||
alloc_size = std::min(alloc_size, capacity - file_off);
|
||||
|
||||
auto m_buffered = lru.GetNextFree();
|
||||
m_buffered->Allocate(alloc_size);
|
||||
|
||||
// if the dst is big enough, read data in place.
|
||||
if (read_size > alloc_size) {
|
||||
R_TRY(fsStorageRead(storage, file_off, dst, read_size));
|
||||
const auto bytes_read = read_size;
|
||||
read_size -= bytes_read;
|
||||
file_off += bytes_read;
|
||||
amount += bytes_read;
|
||||
dst += bytes_read;
|
||||
|
||||
// save the last chunk of data to the m_buffered io.
|
||||
const auto max_advance = std::min<u64>(amount, alloc_size);
|
||||
m_buffered->off = file_off - max_advance;
|
||||
m_buffered->size = max_advance;
|
||||
std::memcpy(m_buffered->data, dst - max_advance, max_advance);
|
||||
} else {
|
||||
R_TRY(fsStorageRead(storage, file_off, m_buffered->data, alloc_size));
|
||||
const auto bytes_read = alloc_size;
|
||||
const auto max_advance = std::min<u64>(read_size, bytes_read);
|
||||
std::memcpy(dst, m_buffered->data, max_advance);
|
||||
|
||||
m_buffered->off = file_off;
|
||||
m_buffered->size = bytes_read;
|
||||
|
||||
read_size -= max_advance;
|
||||
file_off += max_advance;
|
||||
amount += max_advance;
|
||||
dst += max_advance;
|
||||
}
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
void fill_stat(const FILINFO* fno, struct stat *st) {
|
||||
void fill_stat(const char* path, const FILINFO* fno, struct stat *st) {
|
||||
memset(st, 0, sizeof(*st));
|
||||
|
||||
st->st_nlink = 1;
|
||||
|
||||
struct tm tm{};
|
||||
tm.tm_sec = (fno->ftime & 0x1F) << 1;
|
||||
tm.tm_min = (fno->ftime >> 5) & 0x3F;
|
||||
tm.tm_hour = (fno->ftime >> 11);
|
||||
tm.tm_mday = (fno->fdate & 0x1F);
|
||||
tm.tm_mon = ((fno->fdate >> 5) & 0xF) - 1;
|
||||
tm.tm_year = (fno->fdate >> 9) + 80;
|
||||
|
||||
st->st_atime = mktime(&tm);
|
||||
st->st_mtime = st->st_atime;
|
||||
st->st_ctime = st->st_atime;
|
||||
|
||||
// fake file.
|
||||
if (path && is_archive(fno->fattrib)) {
|
||||
st->st_size = 0;
|
||||
char file_path[256];
|
||||
for (u16 i = 0; i < 256; i++) {
|
||||
std::snprintf(file_path, sizeof(file_path), "%s/%02u", path, i);
|
||||
FILINFO file_info;
|
||||
if (FR_OK != f_stat(file_path, &file_info)) {
|
||||
break;
|
||||
}
|
||||
|
||||
st->st_size += file_info.fsize;
|
||||
}
|
||||
|
||||
st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
|
||||
} else
|
||||
if (fno->fattrib & AM_DIR) {
|
||||
st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
|
||||
} else {
|
||||
@@ -242,64 +167,126 @@ static int set_errno(struct _reent *r, int err) {
|
||||
}
|
||||
|
||||
int fat_open(struct _reent *r, void *fileStruct, const char *path, int flags, int mode) {
|
||||
memset(fileStruct, 0, sizeof(FIL));
|
||||
auto file = static_cast<File*>(fileStruct);
|
||||
std::memset(file, 0, sizeof(*file));
|
||||
|
||||
if (FR_OK != f_open((FIL*)fileStruct, path, FA_READ)) {
|
||||
// todo: init array
|
||||
// todo: handle dir.
|
||||
FIL fil{};
|
||||
if (FR_OK == f_open(&fil, path, FA_READ)) {
|
||||
file->file_count = 1;
|
||||
file->files = (FIL*)std::malloc(sizeof(*file->files));
|
||||
std::memcpy(file->files, &fil, sizeof(*file->files));
|
||||
// todo: check what error code is returned here.
|
||||
} else {
|
||||
FILINFO info{};
|
||||
if (FR_OK != f_stat(path, &info)) {
|
||||
return set_errno(r, ENOENT);
|
||||
}
|
||||
|
||||
if (!(info.fattrib & AM_ARC)) {
|
||||
return set_errno(r, ENOENT);
|
||||
}
|
||||
|
||||
char file_path[256];
|
||||
for (u16 i = 0; i < 256; i++) {
|
||||
std::memset(&fil, 0, sizeof(fil));
|
||||
std::snprintf(file_path, sizeof(file_path), "%s/%02u", path, i);
|
||||
|
||||
if (FR_OK != f_open(&fil, file_path, FA_READ)) {
|
||||
break;
|
||||
}
|
||||
|
||||
file->files = (FIL*)std::realloc(file->files, (i + 1) * sizeof(*file->files));
|
||||
std::memcpy(&file->files[i], &fil, sizeof(fil));
|
||||
file->file_count++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!file->files) {
|
||||
return set_errno(r, ENOENT);
|
||||
}
|
||||
|
||||
std::snprintf(file->path, sizeof(file->path), "%s", path);
|
||||
return r->_errno = 0;
|
||||
}
|
||||
|
||||
int fat_close(struct _reent *r, void *fd) {
|
||||
if (FR_OK != f_close((FIL*)fd)) {
|
||||
return set_errno(r, ENOENT);
|
||||
auto file = static_cast<File*>(fd);
|
||||
|
||||
if (file->files) {
|
||||
for (u32 i = 0; i < file->file_count; i++) {
|
||||
f_close(&file->files[i]);
|
||||
}
|
||||
free(file->files);
|
||||
}
|
||||
|
||||
return r->_errno = 0;
|
||||
}
|
||||
|
||||
ssize_t fat_read(struct _reent *r, void *fd, char *ptr, size_t len) {
|
||||
UINT bytes_read;
|
||||
if (FR_OK != f_read((FIL*)fd, ptr, len, &bytes_read)) {
|
||||
return set_errno(r, ENOENT);
|
||||
auto file = static_cast<File*>(fd);
|
||||
UINT total_bytes_read = 0;
|
||||
|
||||
while (len) {
|
||||
UINT bytes_read;
|
||||
auto fil = get_current_file(file);
|
||||
if (!fil) {
|
||||
log_write("[FATFS] failed to get fil\n");
|
||||
return set_errno(r, ENOENT);
|
||||
}
|
||||
|
||||
if (FR_OK != f_read(fil, ptr, len, &bytes_read)) {
|
||||
return set_errno(r, ENOENT);
|
||||
}
|
||||
|
||||
if (!bytes_read) {
|
||||
break;
|
||||
}
|
||||
|
||||
len -= bytes_read;
|
||||
file->off += bytes_read;
|
||||
total_bytes_read += bytes_read;
|
||||
}
|
||||
|
||||
return bytes_read;
|
||||
return total_bytes_read;
|
||||
}
|
||||
|
||||
off_t fat_seek(struct _reent *r, void *fd, off_t pos, int dir) {
|
||||
auto file = static_cast<File*>(fd);
|
||||
const auto size = get_size_from_files(file);
|
||||
|
||||
if (dir == SEEK_CUR) {
|
||||
pos += f_tell((FIL*)fd);
|
||||
pos += file->off;
|
||||
} else if (dir == SEEK_END) {
|
||||
pos = f_size((FIL*)fd);
|
||||
pos = size;
|
||||
}
|
||||
|
||||
if (FR_OK != f_lseek((FIL*)fd, pos)) {
|
||||
set_errno(r, ENOENT);
|
||||
return 0;
|
||||
}
|
||||
file->off = std::clamp<u64>(pos, 0, size);
|
||||
set_current_file_pos(file);
|
||||
|
||||
r->_errno = 0;
|
||||
return f_tell((FIL*)fd);
|
||||
return file->off;
|
||||
}
|
||||
|
||||
int fat_fstat(struct _reent *r, void *fd, struct stat *st) {
|
||||
const FIL* file = (FIL*)fd;
|
||||
auto file = static_cast<File*>(fd);
|
||||
|
||||
/* Only fill the attr and size field, leaving the timestamp blank. */
|
||||
FILINFO info = {0};
|
||||
info.fattrib = file->obj.attr;
|
||||
info.fsize = file->obj.objsize;
|
||||
FILINFO info{};
|
||||
info.fsize = get_size_from_files(file);
|
||||
|
||||
/* Fill stat info. */
|
||||
fill_stat(&info, st);
|
||||
fill_stat(nullptr, &info, st);
|
||||
|
||||
return r->_errno = 0;
|
||||
}
|
||||
|
||||
DIR_ITER* fat_diropen(struct _reent *r, DIR_ITER *dirState, const char *path) {
|
||||
memset(dirState->dirStruct, 0, sizeof(FDIR));
|
||||
auto dir = static_cast<Dir*>(dirState->dirStruct);
|
||||
std::memset(dir, 0, sizeof(*dir));
|
||||
|
||||
if (FR_OK != f_opendir((FDIR*)dirState->dirStruct, path)) {
|
||||
if (FR_OK != f_opendir(&dir->dir, path)) {
|
||||
set_errno(r, ENOENT);
|
||||
return NULL;
|
||||
}
|
||||
@@ -309,15 +296,20 @@ DIR_ITER* fat_diropen(struct _reent *r, DIR_ITER *dirState, const char *path) {
|
||||
}
|
||||
|
||||
int fat_dirreset(struct _reent *r, DIR_ITER *dirState) {
|
||||
if (FR_OK != f_rewinddir((FDIR*)dirState->dirStruct)) {
|
||||
auto dir = static_cast<Dir*>(dirState->dirStruct);
|
||||
|
||||
if (FR_OK != f_rewinddir(&dir->dir)) {
|
||||
return set_errno(r, ENOENT);
|
||||
}
|
||||
|
||||
return r->_errno = 0;
|
||||
}
|
||||
|
||||
int fat_dirnext(struct _reent *r, DIR_ITER *dirState, char *filename, struct stat *filestat) {
|
||||
auto dir = static_cast<Dir*>(dirState->dirStruct);
|
||||
FILINFO fno{};
|
||||
if (FR_OK != f_readdir((FDIR*)dirState->dirStruct, &fno)) {
|
||||
|
||||
if (FR_OK != f_readdir(&dir->dir, &fno)) {
|
||||
return set_errno(r, ENOENT);
|
||||
}
|
||||
|
||||
@@ -326,15 +318,18 @@ int fat_dirnext(struct _reent *r, DIR_ITER *dirState, char *filename, struct sta
|
||||
}
|
||||
|
||||
strcpy(filename, fno.fname);
|
||||
fill_stat(&fno, filestat);
|
||||
fill_stat(dir->path, &fno, filestat);
|
||||
|
||||
return r->_errno = 0;
|
||||
}
|
||||
|
||||
int fat_dirclose(struct _reent *r, DIR_ITER *dirState) {
|
||||
if (FR_OK != f_closedir((FDIR*)dirState->dirStruct)) {
|
||||
auto dir = static_cast<Dir*>(dirState->dirStruct);
|
||||
|
||||
if (FR_OK != f_closedir(&dir->dir)) {
|
||||
return set_errno(r, ENOENT);
|
||||
}
|
||||
|
||||
return r->_errno = 0;
|
||||
}
|
||||
|
||||
@@ -357,19 +352,19 @@ int fat_lstat(struct _reent *r, const char *file, struct stat *st) {
|
||||
return set_errno(r, ENOENT);
|
||||
}
|
||||
|
||||
fill_stat(&fno, st);
|
||||
fill_stat(file, &fno, st);
|
||||
return r->_errno = 0;
|
||||
}
|
||||
|
||||
constexpr devoptab_t DEVOPTAB = {
|
||||
.structSize = sizeof(FIL),
|
||||
.structSize = sizeof(File),
|
||||
.open_r = fat_open,
|
||||
.close_r = fat_close,
|
||||
.read_r = fat_read,
|
||||
.seek_r = fat_seek,
|
||||
.fstat_r = fat_fstat,
|
||||
.stat_r = fat_lstat,
|
||||
.dirStateSize = sizeof(FDIR),
|
||||
.dirStateSize = sizeof(Dir),
|
||||
.diropen_r = fat_diropen,
|
||||
.dirreset_r = fat_dirreset,
|
||||
.dirnext_r = fat_dirnext,
|
||||
@@ -378,37 +373,55 @@ constexpr devoptab_t DEVOPTAB = {
|
||||
.lstat_r = fat_lstat,
|
||||
};
|
||||
|
||||
Mutex g_mutex{};
|
||||
bool g_is_init{};
|
||||
|
||||
} // namespace
|
||||
|
||||
Result MountAll() {
|
||||
SCOPED_MUTEX(&g_mutex);
|
||||
|
||||
if (g_is_init) {
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
for (u32 i = 0; i < FF_VOLUMES; i++) {
|
||||
auto& fat = g_fat_storage[i];
|
||||
const auto& bis = BIS_MOUNT_ENTRIES[i];
|
||||
|
||||
log_write("[FAT] %s\n", bis.volume_name);
|
||||
|
||||
fat.lru_cache[0].Init(fat.buffered_small);
|
||||
fat.lru_cache[1].Init(fat.buffered_large);
|
||||
// log_write("[FAT] %s\n", bis.volume_name);
|
||||
|
||||
fat.devoptab = DEVOPTAB;
|
||||
fat.devoptab.name = bis.volume_name;
|
||||
fat.devoptab.deviceData = &fat;
|
||||
|
||||
R_TRY(fsOpenBisStorage(&fat.storage, bis.id));
|
||||
R_TRY(fsStorageGetSize(&fat.storage, &fat.storage_size));
|
||||
log_write("[FAT] BIS SUCCESS %s\n", bis.volume_name);
|
||||
auto source = std::make_shared<FsStorageSource>(&fat.storage);
|
||||
|
||||
s64 size;
|
||||
R_TRY(source->GetSize(&size));
|
||||
// log_write("[FAT] BIS SUCCESS %s\n", bis.volume_name);
|
||||
|
||||
fat.buffered = std::make_unique<devoptab::common::LruBufferedData>(source, size);
|
||||
|
||||
R_UNLESS(FR_OK == f_mount(&fat.fs, bis.mount_name, 1), 0x1);
|
||||
log_write("[FAT] MOUNT SUCCESS %s\n", bis.volume_name);
|
||||
// log_write("[FAT] MOUNT SUCCESS %s\n", bis.volume_name);
|
||||
|
||||
R_UNLESS(AddDevice(&fat.devoptab) >= 0, 0x1);
|
||||
log_write("[FAT] DEVICE SUCCESS %s\n", bis.volume_name);
|
||||
// log_write("[FAT] DEVICE SUCCESS %s\n", bis.volume_name);
|
||||
}
|
||||
|
||||
g_is_init = true;
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
void UnmountAll() {
|
||||
SCOPED_MUTEX(&g_mutex);
|
||||
|
||||
if (!g_is_init) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (u32 i = 0; i < FF_VOLUMES; i++) {
|
||||
auto& fat = g_fat_storage[i];
|
||||
const auto& bis = BIS_MOUNT_ENTRIES[i];
|
||||
@@ -433,7 +446,7 @@ const char* VolumeStr[] {
|
||||
Result fatfs_read(u8 num, void* dst, u64 offset, u64 size) {
|
||||
// log_write("[FAT] num: %u\n", num);
|
||||
auto& fat = sphaira::fatfs::g_fat_storage[num];
|
||||
return sphaira::fatfs::ReadStorage(&fat.storage, fat.lru_cache, dst, offset, size, fat.storage_size);
|
||||
return fat.buffered->Read2(dst, offset, size);
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@
|
||||
#include "app.hpp"
|
||||
#include "fs.hpp"
|
||||
#include "log.hpp"
|
||||
#include "utils/thread.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <minIni.h>
|
||||
@@ -14,7 +15,6 @@
|
||||
namespace sphaira::ftpsrv {
|
||||
namespace {
|
||||
|
||||
#if ENABLE_NETWORK_INSTALL
|
||||
struct InstallSharedData {
|
||||
Mutex mutex;
|
||||
std::deque<std::string> queued_files;
|
||||
@@ -27,11 +27,8 @@ struct InstallSharedData {
|
||||
bool in_progress;
|
||||
bool enabled;
|
||||
};
|
||||
#endif
|
||||
|
||||
const char* INI_PATH = "/config/ftpsrv/config.ini";
|
||||
constexpr int THREAD_PRIO = PRIO_PREEMPTIVE;
|
||||
constexpr int THREAD_CORE = 2;
|
||||
FtpSrvConfig g_ftpsrv_config = {0};
|
||||
std::atomic_bool g_should_exit = false;
|
||||
bool g_is_running{false};
|
||||
@@ -46,7 +43,6 @@ void ftp_progress_callback(void) {
|
||||
sphaira::App::NotifyFlashLed();
|
||||
}
|
||||
|
||||
#if ENABLE_NETWORK_INSTALL
|
||||
InstallSharedData g_shared_data{};
|
||||
|
||||
const char* SUPPORTED_EXT[] = {
|
||||
@@ -277,11 +273,73 @@ FtpVfs g_vfs_install = {
|
||||
.rmdir = vfs_install_rmdir,
|
||||
.rename = vfs_install_rename,
|
||||
};
|
||||
#endif
|
||||
|
||||
void loop(void* arg) {
|
||||
log_write("[FTP] loop entered\n");
|
||||
|
||||
// load config.
|
||||
{
|
||||
SCOPED_MUTEX(&g_mutex);
|
||||
|
||||
g_ftpsrv_config.log_callback = ftp_log_callback;
|
||||
g_ftpsrv_config.progress_callback = ftp_progress_callback;
|
||||
g_ftpsrv_config.anon = ini_getbool("Login", "anon", 0, INI_PATH);
|
||||
int user_len = ini_gets("Login", "user", "", g_ftpsrv_config.user, sizeof(g_ftpsrv_config.user), INI_PATH);
|
||||
int pass_len = ini_gets("Login", "pass", "", g_ftpsrv_config.pass, sizeof(g_ftpsrv_config.pass), INI_PATH);
|
||||
g_ftpsrv_config.port = ini_getl("Network", "port", 5000, INI_PATH); // 5000 to keep compat with older sphaira
|
||||
g_ftpsrv_config.timeout = ini_getl("Network", "timeout", 0, INI_PATH);
|
||||
g_ftpsrv_config.use_localtime = ini_getbool("Misc", "use_localtime", 0, INI_PATH);
|
||||
bool log_enabled = ini_getbool("Log", "log", 0, INI_PATH);
|
||||
|
||||
// get nx config
|
||||
bool mount_devices = ini_getbool("Nx", "mount_devices", 1, INI_PATH);
|
||||
bool mount_bis = ini_getbool("Nx", "mount_bis", 0, INI_PATH);
|
||||
bool save_writable = ini_getbool("Nx", "save_writable", 0, INI_PATH);
|
||||
g_ftpsrv_config.port = ini_getl("Nx", "app_port", g_ftpsrv_config.port, INI_PATH); // compat
|
||||
|
||||
// get Nx-App overrides
|
||||
g_ftpsrv_config.anon = ini_getbool("Nx-App", "anon", g_ftpsrv_config.anon, INI_PATH);
|
||||
user_len = ini_gets("Nx-App", "user", g_ftpsrv_config.user, g_ftpsrv_config.user, sizeof(g_ftpsrv_config.user), INI_PATH);
|
||||
pass_len = ini_gets("Nx-App", "pass", g_ftpsrv_config.pass, g_ftpsrv_config.pass, sizeof(g_ftpsrv_config.pass), INI_PATH);
|
||||
g_ftpsrv_config.port = ini_getl("Nx-App", "port", g_ftpsrv_config.port, INI_PATH);
|
||||
g_ftpsrv_config.timeout = ini_getl("Nx-App", "timeout", g_ftpsrv_config.timeout, INI_PATH);
|
||||
g_ftpsrv_config.use_localtime = ini_getbool("Nx-App", "use_localtime", g_ftpsrv_config.use_localtime, INI_PATH);
|
||||
log_enabled = ini_getbool("Nx-App", "log", log_enabled, INI_PATH);
|
||||
mount_devices = ini_getbool("Nx-App", "mount_devices", mount_devices, INI_PATH);
|
||||
mount_bis = ini_getbool("Nx-App", "mount_bis", mount_bis, INI_PATH);
|
||||
save_writable = ini_getbool("Nx-App", "save_writable", save_writable, INI_PATH);
|
||||
|
||||
g_should_exit = false;
|
||||
mount_devices = true;
|
||||
g_ftpsrv_config.timeout = 0;
|
||||
|
||||
if (!g_ftpsrv_config.port) {
|
||||
g_ftpsrv_config.port = 5000;
|
||||
log_write("[FTP] no port config, defaulting to 5000\n");
|
||||
}
|
||||
|
||||
// keep compat with older sphaira
|
||||
if (!user_len && !pass_len) {
|
||||
g_ftpsrv_config.anon = true;
|
||||
log_write("[FTP] no user pass, defaulting to anon\n");
|
||||
}
|
||||
|
||||
fsdev_wrapMountSdmc();
|
||||
|
||||
const VfsNxCustomPath custom = {
|
||||
.name = "install",
|
||||
.user = NULL,
|
||||
.func = &g_vfs_install,
|
||||
};
|
||||
|
||||
vfs_nx_init(&custom, mount_devices, save_writable, mount_bis, false);
|
||||
}
|
||||
|
||||
ON_SCOPE_EXIT(
|
||||
vfs_nx_exit();
|
||||
fsdev_wrapUnmountAll();
|
||||
);
|
||||
|
||||
while (!g_should_exit) {
|
||||
ftpsrv_init(&g_ftpsrv_config);
|
||||
while (!g_should_exit) {
|
||||
@@ -305,77 +363,20 @@ bool Init() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (R_FAILED(fsdev_wrapMountSdmc())) {
|
||||
log_write("[FTP] cannot mount sdmc\n");
|
||||
return false;
|
||||
}
|
||||
// if (R_FAILED(fsdev_wrapMountSdmc())) {
|
||||
// log_write("[FTP] cannot mount sdmc\n");
|
||||
// return false;
|
||||
// }
|
||||
|
||||
g_ftpsrv_config.log_callback = ftp_log_callback;
|
||||
g_ftpsrv_config.progress_callback = ftp_progress_callback;
|
||||
g_ftpsrv_config.anon = ini_getbool("Login", "anon", 0, INI_PATH);
|
||||
int user_len = ini_gets("Login", "user", "", g_ftpsrv_config.user, sizeof(g_ftpsrv_config.user), INI_PATH);
|
||||
int pass_len = ini_gets("Login", "pass", "", g_ftpsrv_config.pass, sizeof(g_ftpsrv_config.pass), INI_PATH);
|
||||
g_ftpsrv_config.port = ini_getl("Network", "port", 5000, INI_PATH); // 5000 to keep compat with older sphaira
|
||||
g_ftpsrv_config.timeout = ini_getl("Network", "timeout", 0, INI_PATH);
|
||||
g_ftpsrv_config.use_localtime = ini_getbool("Misc", "use_localtime", 0, INI_PATH);
|
||||
bool log_enabled = ini_getbool("Log", "log", 0, INI_PATH);
|
||||
|
||||
// get nx config
|
||||
bool mount_devices = ini_getbool("Nx", "mount_devices", 1, INI_PATH);
|
||||
bool mount_bis = ini_getbool("Nx", "mount_bis", 0, INI_PATH);
|
||||
bool save_writable = ini_getbool("Nx", "save_writable", 0, INI_PATH);
|
||||
g_ftpsrv_config.port = ini_getl("Nx", "app_port", g_ftpsrv_config.port, INI_PATH); // compat
|
||||
|
||||
// get Nx-App overrides
|
||||
g_ftpsrv_config.anon = ini_getbool("Nx-App", "anon", g_ftpsrv_config.anon, INI_PATH);
|
||||
user_len = ini_gets("Nx-App", "user", g_ftpsrv_config.user, g_ftpsrv_config.user, sizeof(g_ftpsrv_config.user), INI_PATH);
|
||||
pass_len = ini_gets("Nx-App", "pass", g_ftpsrv_config.pass, g_ftpsrv_config.pass, sizeof(g_ftpsrv_config.pass), INI_PATH);
|
||||
g_ftpsrv_config.port = ini_getl("Nx-App", "port", g_ftpsrv_config.port, INI_PATH);
|
||||
g_ftpsrv_config.timeout = ini_getl("Nx-App", "timeout", g_ftpsrv_config.timeout, INI_PATH);
|
||||
g_ftpsrv_config.use_localtime = ini_getbool("Nx-App", "use_localtime", g_ftpsrv_config.use_localtime, INI_PATH);
|
||||
log_enabled = ini_getbool("Nx-App", "log", log_enabled, INI_PATH);
|
||||
mount_devices = ini_getbool("Nx-App", "mount_devices", mount_devices, INI_PATH);
|
||||
mount_bis = ini_getbool("Nx-App", "mount_bis", mount_bis, INI_PATH);
|
||||
save_writable = ini_getbool("Nx-App", "save_writable", save_writable, INI_PATH);
|
||||
|
||||
g_should_exit = false;
|
||||
mount_devices = true;
|
||||
g_ftpsrv_config.timeout = 0;
|
||||
|
||||
if (!g_ftpsrv_config.port) {
|
||||
log_write("[FTP] no port config\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
// keep compat with older sphaira
|
||||
if (!user_len && !pass_len) {
|
||||
log_write("[FTP] no user pass\n");
|
||||
g_ftpsrv_config.anon = true;
|
||||
}
|
||||
|
||||
#if ENABLE_NETWORK_INSTALL
|
||||
const VfsNxCustomPath custom = {
|
||||
.name = "install",
|
||||
.user = NULL,
|
||||
.func = &g_vfs_install,
|
||||
};
|
||||
|
||||
vfs_nx_init(&custom, mount_devices, save_writable, mount_bis, false);
|
||||
#else
|
||||
vfs_nx_init(NULL, mount_devices, save_writable, mount_bis, false);
|
||||
#endif
|
||||
// todo: replace everything with ini_browse for faster loading.
|
||||
// or load everything in the init thread.
|
||||
|
||||
Result rc;
|
||||
if (R_FAILED(rc = threadCreate(&g_thread, loop, nullptr, nullptr, 1024*16, THREAD_PRIO, THREAD_CORE))) {
|
||||
if (R_FAILED(rc = utils::CreateThread(&g_thread, loop, nullptr, 1024*16))) {
|
||||
log_write("[FTP] failed to create nxlink thread: 0x%X\n", rc);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (R_FAILED(rc = svcSetThreadCoreMask(g_thread.handle, THREAD_CORE, THREAD_AFFINITY_DEFAULT(THREAD_CORE)))) {
|
||||
log_write("[FTP] failed to set core mask: 0x%X\n", rc);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (R_FAILED(rc = threadStart(&g_thread))) {
|
||||
log_write("[FTP] failed to start nxlink thread: 0x%X\n", rc);
|
||||
threadClose(&g_thread);
|
||||
@@ -398,8 +399,6 @@ void Exit() {
|
||||
threadWaitForExit(&g_thread);
|
||||
threadClose(&g_thread);
|
||||
|
||||
vfs_nx_exit();
|
||||
fsdev_wrapUnmountAll();
|
||||
memset(&g_ftpsrv_config, 0, sizeof(g_ftpsrv_config));
|
||||
|
||||
log_write("[FTP] exitied\n");
|
||||
@@ -410,7 +409,6 @@ void ExitSignal() {
|
||||
g_should_exit = true;
|
||||
}
|
||||
|
||||
#if ENABLE_NETWORK_INSTALL
|
||||
void InitInstallMode(const OnInstallStart& on_start, const OnInstallWrite& on_write, const OnInstallClose& on_close) {
|
||||
SCOPED_MUTEX(&g_shared_data.mutex);
|
||||
g_shared_data.on_start = on_start;
|
||||
@@ -423,7 +421,6 @@ void DisableInstallMode() {
|
||||
SCOPED_MUTEX(&g_shared_data.mutex);
|
||||
g_shared_data.enabled = false;
|
||||
}
|
||||
#endif
|
||||
|
||||
unsigned GetPort() {
|
||||
SCOPED_MUTEX(&g_mutex);
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
#include <mbedtls/md5.h>
|
||||
#include <utility>
|
||||
|
||||
#include <zlib.h>
|
||||
#include <zstd.h>
|
||||
|
||||
namespace sphaira::hash {
|
||||
namespace {
|
||||
|
||||
@@ -59,12 +62,152 @@ private:
|
||||
|
||||
struct HashSource {
|
||||
virtual ~HashSource() = default;
|
||||
virtual void Update(const void* buf, s64 size) = 0;
|
||||
virtual void Update(const void* buf, s64 size, s64 file_size) = 0;
|
||||
virtual void Get(std::string& out) = 0;
|
||||
};
|
||||
|
||||
struct HashNull final : HashSource {
|
||||
void Update(const void* buf, s64 size, s64 file_size) override {
|
||||
m_in_size += size;
|
||||
}
|
||||
|
||||
void Get(std::string& out) override {
|
||||
char str[64];
|
||||
std::snprintf(str, sizeof(str), "%zu bytes", m_in_size);
|
||||
out = str;
|
||||
}
|
||||
|
||||
private:
|
||||
size_t m_in_size{};
|
||||
};
|
||||
|
||||
// this currently crashes when freeing the pool :/
|
||||
#define USE_THREAD_POOL 0
|
||||
struct HashZstd final : HashSource {
|
||||
HashZstd() {
|
||||
const auto num_threads = 3;
|
||||
const auto level = ZSTD_CLEVEL_DEFAULT;
|
||||
|
||||
m_ctx = ZSTD_createCCtx();
|
||||
if (!m_ctx) {
|
||||
log_write("[ZSTD] failed to create ctx\n");
|
||||
}
|
||||
|
||||
|
||||
#if USE_THREAD_POOL
|
||||
m_pool = ZSTD_createThreadPool(num_threads);
|
||||
if (!m_pool) {
|
||||
log_write("[ZSTD] failed to create pool\n");
|
||||
}
|
||||
|
||||
if (ZSTD_isError(ZSTD_CCtx_refThreadPool(m_ctx, m_pool))) {
|
||||
log_write("[ZSTD] failed ZSTD_CCtx_refThreadPool(m_pool)\n");
|
||||
}
|
||||
#endif
|
||||
if (ZSTD_isError(ZSTD_CCtx_setParameter(m_ctx, ZSTD_c_compressionLevel, level))) {
|
||||
log_write("[ZSTD] failed ZSTD_CCtx_setParameter(ZSTD_c_compressionLevel)\n");
|
||||
}
|
||||
if (ZSTD_isError(ZSTD_CCtx_setParameter(m_ctx, ZSTD_c_nbWorkers, num_threads))) {
|
||||
log_write("[ZSTD] failed ZSTD_CCtx_setParameter(ZSTD_c_nbWorkers)\n");
|
||||
}
|
||||
|
||||
m_out_buf.resize(ZSTD_CStreamOutSize());
|
||||
}
|
||||
|
||||
~HashZstd() {
|
||||
ZSTD_freeCCtx(m_ctx);
|
||||
#if USE_THREAD_POOL
|
||||
// crashes here during ZSTD_pthread_join()
|
||||
// ZSTD_freeThreadPool(m_pool);
|
||||
#endif
|
||||
}
|
||||
|
||||
void Update(const void* buf, s64 size, s64 file_size) override {
|
||||
ZSTD_inBuffer input = { buf, (u64)size, 0 };
|
||||
|
||||
const auto last_chunk = m_in_size + size >= file_size;
|
||||
const auto mode = last_chunk ? ZSTD_e_end : ZSTD_e_continue;
|
||||
|
||||
while (input.pos < input.size) {
|
||||
ZSTD_outBuffer output = { m_out_buf.data(), m_out_buf.size(), 0 };
|
||||
const size_t remaining = ZSTD_compressStream2(m_ctx, &output , &input, mode);
|
||||
|
||||
if (ZSTD_isError(remaining)) {
|
||||
log_write("[ZSTD] error: %zu\n", remaining);
|
||||
break;
|
||||
}
|
||||
|
||||
m_out_size += output.pos;
|
||||
};
|
||||
|
||||
m_in_size += size;
|
||||
}
|
||||
|
||||
void Get(std::string& out) override {
|
||||
log_write("getting size: %zu vs %zu\n", m_out_size, m_in_size);
|
||||
char str[64];
|
||||
const u32 percentage = ((double)m_out_size / (double)m_in_size) * 100.0;
|
||||
std::snprintf(str, sizeof(str), "%u%%", percentage);
|
||||
out = str;
|
||||
log_write("got size: %zu vs %zu\n", m_out_size, m_in_size);
|
||||
}
|
||||
|
||||
private:
|
||||
ZSTD_CCtx* m_ctx{};
|
||||
ZSTD_threadPool* m_pool{};
|
||||
std::vector<u8> m_out_buf{};
|
||||
size_t m_in_size{};
|
||||
size_t m_out_size{};
|
||||
};
|
||||
|
||||
struct HashDeflate final : HashSource {
|
||||
HashDeflate() {
|
||||
deflateInit(&m_ctx, Z_DEFAULT_COMPRESSION);
|
||||
m_out_buf.resize(deflateBound(&m_ctx, 1024*1024*16)); // max chunk size.
|
||||
}
|
||||
|
||||
~HashDeflate() {
|
||||
deflateEnd(&m_ctx);
|
||||
}
|
||||
|
||||
void Update(const void* buf, s64 size, s64 file_size) override {
|
||||
m_ctx.avail_in = size;
|
||||
m_ctx.next_in = const_cast<Bytef*>((const Bytef*)buf);
|
||||
|
||||
const auto last_chunk = m_in_size + size >= file_size;
|
||||
const auto mode = last_chunk ? Z_FINISH : Z_NO_FLUSH;
|
||||
|
||||
while (m_ctx.avail_in != 0) {
|
||||
m_ctx.next_out = m_out_buf.data();
|
||||
m_ctx.avail_out = m_out_buf.size();
|
||||
|
||||
const auto rc = deflate(&m_ctx, mode);
|
||||
if (Z_OK != rc) {
|
||||
if (Z_STREAM_END != rc) {
|
||||
log_write("[ZLIB] deflate error: %d\n", rc);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
m_in_size += size;
|
||||
}
|
||||
|
||||
void Get(std::string& out) override {
|
||||
char str[64];
|
||||
const u32 percentage = ((double)m_ctx.total_out / (double)m_in_size) * 100.0;
|
||||
std::snprintf(str, sizeof(str), "%u%%", percentage);
|
||||
out = str;
|
||||
}
|
||||
|
||||
private:
|
||||
z_stream m_ctx{};
|
||||
std::vector<u8> m_out_buf{};
|
||||
size_t m_in_size{};
|
||||
};
|
||||
|
||||
struct HashCrc32 final : HashSource {
|
||||
void Update(const void* buf, s64 size) override {
|
||||
void Update(const void* buf, s64 size, s64 file_size) override {
|
||||
m_seed = crc32CalculateWithSeed(m_seed, buf, size);
|
||||
}
|
||||
|
||||
@@ -88,7 +231,7 @@ struct HashMd5 final : HashSource {
|
||||
mbedtls_md5_free(&m_ctx);
|
||||
}
|
||||
|
||||
void Update(const void* buf, s64 size) override {
|
||||
void Update(const void* buf, s64 size, s64 file_size) override {
|
||||
mbedtls_md5_update_ret(&m_ctx, (const u8*)buf, size);
|
||||
}
|
||||
|
||||
@@ -113,7 +256,7 @@ struct HashSha1 final : HashSource {
|
||||
sha1ContextCreate(&m_ctx);
|
||||
}
|
||||
|
||||
void Update(const void* buf, s64 size) override {
|
||||
void Update(const void* buf, s64 size, s64 file_size) override {
|
||||
sha1ContextUpdate(&m_ctx, buf, size);
|
||||
}
|
||||
|
||||
@@ -138,7 +281,7 @@ struct HashSha256 final : HashSource {
|
||||
sha256ContextCreate(&m_ctx);
|
||||
}
|
||||
|
||||
void Update(const void* buf, s64 size) override {
|
||||
void Update(const void* buf, s64 size, s64 file_size) override {
|
||||
sha256ContextUpdate(&m_ctx, buf, size);
|
||||
}
|
||||
|
||||
@@ -167,7 +310,7 @@ Result Hash(ui::ProgressBox* pbox, std::unique_ptr<HashSource> hash, BaseSource*
|
||||
return source->Read(data, off, size, bytes_read);
|
||||
},
|
||||
[&](const void* data, s64 off, s64 size) -> Result {
|
||||
hash->Update(data, size);
|
||||
hash->Update(data, size, file_size);
|
||||
R_SUCCEED();
|
||||
}
|
||||
));
|
||||
@@ -184,6 +327,9 @@ auto GetTypeStr(Type type) -> const char* {
|
||||
case Type::Md5: return "MD5";
|
||||
case Type::Sha1: return "SHA1";
|
||||
case Type::Sha256: return "SHA256";
|
||||
case Type::Null: return "/dev/null (Speed Test)";
|
||||
case Type::Deflate: return "Deflate (Speed Test)";
|
||||
case Type::Zstd: return "ZSTD (Speed Test)";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
@@ -194,6 +340,9 @@ Result Hash(ui::ProgressBox* pbox, Type type, BaseSource* source, std::string& o
|
||||
case Type::Md5: return Hash(pbox, std::make_unique<HashMd5>(), source, out);
|
||||
case Type::Sha1: return Hash(pbox, std::make_unique<HashSha1>(), source, out);
|
||||
case Type::Sha256: return Hash(pbox, std::make_unique<HashSha256>(), source, out);
|
||||
case Type::Null: return Hash(pbox, std::make_unique<HashNull>(), source, out);
|
||||
case Type::Deflate: return Hash(pbox, std::make_unique<HashDeflate>(), source, out);
|
||||
case Type::Zstd: return Hash(pbox, std::make_unique<HashZstd>(), source, out);
|
||||
}
|
||||
std::unreachable();
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
namespace sphaira::haze {
|
||||
namespace {
|
||||
|
||||
#if ENABLE_NETWORK_INSTALL
|
||||
struct InstallSharedData {
|
||||
Mutex mutex;
|
||||
std::string current_file;
|
||||
@@ -25,7 +24,6 @@ struct InstallSharedData {
|
||||
bool in_progress;
|
||||
bool enabled;
|
||||
};
|
||||
#endif
|
||||
|
||||
constexpr int THREAD_PRIO = 0x20;
|
||||
constexpr int THREAD_CORE = 2;
|
||||
@@ -33,7 +31,6 @@ std::atomic_bool g_should_exit = false;
|
||||
bool g_is_running{false};
|
||||
Mutex g_mutex{};
|
||||
|
||||
#if ENABLE_NETWORK_INSTALL
|
||||
InstallSharedData g_shared_data{};
|
||||
|
||||
const char* SUPPORTED_EXT[] = {
|
||||
@@ -58,7 +55,6 @@ void on_thing() {
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
struct FsProxyBase : ::haze::FileSystemProxyImpl {
|
||||
FsProxyBase(const char* name, const char* display_name) : m_name{name}, m_display_name{display_name} {
|
||||
@@ -411,7 +407,6 @@ struct FsDevNullProxy final : FsProxyVfs {
|
||||
}
|
||||
};
|
||||
|
||||
#if ENABLE_NETWORK_INSTALL
|
||||
struct FsInstallProxy final : FsProxyVfs {
|
||||
using FsProxyVfs::FsProxyVfs;
|
||||
|
||||
@@ -537,7 +532,6 @@ struct FsInstallProxy final : FsProxyVfs {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
#endif
|
||||
|
||||
::haze::FsEntries g_fs_entries{};
|
||||
|
||||
@@ -582,9 +576,7 @@ bool Init() {
|
||||
g_fs_entries.emplace_back(std::make_shared<FsProxy>(std::make_unique<fs::FsNativeImage>(FsImageDirectoryId_Nand), "image_nand", "Image nand"));
|
||||
g_fs_entries.emplace_back(std::make_shared<FsProxy>(std::make_unique<fs::FsNativeImage>(FsImageDirectoryId_Sd), "image_sd", "Image sd"));
|
||||
g_fs_entries.emplace_back(std::make_shared<FsDevNullProxy>("DevNull", "DevNull (Speed Test)"));
|
||||
#if ENABLE_NETWORK_INSTALL
|
||||
g_fs_entries.emplace_back(std::make_shared<FsInstallProxy>("install", "Install (NSP, XCI, NSZ, XCZ)"));
|
||||
#endif
|
||||
|
||||
g_should_exit = false;
|
||||
if (!::haze::Initialize(haze_callback, THREAD_PRIO, THREAD_CORE, g_fs_entries)) {
|
||||
@@ -595,6 +587,11 @@ bool Init() {
|
||||
return g_is_running = true;
|
||||
}
|
||||
|
||||
bool IsInit() {
|
||||
SCOPED_MUTEX(&g_mutex);
|
||||
return g_is_running;
|
||||
}
|
||||
|
||||
void Exit() {
|
||||
SCOPED_MUTEX(&g_mutex);
|
||||
if (!g_is_running) {
|
||||
@@ -609,7 +606,6 @@ void Exit() {
|
||||
log_write("[MTP] exitied\n");
|
||||
}
|
||||
|
||||
#if ENABLE_NETWORK_INSTALL
|
||||
void InitInstallMode(const OnInstallStart& on_start, const OnInstallWrite& on_write, const OnInstallClose& on_close) {
|
||||
SCOPED_MUTEX(&g_shared_data.mutex);
|
||||
g_shared_data.on_start = on_start;
|
||||
@@ -622,6 +618,5 @@ void DisableInstallMode() {
|
||||
SCOPED_MUTEX(&g_shared_data.mutex);
|
||||
g_shared_data.enabled = false;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace sphaira::haze
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "location.hpp"
|
||||
#include "fs.hpp"
|
||||
#include "app.hpp"
|
||||
#include "usbdvd.hpp"
|
||||
|
||||
#include <ff.h>
|
||||
#include <cstring>
|
||||
@@ -76,9 +77,22 @@ auto Load() -> Entries {
|
||||
}
|
||||
|
||||
auto GetStdio(bool write) -> StdioEntries {
|
||||
StdioEntries out{};
|
||||
|
||||
// try and load usbdvd entry.
|
||||
// todo: check if more than 1 entry is supported.
|
||||
// todo: only call if usbdvd is init.
|
||||
if (!write) {
|
||||
StdioEntry entry;
|
||||
if (usbdvd::GetMountPoint(entry)) {
|
||||
out.emplace_back(entry);
|
||||
}
|
||||
}
|
||||
|
||||
// bail out early if usbhdd is disabled.
|
||||
if (!App::GetHddEnable()) {
|
||||
log_write("[USBHSFS] not enabled\n");
|
||||
return {};
|
||||
return out;
|
||||
}
|
||||
|
||||
static UsbHsFsDevice devices[0x20];
|
||||
@@ -86,8 +100,6 @@ auto GetStdio(bool write) -> StdioEntries {
|
||||
log_write("[USBHSFS] got connected: %u\n", usbHsFsGetPhysicalDeviceCount());
|
||||
log_write("[USBHSFS] got count: %u\n", count);
|
||||
|
||||
StdioEntries out{};
|
||||
|
||||
for (s32 i = 0; i < count; i++) {
|
||||
const auto& e = devices[i];
|
||||
|
||||
@@ -109,9 +121,6 @@ auto GetStdio(bool write) -> StdioEntries {
|
||||
auto GetFat() -> StdioEntries {
|
||||
StdioEntries out{};
|
||||
|
||||
// todo: move this somewhere else.
|
||||
out.emplace_back("Qlaunch_romfs:/", "Qlaunch RomFS (Read Only)", true);
|
||||
|
||||
for (auto& e : VolumeStr) {
|
||||
char path[64];
|
||||
std::snprintf(path, sizeof(path), "%s:/", e);
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#include "log.hpp"
|
||||
#include "defines.hpp"
|
||||
#include <cstdio>
|
||||
#include <cstdarg>
|
||||
#include <ctime>
|
||||
#include <atomic>
|
||||
#include <unistd.h>
|
||||
#include <mutex>
|
||||
#include <switch.h>
|
||||
|
||||
#if sphaira_USE_LOG
|
||||
@@ -10,18 +12,19 @@ namespace {
|
||||
|
||||
constexpr const char* logpath = "/config/sphaira/log.txt";
|
||||
|
||||
int nxlink_socket{};
|
||||
bool g_file_open{};
|
||||
std::mutex mutex{};
|
||||
std::atomic_int32_t nxlink_socket{};
|
||||
std::atomic_bool g_file_open{};
|
||||
Mutex g_mutex;
|
||||
|
||||
void log_write_arg_internal(const char* s, std::va_list* v) {
|
||||
const auto t = std::time(nullptr);
|
||||
const auto tm = std::localtime(&t);
|
||||
|
||||
static char buf[512];
|
||||
char buf[512];
|
||||
const auto len = std::snprintf(buf, sizeof(buf), "[%02u:%02u:%02u] -> ", tm->tm_hour, tm->tm_min, tm->tm_sec);
|
||||
std::vsnprintf(buf + len, sizeof(buf) - len, s, *v);
|
||||
|
||||
SCOPED_MUTEX(&g_mutex);
|
||||
if (g_file_open) {
|
||||
auto file = std::fopen(logpath, "a");
|
||||
if (file) {
|
||||
@@ -39,7 +42,7 @@ void log_write_arg_internal(const char* s, std::va_list* v) {
|
||||
extern "C" {
|
||||
|
||||
auto log_file_init() -> bool {
|
||||
std::scoped_lock lock{mutex};
|
||||
SCOPED_MUTEX(&g_mutex);
|
||||
if (g_file_open) {
|
||||
return false;
|
||||
}
|
||||
@@ -55,7 +58,7 @@ auto log_file_init() -> bool {
|
||||
}
|
||||
|
||||
auto log_nxlink_init() -> bool {
|
||||
std::scoped_lock lock{mutex};
|
||||
SCOPED_MUTEX(&g_mutex);
|
||||
if (nxlink_socket) {
|
||||
return false;
|
||||
}
|
||||
@@ -65,14 +68,14 @@ auto log_nxlink_init() -> bool {
|
||||
}
|
||||
|
||||
void log_file_exit() {
|
||||
std::scoped_lock lock{mutex};
|
||||
SCOPED_MUTEX(&g_mutex);
|
||||
if (g_file_open) {
|
||||
g_file_open = false;
|
||||
}
|
||||
}
|
||||
|
||||
void log_nxlink_exit() {
|
||||
std::scoped_lock lock{mutex};
|
||||
SCOPED_MUTEX(&g_mutex);
|
||||
if (nxlink_socket) {
|
||||
close(nxlink_socket);
|
||||
nxlink_socket = 0;
|
||||
@@ -80,7 +83,6 @@ void log_nxlink_exit() {
|
||||
}
|
||||
|
||||
bool log_is_init() {
|
||||
std::scoped_lock lock{mutex};
|
||||
return g_file_open || nxlink_socket;
|
||||
}
|
||||
|
||||
@@ -89,7 +91,6 @@ void log_write(const char* s, ...) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::scoped_lock lock{mutex};
|
||||
std::va_list v{};
|
||||
va_start(v, s);
|
||||
log_write_arg_internal(s, &v);
|
||||
@@ -101,7 +102,6 @@ void log_write_arg(const char* s, va_list* v) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::scoped_lock lock{mutex};
|
||||
log_write_arg_internal(s, v);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include <memory>
|
||||
#include "app.hpp"
|
||||
#include "log.hpp"
|
||||
#include "ui/menus/main_menu.hpp"
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (!argc || !argv) {
|
||||
@@ -9,6 +10,7 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
|
||||
auto app = std::make_unique<sphaira::App>(argv[0]);
|
||||
app->Push<sphaira::ui::menu::main::MainMenu>();
|
||||
app->Loop();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
#include <cstring>
|
||||
#include <cstdio>
|
||||
|
||||
#include "log.hpp"
|
||||
|
||||
namespace sphaira::mz {
|
||||
namespace {
|
||||
|
||||
@@ -193,8 +195,10 @@ voidpf minizip_open_file_func_stdio(voidpf opaque, const void* filename, int mod
|
||||
if ((mode & ZLIB_FILEFUNC_MODE_READWRITEFILTER) == ZLIB_FILEFUNC_MODE_READ) {
|
||||
mode_fopen = "rb";
|
||||
} else if (mode & ZLIB_FILEFUNC_MODE_EXISTING) {
|
||||
log_write("[ZIP] opening r/w\n");
|
||||
mode_fopen = "r+b";
|
||||
} else if (mode & ZLIB_FILEFUNC_MODE_CREATE) {
|
||||
log_write("[ZIP] opening r/w +\n");
|
||||
mode_fopen = "wb";
|
||||
} else {
|
||||
return NULL;
|
||||
@@ -219,6 +223,7 @@ long minizip_seek_file_func_stdio(voidpf opaque, voidpf stream, ZPOS64_T offset,
|
||||
|
||||
uLong minizip_read_file_func_stdio(voidpf opaque, voidpf stream, void* buf, uLong size) {
|
||||
auto file = static_cast<std::FILE*>(stream);
|
||||
log_write("[ZIP] doing read\n");
|
||||
return std::fread(buf, 1, size, file);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,11 +13,6 @@
|
||||
namespace sphaira {
|
||||
namespace {
|
||||
|
||||
struct NroData {
|
||||
NroStart start;
|
||||
NroHeader header;
|
||||
};
|
||||
|
||||
auto nro_parse_internal(fs::Fs* fs, const fs::FsPath& path, NroEntry& entry) -> Result {
|
||||
entry.path = path;
|
||||
|
||||
@@ -60,7 +55,7 @@ auto nro_parse_internal(fs::Fs* fs, const fs::FsPath& path, NroEntry& entry) ->
|
||||
std::strcpy(nacp.lang.author, "Unknown");
|
||||
std::strcpy(nacp.display_version, "Unknown");
|
||||
|
||||
entry.romfs_offset = entry.romfs_size = entry.icon_offset = entry.icon_size = 0;
|
||||
entry.icon_offset = entry.icon_size = 0;
|
||||
entry.is_nacp_valid = false;
|
||||
} else {
|
||||
entry.size += sizeof(asset) + asset.icon.size + asset.nacp.size + asset.romfs.size;
|
||||
@@ -70,8 +65,6 @@ auto nro_parse_internal(fs::Fs* fs, const fs::FsPath& path, NroEntry& entry) ->
|
||||
// lazy load the icons
|
||||
entry.icon_size = asset.icon.size;
|
||||
entry.icon_offset = data.header.size + asset.icon.offset;
|
||||
entry.romfs_offset = data.header.size + asset.romfs.offset;
|
||||
entry.romfs_size = asset.romfs.size;
|
||||
entry.is_nacp_valid = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "nro.hpp"
|
||||
#include "log.hpp"
|
||||
#include "fs.hpp"
|
||||
#include "utils/thread.hpp"
|
||||
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
@@ -24,7 +25,7 @@ namespace {
|
||||
using Socket = int;
|
||||
constexpr s32 SERVER_PORT = NXLINK_SERVER_PORT;
|
||||
constexpr s32 CLIENT_PORT = NXLINK_CLIENT_PORT;
|
||||
constexpr s32 ZLIB_CHUNK = 0x4000;
|
||||
constexpr s32 ZLIB_CHUNK = 1024*64;
|
||||
|
||||
constexpr s32 ERR_OK = 0;
|
||||
constexpr s32 ERR_FILE = -1;
|
||||
@@ -35,7 +36,7 @@ constexpr const char UDP_MAGIC_SERVER[] = {"nxboot"};
|
||||
constexpr const char UDP_MAGIC_CLIENT[] = {"bootnx"};
|
||||
|
||||
Thread g_thread{};
|
||||
std::mutex g_mutex{};
|
||||
Mutex g_mutex{};
|
||||
std::atomic_bool g_quit{false};
|
||||
bool g_is_running{false};
|
||||
NxlinkCallback g_callback{};
|
||||
@@ -55,7 +56,9 @@ struct SocketWrapper {
|
||||
}
|
||||
}
|
||||
void nonBlocking() {
|
||||
fcntl(sock, F_SETFL, fcntl(sock, F_GETFL) | O_NONBLOCK);
|
||||
if (this->sock > 0) {
|
||||
fcntl(sock, F_SETFL, fcntl(sock, F_GETFL) | O_NONBLOCK);
|
||||
}
|
||||
}
|
||||
Socket operator=(Socket s) { return this->sock = s; }
|
||||
operator int() { return this->sock; }
|
||||
@@ -106,11 +109,11 @@ void WriteCallbackProgress(NxlinkCallbackType type, s64 offset, s64 size) {
|
||||
g_callback(&data);
|
||||
}
|
||||
|
||||
auto recvall(int sock, void* buf, int size) -> bool {
|
||||
auto recvall(int sock, void* buf, int size, sockaddr* src_addr = nullptr, socklen_t* addrlen = nullptr) -> bool {
|
||||
auto p = static_cast<u8*>(buf);
|
||||
int got{}, left{size};
|
||||
while (!g_quit && got < size) {
|
||||
const auto len = recv(sock, p + got, left, 0);
|
||||
const auto len = recvfrom(sock, p + got, left, 0, src_addr, addrlen);
|
||||
if (len == -1) {
|
||||
if (errno != EWOULDBLOCK && errno != EAGAIN) {
|
||||
return false;
|
||||
@@ -124,11 +127,11 @@ auto recvall(int sock, void* buf, int size) -> bool {
|
||||
return !g_quit;
|
||||
}
|
||||
|
||||
auto sendall(Socket sock, const void* buf, int size) -> bool {
|
||||
auto sendall(Socket sock, const void* buf, int size, const sockaddr* dest_addr = nullptr, socklen_t addrlen = 0) -> bool {
|
||||
auto p = static_cast<const u8*>(buf);
|
||||
int sent{}, left{size};
|
||||
while (!g_quit && sent < size) {
|
||||
const auto len = send(sock, p + sent, left, 0);
|
||||
const auto len = sendto(sock, p + sent, left, 0, dest_addr, addrlen);
|
||||
if (len == -1) {
|
||||
if (errno != EWOULDBLOCK && errno != EAGAIN) {
|
||||
return false;
|
||||
@@ -141,6 +144,23 @@ auto sendall(Socket sock, const void* buf, int size) -> bool {
|
||||
return !g_quit;
|
||||
}
|
||||
|
||||
auto acceptall(Socket sock, sockaddr* addr, socklen_t* addrlen) -> Socket {
|
||||
while (!g_quit) {
|
||||
Socket connfd = accept(sock, addr, addrlen);
|
||||
if (connfd < 0) {
|
||||
if (errno != EWOULDBLOCK && errno != EAGAIN) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
log_write("[NXLINK] blocking socket in accept, trying again\n");
|
||||
svcSleepThread(1e+6);
|
||||
} else {
|
||||
return connfd;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto get_file_data(Socket sock, int max) -> std::vector<u8> {
|
||||
std::vector<u8> buf(max);
|
||||
std::vector<u8> chunk(ZLIB_CHUNK);
|
||||
@@ -173,7 +193,7 @@ auto get_file_data(Socket sock, int max) -> std::vector<u8> {
|
||||
}
|
||||
|
||||
void loop(void* args) {
|
||||
log_write("in nxlink thread func\n");
|
||||
log_write("[NXLINK] in nxlink thread func\n");
|
||||
const sockaddr_in servaddr{
|
||||
.sin_family = AF_INET,
|
||||
.sin_port = htons(SERVER_PORT),
|
||||
@@ -213,33 +233,33 @@ void loop(void* args) {
|
||||
SocketWrapper sock_udp(AF_INET, SOCK_DGRAM, 0);
|
||||
|
||||
if (sock < 0 || sock_udp < 0) {
|
||||
log_write("failed to get sock/sock_udp: 0x%X\n", socketGetLastResult());
|
||||
log_write("[NXLINK] failed to get sock/sock_udp: 0x%X %s\n", socketGetLastResult(), strerror(errno));
|
||||
continue;
|
||||
}
|
||||
|
||||
u32 tmpval = 1;
|
||||
if (0 > setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &tmpval, sizeof(tmpval))) {
|
||||
log_write("set sockopt(): 0x%X\n", socketGetLastResult());
|
||||
log_write("[NXLINK] set sockopt(): 0x%X %s\n", socketGetLastResult(), strerror(errno));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (0 > setsockopt(sock_udp, SOL_SOCKET, SO_REUSEADDR, &tmpval, sizeof(tmpval))) {
|
||||
log_write("set sockopt(): 0x%X\n", socketGetLastResult());
|
||||
log_write("[NXLINK] set sockopt(): 0x%X %s\n", socketGetLastResult(), strerror(errno));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (0 > bind(sock, (const sockaddr*)&servaddr, sizeof(servaddr))) {
|
||||
log_write("failed to get bind(sock): 0x%X\n", socketGetLastResult());
|
||||
log_write("[NXLINK] failed to get bind(sock): 0x%X %s\n", socketGetLastResult(), strerror(errno));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (0 > bind(sock_udp, (const sockaddr*)&servaddr, sizeof(servaddr))) {
|
||||
log_write("failed to get bind(sock_udp): 0x%X\n", socketGetLastResult());
|
||||
log_write("[NXLINK] failed to get bind(sock_udp): 0x%X %s\n", socketGetLastResult(), strerror(errno));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (0 > listen(sock, 10)) {
|
||||
log_write("failed to get listen: 0x%X\n", socketGetLastResult());
|
||||
log_write("[NXLINK] failed to get listen: 0x%X %s\n", socketGetLastResult(), strerror(errno));
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -252,7 +272,7 @@ void loop(void* args) {
|
||||
pfds[1].events = POLLIN;
|
||||
|
||||
while (!g_quit) {
|
||||
auto poll_rc = poll(pfds, std::size(pfds), 1000/60);
|
||||
auto poll_rc = poll(pfds, std::size(pfds), 100);
|
||||
if (poll_rc < 0) {
|
||||
break;
|
||||
} else if (poll_rc == 0) {
|
||||
@@ -264,48 +284,52 @@ void loop(void* args) {
|
||||
if (pfds[1].revents & POLLIN) {
|
||||
char recvbuf[6];
|
||||
socklen_t from_len = sizeof(sa_remote);
|
||||
auto udp_len = recvfrom(sock_udp, recvbuf, sizeof(recvbuf), 0, (sockaddr*)&sa_remote, &from_len);
|
||||
if (udp_len == sizeof(recvbuf) && !std::strncmp(recvbuf, UDP_MAGIC_SERVER, std::strlen(UDP_MAGIC_SERVER))) {
|
||||
// log_write("got udp len: %d - %.*s\n", udp_len, udp_len, recvbuf);
|
||||
if (!recvall(sock_udp, recvbuf, sizeof(recvbuf), (sockaddr*)&sa_remote, &from_len)) {
|
||||
log_write("[NXLINK] failed to get udp socket: 0x%X %s\n", socketGetLastResult(), strerror(errno));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!std::strncmp(recvbuf, UDP_MAGIC_SERVER, std::strlen(UDP_MAGIC_SERVER))) {
|
||||
sa_remote.sin_family = AF_INET;
|
||||
sa_remote.sin_port = htons(NXLINK_CLIENT_PORT);
|
||||
udp_len = sendto(sock_udp, UDP_MAGIC_CLIENT, std::strlen(UDP_MAGIC_CLIENT), 0, (const sockaddr*)&sa_remote, sizeof(sa_remote));
|
||||
if (udp_len != std::strlen(UDP_MAGIC_CLIENT)) {
|
||||
log_write("nxlink failed to send udp packet\n");
|
||||
if (!sendall(sock_udp, UDP_MAGIC_CLIENT, std::strlen(UDP_MAGIC_CLIENT), (const sockaddr*)&sa_remote, sizeof(sa_remote))) {
|
||||
log_write("[NXLINK] failed to send udp socket: 0x%X %s\n", socketGetLastResult(), strerror(errno));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
socklen_t accept_len = sizeof(sa_remote);
|
||||
SocketWrapper connfd = accept(sock, (sockaddr*)&sa_remote, &accept_len);
|
||||
SocketWrapper connfd = acceptall(sock, (sockaddr*)&sa_remote, &accept_len);
|
||||
if (connfd < 0) {
|
||||
log_write("[NXLINK] failed to accept socket: 0x%X %s\n", socketGetLastResult(), strerror(errno));
|
||||
continue;
|
||||
}
|
||||
|
||||
WriteCallbackNone(NxlinkCallbackType_Connected);
|
||||
|
||||
u32 namelen{};
|
||||
if (!recvall(connfd, &namelen, sizeof(namelen))) {
|
||||
log_write("failed to get name: 0x%X\n", socketGetLastResult());
|
||||
log_write("[NXLINK] failed to get name: 0x%X %s\n", socketGetLastResult(), strerror(errno));
|
||||
continue;
|
||||
}
|
||||
|
||||
fs::FsPath name{};
|
||||
if (namelen >= sizeof(name)) {
|
||||
log_write("namelen is bigger than name: 0x%X\n", socketGetLastResult());
|
||||
log_write("[NXLINK] namelen is bigger than name: 0x%X %s\n", socketGetLastResult(), strerror(errno));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!recvall(connfd, name, namelen)) {
|
||||
log_write("failed to get name: 0x%X\n", socketGetLastResult());
|
||||
log_write("[NXLINK] failed to get name: 0x%X %s\n", socketGetLastResult(), strerror(errno));
|
||||
continue;
|
||||
}
|
||||
|
||||
log_write("got name: %s\n", name.s);
|
||||
log_write("[NXLINK] got name: %s\n", name.s);
|
||||
|
||||
u32 filesize{};
|
||||
if (!recvall(connfd, &filesize, sizeof(filesize))) {
|
||||
log_write("failed to get filesize: 0x%X\n", socketGetLastResult());
|
||||
log_write("[NXLINK] failed to get filesize: 0x%X %s\n", socketGetLastResult(), strerror(errno));
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -319,7 +343,7 @@ void loop(void* args) {
|
||||
|
||||
// tell nxlink that we want this file
|
||||
if (!sendall(connfd, &ERR_OK, sizeof(ERR_OK))) {
|
||||
log_write("failed to tell nxlink that we want the file: 0x%X\n", socketGetLastResult());
|
||||
log_write("[NXLINK] failed to tell nxlink that we want the file: 0x%X %s\n", socketGetLastResult(), strerror(errno));
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -329,6 +353,7 @@ void loop(void* args) {
|
||||
WriteCallbackFile(NxlinkCallbackType_WriteEnd, name);
|
||||
|
||||
if (file_data.empty()) {
|
||||
log_write("[NXLINK] failed to get file data: 0x%X %s\n", socketGetLastResult(), strerror(errno));
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -343,7 +368,7 @@ void loop(void* args) {
|
||||
// if (R_FAILED(rc = create_directories(fs, path))) {
|
||||
if (R_FAILED(rc = fs.CreateDirectoryRecursivelyWithPath(path))) {
|
||||
sendall(connfd, &ERR_FILE, sizeof(ERR_FILE));
|
||||
log_write("failed to create directories: %X\n", rc);
|
||||
log_write("[NXLINK] failed to create directories: %X\n", rc);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -351,7 +376,7 @@ void loop(void* args) {
|
||||
const auto temp_path = path + "~";
|
||||
if (R_FAILED(rc = fs.CreateFile(temp_path, file_data.size(), 0)) && rc != FsError_PathAlreadyExists) {
|
||||
sendall(connfd, &ERR_FILE, sizeof(ERR_FILE));
|
||||
log_write("failed to create file: %X\n", rc);
|
||||
log_write("[NXLINK] failed to create file: %X\n", rc);
|
||||
continue;
|
||||
}
|
||||
ON_SCOPE_EXIT(fs.DeleteFile(temp_path));
|
||||
@@ -360,13 +385,13 @@ void loop(void* args) {
|
||||
fs::File f;
|
||||
if (R_FAILED(rc = fs.OpenFile(temp_path, FsOpenMode_Write, &f))) {
|
||||
sendall(connfd, &ERR_FILE, sizeof(ERR_FILE));
|
||||
log_write("failed to open file %X\n", rc);
|
||||
log_write("[NXLINK] failed to open file %X\n", rc);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (R_FAILED(rc = f.SetSize(file_data.size()))) {
|
||||
sendall(connfd, &ERR_FILE, sizeof(ERR_FILE));
|
||||
log_write("failed to set file size: 0x%X\n", socketGetLastResult());
|
||||
log_write("[NXLINK] failed to set file size: 0x%X\n", socketGetLastResult());
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -374,7 +399,7 @@ void loop(void* args) {
|
||||
while (offset < file_data.size()) {
|
||||
svcSleepThread(YieldType_WithoutCoreMigration);
|
||||
|
||||
u64 chunk_size = ZLIB_CHUNK;
|
||||
u64 chunk_size = 1024*1024;
|
||||
if (offset + chunk_size > file_data.size()) {
|
||||
chunk_size = file_data.size() - offset;
|
||||
}
|
||||
@@ -387,23 +412,25 @@ void loop(void* args) {
|
||||
// if (R_FAILED(rc = fsFileWrite(&f, 0, file_data.data(), file_data.size(), FsWriteOption_None))) {
|
||||
if (R_FAILED(rc)) {
|
||||
sendall(connfd, &ERR_FILE, sizeof(ERR_FILE));
|
||||
log_write("failed to write: 0x%X\n", socketGetLastResult());
|
||||
log_write("[NXLINK] failed to write: 0x%X\n", socketGetLastResult());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (R_FAILED(rc = fs.DeleteFile(path)) && rc != FsError_PathNotFound) {
|
||||
log_write("failed to delete %X\n", rc);
|
||||
log_write("[NXLINK] failed to delete %X\n", rc);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (R_FAILED(rc = fs.RenameFile(temp_path, path))) {
|
||||
log_write("failed to rename %X\n", rc);
|
||||
log_write("[NXLINK] failed to rename %X\n", rc);
|
||||
continue;
|
||||
}
|
||||
|
||||
// log error here, but don't fail as we already have the nro
|
||||
// so this just means that nxlink server won't start.
|
||||
if (!sendall(connfd, &ERR_OK, sizeof(ERR_OK))) {
|
||||
log_write("failed to send ok message: 0x%X\n", socketGetLastResult());
|
||||
log_write("[NXLINK] failed to send ok message: 0x%X %s\n", socketGetLastResult(), strerror(errno));
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -435,7 +462,7 @@ void loop(void* args) {
|
||||
}
|
||||
args += nxlinked;
|
||||
|
||||
// log_write("launching with: %s %s\n", path.c_str(), args.c_str());
|
||||
// log_write("[NXLINK] launching with: %s %s\n", path.c_str(), args.c_str());
|
||||
if (R_SUCCEEDED(sphaira::nro_launch(path, args))) {
|
||||
g_quit = true;
|
||||
}
|
||||
@@ -449,7 +476,7 @@ void loop(void* args) {
|
||||
extern "C" {
|
||||
|
||||
bool nxlinkInitialize(NxlinkCallback callback) {
|
||||
std::scoped_lock lock{g_mutex};
|
||||
SCOPED_MUTEX(&g_mutex);
|
||||
if (g_is_running) {
|
||||
return false;
|
||||
}
|
||||
@@ -458,13 +485,13 @@ bool nxlinkInitialize(NxlinkCallback callback) {
|
||||
g_quit = false;
|
||||
|
||||
Result rc;
|
||||
if (R_FAILED(rc = threadCreate(&g_thread, loop, nullptr, nullptr, 1024*64, PRIO_PREEMPTIVE, 2))) {
|
||||
log_write("failed to create nxlink thread: 0x%X\n", rc);
|
||||
if (R_FAILED(rc = sphaira::utils::CreateThread(&g_thread, loop, nullptr, 1024*64))) {
|
||||
log_write("[NXLINK] failed to create nxlink thread: 0x%X\n", rc);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (R_FAILED(rc = threadStart(&g_thread))) {
|
||||
log_write("failed to start nxlink thread: 0x%X\n", rc);
|
||||
log_write("[NXLINK] failed to start nxlink thread: 0x%X\n", rc);
|
||||
threadClose(&g_thread);
|
||||
return false;
|
||||
}
|
||||
@@ -473,17 +500,15 @@ bool nxlinkInitialize(NxlinkCallback callback) {
|
||||
}
|
||||
|
||||
void nxlinkExit() {
|
||||
std::scoped_lock lock{g_mutex};
|
||||
if (g_is_running) {
|
||||
g_is_running = false;
|
||||
}
|
||||
SCOPED_MUTEX(&g_mutex);
|
||||
g_is_running = false;
|
||||
g_quit = true;
|
||||
threadWaitForExit(&g_thread);
|
||||
threadClose(&g_thread);
|
||||
}
|
||||
|
||||
void nxlinkSignalExit() {
|
||||
std::scoped_lock lock{g_mutex};
|
||||
SCOPED_MUTEX(&g_mutex);
|
||||
g_quit = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ auto OptionBase<T>::GetInternal(const char* name) -> T {
|
||||
m_value = ini_getbool(m_section.c_str(), name, m_default_value, App::CONFIG_PATH);
|
||||
} else if constexpr(std::is_same_v<T, long>) {
|
||||
m_value = ini_getl(m_section.c_str(), name, m_default_value, App::CONFIG_PATH);
|
||||
} else if constexpr(std::is_same_v<T, float>) {
|
||||
m_value = ini_getf(m_section.c_str(), name, m_default_value, App::CONFIG_PATH);
|
||||
} else if constexpr(std::is_same_v<T, std::string>) {
|
||||
char buf[FS_MAX_PATH];
|
||||
ini_gets(m_section.c_str(), name, m_default_value.c_str(), buf, sizeof(buf), App::CONFIG_PATH);
|
||||
@@ -52,6 +54,8 @@ void OptionBase<T>::Set(T value) {
|
||||
ini_putl(m_section.c_str(), m_name.c_str(), value, App::CONFIG_PATH);
|
||||
} else if constexpr(std::is_same_v<T, long>) {
|
||||
ini_putl(m_section.c_str(), m_name.c_str(), value, App::CONFIG_PATH);
|
||||
} else if constexpr(std::is_same_v<T, float>) {
|
||||
ini_putf(m_section.c_str(), m_name.c_str(), value, App::CONFIG_PATH);
|
||||
} else if constexpr(std::is_same_v<T, std::string>) {
|
||||
ini_puts(m_section.c_str(), m_name.c_str(), value.c_str(), App::CONFIG_PATH);
|
||||
}
|
||||
@@ -71,6 +75,8 @@ auto OptionBase<T>::LoadFrom(const char* name, const char* value) -> bool {
|
||||
m_value = ini_parse_getbool(value, m_default_value);
|
||||
} else if constexpr(std::is_same_v<T, long>) {
|
||||
m_value = ini_parse_getl(value, m_default_value);
|
||||
} else if constexpr(std::is_same_v<T, float>) {
|
||||
m_value = ini_atof(value);
|
||||
} else if constexpr(std::is_same_v<T, std::string>) {
|
||||
m_value = value;
|
||||
}
|
||||
@@ -84,6 +90,7 @@ auto OptionBase<T>::LoadFrom(const char* name, const char* value) -> bool {
|
||||
|
||||
template struct OptionBase<bool>;
|
||||
template struct OptionBase<long>;
|
||||
template struct OptionBase<float>;
|
||||
template struct OptionBase<std::string>;
|
||||
|
||||
} // namespace sphaira::option
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "defines.hpp"
|
||||
#include "app.hpp"
|
||||
#include "minizip_helper.hpp"
|
||||
#include "utils/thread.hpp"
|
||||
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
@@ -72,13 +73,13 @@ public:
|
||||
};
|
||||
|
||||
struct ThreadData {
|
||||
ThreadData(ui::ProgressBox* _pbox, s64 size, const ReadCallback& _rfunc, const WriteCallback& _wfunc, u64 buffer_size);
|
||||
ThreadData(ui::ProgressBox* _pbox, s64 size, const ReadCallback& _rfunc, const DecompressCallback& _dfunc, const WriteCallback& _wfunc, u64 buffer_size);
|
||||
|
||||
auto GetResults() volatile -> Result;
|
||||
void WakeAllThreads();
|
||||
|
||||
auto IsAnyRunning() volatile const -> bool {
|
||||
return read_running || write_running;
|
||||
return read_running || decompress_running || write_running;
|
||||
}
|
||||
|
||||
auto GetWriteOffset() volatile const -> s64 {
|
||||
@@ -100,6 +101,17 @@ struct ThreadData {
|
||||
void SetReadResult(Result result) {
|
||||
read_result = result;
|
||||
|
||||
// wake up decompress thread as it may be waiting on data that never comes.
|
||||
condvarWakeOne(std::addressof(can_decompress));
|
||||
|
||||
if (R_FAILED(result)) {
|
||||
ueventSignal(GetDoneEvent());
|
||||
}
|
||||
}
|
||||
|
||||
void SetDecompressResult(Result result) {
|
||||
decompress_result = result;
|
||||
|
||||
// wake up write thread as it may be waiting on data that never comes.
|
||||
condvarWakeOne(std::addressof(can_write));
|
||||
|
||||
@@ -110,6 +122,10 @@ struct ThreadData {
|
||||
|
||||
void SetWriteResult(Result result) {
|
||||
write_result = result;
|
||||
|
||||
// wake up decompress thread as it may be waiting on data that never comes.
|
||||
condvarWakeOne(std::addressof(can_decompress_write));
|
||||
|
||||
ueventSignal(GetDoneEvent());
|
||||
}
|
||||
|
||||
@@ -122,9 +138,12 @@ struct ThreadData {
|
||||
|
||||
Result Pull(void* data, s64 size, u64* bytes_read);
|
||||
Result readFuncInternal();
|
||||
Result decompressFuncInternal();
|
||||
Result writeFuncInternal();
|
||||
|
||||
private:
|
||||
Result SetDecompressBuf(std::vector<u8>& buf, s64 off, s64 size);
|
||||
Result GetDecompressBuf(std::vector<u8>& buf_out, s64& off_out);
|
||||
Result SetWriteBuf(std::vector<u8>& buf, s64 size);
|
||||
Result GetWriteBuf(std::vector<u8>& buf_out, s64& off_out);
|
||||
Result SetPullBuf(std::vector<u8>& buf, s64 size);
|
||||
@@ -136,21 +155,30 @@ private:
|
||||
// these need to be copied
|
||||
ui::ProgressBox* const pbox;
|
||||
const ReadCallback& rfunc;
|
||||
const DecompressCallback& dfunc;
|
||||
const WriteCallback& wfunc;
|
||||
|
||||
// these need to be created
|
||||
Mutex mutex{};
|
||||
Mutex read_mutex{};
|
||||
Mutex write_mutex{};
|
||||
|
||||
Mutex pull_mutex{};
|
||||
|
||||
CondVar can_read{};
|
||||
CondVar can_write{};
|
||||
CondVar can_decompress{};
|
||||
CondVar can_decompress_write{};
|
||||
|
||||
// only used when pull is active.
|
||||
CondVar can_pull{};
|
||||
CondVar can_pull_write{};
|
||||
|
||||
UEvent m_uevent_done{};
|
||||
UEvent m_uevent_progres{};
|
||||
|
||||
RingBuf<2> read_buffers{};
|
||||
RingBuf<2> write_buffers{};
|
||||
|
||||
std::vector<u8> pull_buffer{};
|
||||
s64 pull_buffer_offset{};
|
||||
|
||||
@@ -159,27 +187,35 @@ private:
|
||||
|
||||
// these are shared between threads
|
||||
std::atomic<s64> read_offset{};
|
||||
std::atomic<s64> decompress_offset{};
|
||||
std::atomic<s64> write_offset{};
|
||||
|
||||
std::atomic<Result> read_result{};
|
||||
std::atomic<Result> decompress_result{};
|
||||
std::atomic<Result> write_result{};
|
||||
std::atomic<Result> pull_result{};
|
||||
|
||||
std::atomic_bool read_running{true};
|
||||
std::atomic_bool decompress_running{true};
|
||||
std::atomic_bool write_running{true};
|
||||
};
|
||||
|
||||
ThreadData::ThreadData(ui::ProgressBox* _pbox, s64 size, const ReadCallback& _rfunc, const WriteCallback& _wfunc, u64 buffer_size)
|
||||
ThreadData::ThreadData(ui::ProgressBox* _pbox, s64 size, const ReadCallback& _rfunc, const DecompressCallback& _dfunc, const WriteCallback& _wfunc, u64 buffer_size)
|
||||
: pbox{_pbox}
|
||||
, rfunc{_rfunc}
|
||||
, dfunc{_dfunc}
|
||||
, wfunc{_wfunc}
|
||||
, read_buffer_size{buffer_size}
|
||||
, write_size{size} {
|
||||
mutexInit(std::addressof(mutex));
|
||||
mutexInit(std::addressof(read_mutex));
|
||||
mutexInit(std::addressof(write_mutex));
|
||||
mutexInit(std::addressof(pull_mutex));
|
||||
|
||||
condvarInit(std::addressof(can_read));
|
||||
condvarInit(std::addressof(can_decompress));
|
||||
condvarInit(std::addressof(can_decompress_write));
|
||||
condvarInit(std::addressof(can_write));
|
||||
|
||||
condvarInit(std::addressof(can_pull));
|
||||
condvarInit(std::addressof(can_pull_write));
|
||||
|
||||
@@ -190,6 +226,7 @@ ThreadData::ThreadData(ui::ProgressBox* _pbox, s64 size, const ReadCallback& _rf
|
||||
auto ThreadData::GetResults() volatile -> Result {
|
||||
R_TRY(pbox->ShouldExitResult());
|
||||
R_TRY(read_result.load());
|
||||
R_TRY(decompress_result.load());
|
||||
R_TRY(write_result.load());
|
||||
R_TRY(pull_result.load());
|
||||
R_SUCCEED();
|
||||
@@ -198,44 +235,80 @@ auto ThreadData::GetResults() volatile -> Result {
|
||||
void ThreadData::WakeAllThreads() {
|
||||
condvarWakeAll(std::addressof(can_read));
|
||||
condvarWakeAll(std::addressof(can_write));
|
||||
condvarWakeAll(std::addressof(can_decompress));
|
||||
condvarWakeAll(std::addressof(can_decompress_write));
|
||||
condvarWakeAll(std::addressof(can_pull));
|
||||
condvarWakeAll(std::addressof(can_pull_write));
|
||||
|
||||
mutexUnlock(std::addressof(mutex));
|
||||
mutexUnlock(std::addressof(read_mutex));
|
||||
mutexUnlock(std::addressof(write_mutex));
|
||||
mutexUnlock(std::addressof(pull_mutex));
|
||||
}
|
||||
|
||||
Result ThreadData::SetDecompressBuf(std::vector<u8>& buf, s64 off, s64 size) {
|
||||
buf.resize(size);
|
||||
|
||||
mutexLock(std::addressof(read_mutex));
|
||||
if (!read_buffers.ringbuf_free()) {
|
||||
if (!write_running) {
|
||||
R_SUCCEED();
|
||||
}
|
||||
R_TRY(condvarWait(std::addressof(can_read), std::addressof(read_mutex)));
|
||||
}
|
||||
|
||||
ON_SCOPE_EXIT(mutexUnlock(std::addressof(read_mutex)));
|
||||
R_TRY(GetResults());
|
||||
read_buffers.ringbuf_push(buf, off);
|
||||
return condvarWakeOne(std::addressof(can_decompress));
|
||||
}
|
||||
|
||||
Result ThreadData::GetDecompressBuf(std::vector<u8>& buf_out, s64& off_out) {
|
||||
mutexLock(std::addressof(read_mutex));
|
||||
if (!read_buffers.ringbuf_size()) {
|
||||
if (!read_running) {
|
||||
buf_out.resize(0);
|
||||
R_SUCCEED();
|
||||
}
|
||||
R_TRY(condvarWait(std::addressof(can_decompress), std::addressof(read_mutex)));
|
||||
}
|
||||
|
||||
ON_SCOPE_EXIT(mutexUnlock(std::addressof(read_mutex)));
|
||||
R_TRY(GetResults());
|
||||
read_buffers.ringbuf_pop(buf_out, off_out);
|
||||
return condvarWakeOne(std::addressof(can_read));
|
||||
}
|
||||
|
||||
Result ThreadData::SetWriteBuf(std::vector<u8>& buf, s64 size) {
|
||||
buf.resize(size);
|
||||
|
||||
mutexLock(std::addressof(mutex));
|
||||
mutexLock(std::addressof(write_mutex));
|
||||
if (!write_buffers.ringbuf_free()) {
|
||||
if (!write_running) {
|
||||
if (!decompress_running) {
|
||||
R_SUCCEED();
|
||||
}
|
||||
R_TRY(condvarWait(std::addressof(can_read), std::addressof(mutex)));
|
||||
R_TRY(condvarWait(std::addressof(can_decompress_write), std::addressof(write_mutex)));
|
||||
}
|
||||
|
||||
ON_SCOPE_EXIT(mutexUnlock(std::addressof(mutex)));
|
||||
ON_SCOPE_EXIT(mutexUnlock(std::addressof(write_mutex)));
|
||||
R_TRY(GetResults());
|
||||
write_buffers.ringbuf_push(buf, 0);
|
||||
return condvarWakeOne(std::addressof(can_write));
|
||||
}
|
||||
|
||||
Result ThreadData::GetWriteBuf(std::vector<u8>& buf_out, s64& off_out) {
|
||||
mutexLock(std::addressof(mutex));
|
||||
mutexLock(std::addressof(write_mutex));
|
||||
if (!write_buffers.ringbuf_size()) {
|
||||
if (!read_running) {
|
||||
if (!decompress_running) {
|
||||
buf_out.resize(0);
|
||||
R_SUCCEED();
|
||||
}
|
||||
R_TRY(condvarWait(std::addressof(can_write), std::addressof(mutex)));
|
||||
R_TRY(condvarWait(std::addressof(can_write), std::addressof(write_mutex)));
|
||||
}
|
||||
|
||||
ON_SCOPE_EXIT(mutexUnlock(std::addressof(mutex)));
|
||||
ON_SCOPE_EXIT(mutexUnlock(std::addressof(write_mutex)));
|
||||
R_TRY(GetResults());
|
||||
write_buffers.ringbuf_pop(buf_out, off_out);
|
||||
return condvarWakeOne(std::addressof(can_read));
|
||||
return condvarWakeOne(std::addressof(can_decompress_write));
|
||||
}
|
||||
|
||||
Result ThreadData::SetPullBuf(std::vector<u8>& buf, s64 size) {
|
||||
@@ -296,6 +369,7 @@ Result ThreadData::readFuncInternal() {
|
||||
|
||||
while (this->read_offset < this->write_size && R_SUCCEEDED(this->GetResults())) {
|
||||
// read more data
|
||||
const auto buffer_offset = this->read_offset.load();
|
||||
s64 read_size = this->read_buffer_size;
|
||||
|
||||
u64 bytes_read{};
|
||||
@@ -306,13 +380,82 @@ Result ThreadData::readFuncInternal() {
|
||||
}
|
||||
|
||||
auto buf_size = bytes_read;
|
||||
R_TRY(this->SetWriteBuf(buf, buf_size));
|
||||
R_TRY(this->SetDecompressBuf(buf, buffer_offset, buf_size));
|
||||
}
|
||||
|
||||
log_write("finished read thread success!\n");
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
// read thread reads all data from the source
|
||||
Result ThreadData::decompressFuncInternal() {
|
||||
ON_SCOPE_EXIT( decompress_running = false; );
|
||||
|
||||
std::vector<u8> buf{};
|
||||
std::vector<u8> temp_buf{};
|
||||
buf.reserve(this->read_buffer_size);
|
||||
temp_buf.reserve(this->read_buffer_size);
|
||||
const auto temp_buf_flush_max = this->read_buffer_size / 2;
|
||||
|
||||
while (this->decompress_offset < this->write_size && R_SUCCEEDED(this->GetResults())) {
|
||||
s64 decompress_buf_off{};
|
||||
R_TRY(this->GetDecompressBuf(buf, decompress_buf_off));
|
||||
if (buf.empty()) {
|
||||
log_write("exiting decompress func early because no data was received\n");
|
||||
break;
|
||||
}
|
||||
|
||||
if (this->dfunc) {
|
||||
R_TRY(this->dfunc(buf.data(), decompress_buf_off, buf.size(), [&](const void* _data, s64 size) -> Result {
|
||||
auto data = (const u8*)_data;
|
||||
|
||||
while (size) {
|
||||
const auto block_off = temp_buf.size();
|
||||
const auto rsize = std::min<s64>(size, temp_buf_flush_max - block_off);
|
||||
|
||||
temp_buf.resize(block_off + rsize);
|
||||
std::memcpy(temp_buf.data() + block_off, data, rsize);
|
||||
|
||||
if (temp_buf.size() == temp_buf_flush_max) {
|
||||
// log_write("flushing data: %zu %.2f MiB\n", temp_buf.size(), temp_buf.size() / 1024.0 / 1024.0);
|
||||
R_TRY(this->SetWriteBuf(temp_buf, temp_buf.size()));
|
||||
temp_buf.resize(0);
|
||||
}
|
||||
|
||||
size -= rsize;
|
||||
this->decompress_offset += rsize;
|
||||
data += rsize;
|
||||
|
||||
// const auto buf_off = temp_buf.size();
|
||||
// temp_buf.resize(buf_off + size);
|
||||
// std::memcpy(temp_buf.data() + buf_off, data, size);
|
||||
// this->decompress_offset += size;
|
||||
|
||||
// if (temp_buf.size() >= temp_buf_flush_max) {
|
||||
// // log_write("flushing data: %zu %.2f MiB\n", temp_buf.size(), temp_buf.size() / 1024.0 / 1024.0);
|
||||
// R_TRY(this->SetWriteBuf(temp_buf, temp_buf.size()));
|
||||
// temp_buf.resize(0);
|
||||
// }
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}));
|
||||
} else {
|
||||
this->decompress_offset += buf.size();
|
||||
R_TRY(this->SetWriteBuf(buf, buf.size()));
|
||||
}
|
||||
}
|
||||
|
||||
// flush buffer.
|
||||
if (!temp_buf.empty()) {
|
||||
log_write("flushing data: %zu\n", temp_buf.size());
|
||||
R_TRY(this->SetWriteBuf(temp_buf, temp_buf.size()));
|
||||
}
|
||||
|
||||
log_write("finished decompress thread success!\n");
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
// write thread writes data to wfunc.
|
||||
Result ThreadData::writeFuncInternal() {
|
||||
ON_SCOPE_EXIT( write_running = false; );
|
||||
@@ -349,17 +492,20 @@ void readFunc(void* d) {
|
||||
log_write("read thread returned now\n");
|
||||
}
|
||||
|
||||
void decompressFunc(void* d) {
|
||||
log_write("hello decomp thread func\n");
|
||||
auto t = static_cast<ThreadData*>(d);
|
||||
t->SetDecompressResult(t->decompressFuncInternal());
|
||||
log_write("decompress thread returned now\n");
|
||||
}
|
||||
|
||||
void writeFunc(void* d) {
|
||||
auto t = static_cast<ThreadData*>(d);
|
||||
t->SetWriteResult(t->writeFuncInternal());
|
||||
log_write("write thread returned now\n");
|
||||
}
|
||||
|
||||
auto GetAlternateCore(int id) {
|
||||
return id == 1 ? 2 : 1;
|
||||
}
|
||||
|
||||
Result TransferInternal(ui::ProgressBox* pbox, s64 size, const ReadCallback& rfunc, const WriteCallback& wfunc, const StartCallback2& sfunc, Mode mode, u64 buffer_size = NORMAL_BUFFER_SIZE) {
|
||||
Result TransferInternal(ui::ProgressBox* pbox, s64 size, const ReadCallback& rfunc, const DecompressCallback& dfunc, const WriteCallback& wfunc, const StartCallback2& sfunc, Mode mode, u64 buffer_size = NORMAL_BUFFER_SIZE) {
|
||||
const auto is_file_based_emummc = App::IsFileBaseEmummc();
|
||||
|
||||
if (is_file_based_emummc) {
|
||||
@@ -403,27 +549,30 @@ Result TransferInternal(ui::ProgressBox* pbox, s64 size, const ReadCallback& rfu
|
||||
R_SUCCEED();
|
||||
}
|
||||
else {
|
||||
const auto WRITE_THREAD_CORE = sfunc ? pbox->GetCpuId() : GetAlternateCore(pbox->GetCpuId());
|
||||
const auto READ_THREAD_CORE = GetAlternateCore(WRITE_THREAD_CORE);
|
||||
|
||||
ThreadData t_data{pbox, size, rfunc, wfunc, buffer_size};
|
||||
ThreadData t_data{pbox, size, rfunc, dfunc, wfunc, buffer_size};
|
||||
|
||||
Thread t_read{};
|
||||
R_TRY(threadCreate(&t_read, readFunc, std::addressof(t_data), nullptr, 1024*256, 0x3B, READ_THREAD_CORE));
|
||||
R_TRY(utils::CreateThread(&t_read, readFunc, std::addressof(t_data)));
|
||||
ON_SCOPE_EXIT(threadClose(&t_read));
|
||||
|
||||
Thread t_decompress{};
|
||||
R_TRY(utils::CreateThread(&t_decompress, decompressFunc, std::addressof(t_data)));
|
||||
ON_SCOPE_EXIT(threadClose(&t_decompress));
|
||||
|
||||
Thread t_write{};
|
||||
R_TRY(threadCreate(&t_write, writeFunc, std::addressof(t_data), nullptr, 1024*256, 0x3B, WRITE_THREAD_CORE));
|
||||
R_TRY(utils::CreateThread(&t_write, writeFunc, std::addressof(t_data)));
|
||||
ON_SCOPE_EXIT(threadClose(&t_write));
|
||||
|
||||
const auto start_threads = [&]() -> Result {
|
||||
log_write("starting threads\n");
|
||||
R_TRY(threadStart(std::addressof(t_read)));
|
||||
R_TRY(threadStart(std::addressof(t_decompress)));
|
||||
R_TRY(threadStart(std::addressof(t_write)));
|
||||
R_SUCCEED();
|
||||
};
|
||||
|
||||
ON_SCOPE_EXIT(threadWaitForExit(std::addressof(t_read)));
|
||||
ON_SCOPE_EXIT(threadWaitForExit(std::addressof(t_decompress)));
|
||||
ON_SCOPE_EXIT(threadWaitForExit(std::addressof(t_write)));
|
||||
|
||||
if (sfunc) {
|
||||
@@ -463,6 +612,8 @@ Result TransferInternal(ui::ProgressBox* pbox, s64 size, const ReadCallback& rfu
|
||||
|
||||
if (R_FAILED(waitSingleHandle(t_read.handle, 1000))) {
|
||||
continue;
|
||||
} else if (R_FAILED(waitSingleHandle(t_decompress.handle, 1000))) {
|
||||
continue;
|
||||
} else if (R_FAILED(waitSingleHandle(t_write.handle, 1000))) {
|
||||
continue;
|
||||
}
|
||||
@@ -485,18 +636,22 @@ Result TransferInternal(ui::ProgressBox* pbox, s64 size, const ReadCallback& rfu
|
||||
} // namespace
|
||||
|
||||
Result Transfer(ui::ProgressBox* pbox, s64 size, const ReadCallback& rfunc, const WriteCallback& wfunc, Mode mode) {
|
||||
return TransferInternal(pbox, size, rfunc, wfunc, nullptr, mode);
|
||||
return TransferInternal(pbox, size, rfunc, nullptr, wfunc, nullptr, mode);
|
||||
}
|
||||
|
||||
Result Transfer(ui::ProgressBox* pbox, s64 size, const ReadCallback& rfunc, const DecompressCallback& dfunc, const WriteCallback& wfunc, Mode mode) {
|
||||
return TransferInternal(pbox, size, rfunc, dfunc, wfunc, nullptr, mode);
|
||||
}
|
||||
|
||||
Result TransferPull(ui::ProgressBox* pbox, s64 size, const ReadCallback& rfunc, const StartCallback& sfunc, Mode mode) {
|
||||
return TransferInternal(pbox, size, rfunc, nullptr, [sfunc](StartThreadCallback start, PullCallback pull) -> Result {
|
||||
return TransferInternal(pbox, size, rfunc, nullptr, nullptr, [sfunc](StartThreadCallback start, PullCallback pull) -> Result {
|
||||
R_TRY(start());
|
||||
return sfunc(pull);
|
||||
}, mode);
|
||||
}
|
||||
|
||||
Result TransferPull(ui::ProgressBox* pbox, s64 size, const ReadCallback& rfunc, const StartCallback2& sfunc, Mode mode) {
|
||||
return TransferInternal(pbox, size, rfunc, nullptr, sfunc, mode);
|
||||
return TransferInternal(pbox, size, rfunc, nullptr, nullptr, sfunc, mode);
|
||||
}
|
||||
|
||||
Result TransferUnzip(ui::ProgressBox* pbox, void* zfile, fs::Fs* fs, const fs::FsPath& path, s64 size, u32 crc32, Mode mode) {
|
||||
@@ -537,6 +692,7 @@ Result TransferUnzip(ui::ProgressBox* pbox, void* zfile, fs::Fs* fs, const fs::F
|
||||
*bytes_read = result;
|
||||
R_SUCCEED();
|
||||
},
|
||||
nullptr,
|
||||
[&](const void* data, s64 off, s64 size) -> Result {
|
||||
return f.Write(off, data, size, FsWriteOption_None);
|
||||
},
|
||||
@@ -568,6 +724,7 @@ Result TransferZip(ui::ProgressBox* pbox, void* zfile, fs::Fs* fs, const fs::FsP
|
||||
}
|
||||
return rc;
|
||||
},
|
||||
nullptr,
|
||||
[&](const void* data, s64 off, s64 size) -> Result {
|
||||
if (ZIP_OK != zipWriteInFileInZip(zfile, data, size)) {
|
||||
log_write("failed to write zip file: %s\n", path.s);
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
#include "yati/nx/nca.hpp"
|
||||
#include "yati/nx/ncm.hpp"
|
||||
|
||||
#include "utils/thread.hpp"
|
||||
|
||||
#include <cstring>
|
||||
#include <atomic>
|
||||
#include <ranges>
|
||||
@@ -18,9 +20,6 @@
|
||||
namespace sphaira::title {
|
||||
namespace {
|
||||
|
||||
constexpr int THREAD_PRIO = PRIO_PREEMPTIVE;
|
||||
constexpr int THREAD_CORE = 1;
|
||||
|
||||
struct ThreadData {
|
||||
ThreadData(bool title_cache);
|
||||
|
||||
@@ -383,8 +382,7 @@ Result Init() {
|
||||
}
|
||||
|
||||
g_thread_data = std::make_unique<ThreadData>(true);
|
||||
R_TRY(threadCreate(&g_thread, ThreadFunc, g_thread_data.get(), nullptr, 1024*32, THREAD_PRIO, THREAD_CORE));
|
||||
svcSetThreadCoreMask(g_thread.handle, THREAD_CORE, THREAD_AFFINITY_DEFAULT(THREAD_CORE));
|
||||
R_TRY(utils::CreateThread(&g_thread, ThreadFunc, g_thread_data.get(), 1024*32));
|
||||
R_TRY(threadStart(&g_thread));
|
||||
}
|
||||
|
||||
|
||||
@@ -100,6 +100,7 @@ auto GetCodeMessage(Result rc) -> const char* {
|
||||
case Result_ThemezerFailedToDownloadTheme: return "SphairaError_ThemezerFailedToDownloadTheme";
|
||||
case Result_MainFailedToDownloadUpdate: return "SphairaError_MainFailedToDownloadUpdate";
|
||||
case Result_UsbDsBadDeviceSpeed: return "SphairaError_UsbDsBadDeviceSpeed";
|
||||
case Result_NcaBadMagic: return "SphairaError_NcaBadMagic";
|
||||
case Result_NspBadMagic: return "SphairaError_NspBadMagic";
|
||||
case Result_XciBadMagic: return "SphairaError_XciBadMagic";
|
||||
case Result_XciSecurePartitionNotFound: return "SphairaError_XciSecurePartitionNotFound";
|
||||
@@ -147,6 +148,16 @@ auto GetCodeMessage(Result rc) -> const char* {
|
||||
case Result_YatiCertNotFound: return "SphairaError_YatiCertNotFound";
|
||||
case Result_YatiNcmDbCorruptHeader: return "SphairaError_YatiNcmDbCorruptHeader";
|
||||
case Result_YatiNcmDbCorruptInfos: return "SphairaError_YatiNcmDbCorruptInfos";
|
||||
|
||||
case Result_NszFailedCreateCctx: return "SphairaError_NszFailedCreateCctx";
|
||||
case Result_NszFailedSetCompressionLevel: return "SphairaError_NszFailedSetCompressionLevel";
|
||||
case Result_NszFailedSetThreadCount: return "SphairaError_NszFailedSetThreadCount";
|
||||
case Result_NszFailedSetLongDistanceMode: return "SphairaError_NszFailedSetLongDistanceMode";
|
||||
case Result_NszFailedResetCctx: return "SphairaError_NszFailedResetCctx";
|
||||
case Result_NszFailedCompress2: return "SphairaError_NszFailedCompress2";
|
||||
case Result_NszFailedCompressStream2: return "SphairaError_NszFailedCompressStream2";
|
||||
case Result_NszTooManyBlocks: return "SphairaError_NszTooManyBlocks";
|
||||
case Result_NszMissingBlocks: return "SphairaError_NszMissingBlocks";
|
||||
}
|
||||
|
||||
return "";
|
||||
@@ -166,7 +177,7 @@ ErrorBox::ErrorBox(const std::string& message) : m_message{message} {
|
||||
SetPop();
|
||||
}});
|
||||
|
||||
App::PlaySoundEffect(SoundEffect::SoundEffect_Error);
|
||||
App::PlaySoundEffect(SoundEffect::Error);
|
||||
}
|
||||
|
||||
ErrorBox::ErrorBox(Result code, const std::string& message) : ErrorBox{message} {
|
||||
|
||||
@@ -72,7 +72,7 @@ auto List::ScrollDown(s64& index, s64 step, s64 count) -> bool {
|
||||
}
|
||||
|
||||
if (index != old_index) {
|
||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
||||
App::PlaySoundEffect(SoundEffect::Scroll);
|
||||
s64 delta = index - old_index;
|
||||
s64 start = m_yoff / max * m_row;
|
||||
|
||||
@@ -110,7 +110,7 @@ auto List::ScrollUp(s64& index, s64 step, s64 count) -> bool {
|
||||
}
|
||||
|
||||
if (index != old_index) {
|
||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
||||
App::PlaySoundEffect(SoundEffect::Scroll);
|
||||
s64 start = m_yoff / max * m_row;
|
||||
|
||||
while (index < start) {
|
||||
|
||||
@@ -577,13 +577,13 @@ EntryMenu::EntryMenu(Entry& entry, const LazyImage& default_icon, Menu& menu)
|
||||
std::make_pair(Button::DPAD_DOWN | Button::RS_DOWN, Action{[this](){
|
||||
if (m_index < (m_options.size() - 1)) {
|
||||
SetIndex(m_index + 1);
|
||||
App::PlaySoundEffect(SoundEffect_Focus);
|
||||
App::PlaySoundEffect(SoundEffect::Focus);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::DPAD_UP | Button::RS_UP, Action{[this](){
|
||||
if (m_index != 0) {
|
||||
SetIndex(m_index - 1);
|
||||
App::PlaySoundEffect(SoundEffect_Focus);
|
||||
App::PlaySoundEffect(SoundEffect::Focus);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::X, Action{"Options"_i18n, [this](){
|
||||
@@ -1007,7 +1007,7 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
if (touch && m_index == i) {
|
||||
FireAction(Button::A);
|
||||
} else {
|
||||
App::PlaySoundEffect(SoundEffect_Focus);
|
||||
App::PlaySoundEffect(SoundEffect::Focus);
|
||||
SetIndex(i);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -6,13 +6,16 @@ namespace {
|
||||
|
||||
} // namespace
|
||||
|
||||
Menu::Menu(const fs::FsPath& path) : MenuBase{path, MenuFlag_None}, m_path{path} {
|
||||
Menu::Menu(fs::Fs* fs, const fs::FsPath& path)
|
||||
: MenuBase{path, MenuFlag_None}
|
||||
, m_fs{fs}
|
||||
, m_path{path} {
|
||||
SetAction(Button::B, Action{"Back"_i18n, [this](){
|
||||
SetPop();
|
||||
}});
|
||||
|
||||
std::string buf;
|
||||
if (R_SUCCEEDED(m_fs.OpenFile(m_path, FsOpenMode_Read, &m_file))) {
|
||||
if (R_SUCCEEDED(m_fs->OpenFile(m_path, FsOpenMode_Read, &m_file))) {
|
||||
m_file.GetSize(&m_file_size);
|
||||
buf.resize(m_file_size + 1);
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
#include "ui/menus/filebrowser.hpp"
|
||||
#include "ui/menus/homebrew.hpp"
|
||||
#include "ui/menus/file_viewer.hpp"
|
||||
#include "ui/menus/image_viewer.hpp"
|
||||
|
||||
#include "ui/sidebar.hpp"
|
||||
#include "ui/option_box.hpp"
|
||||
#include "ui/popup_list.hpp"
|
||||
#include "ui/progress_box.hpp"
|
||||
#include "ui/error_box.hpp"
|
||||
#include "ui/menus/file_viewer.hpp"
|
||||
#include "ui/music_player.hpp"
|
||||
|
||||
#include "utils/devoptab.hpp"
|
||||
|
||||
@@ -28,6 +31,7 @@
|
||||
#include "yati/yati.hpp"
|
||||
#include "yati/source/file.hpp"
|
||||
|
||||
#include <usbdvd.h>
|
||||
#include <minIni.h>
|
||||
#include <minizip/zip.h>
|
||||
#include <minizip/unzip.h>
|
||||
@@ -80,10 +84,10 @@ constexpr FsEntry FS_ENTRIES[]{
|
||||
|
||||
constexpr std::string_view AUDIO_EXTENSIONS[] = {
|
||||
"mp3", "ogg", "flac", "wav", "aac" "ac3", "aif", "asf", "bfwav",
|
||||
"bfsar", "bfstm",
|
||||
"bfsar", "bfstm", "bwav",
|
||||
};
|
||||
constexpr std::string_view VIDEO_EXTENSIONS[] = {
|
||||
"mp4", "mkv", "m3u", "m3u8", "hls", "vob", "avi", "dv", "flv", "m2ts",
|
||||
"mp4", "mkv", "m3u", "m3u8", "hls", "vob", "avi", "dv", "flv", "m2ts", "webm",
|
||||
"m2v", "m4a", "mov", "mpeg", "mpg", "mts", "swf", "ts", "vob", "wma", "wmv",
|
||||
};
|
||||
constexpr std::string_view IMAGE_EXTENSIONS[] = {
|
||||
@@ -98,6 +102,9 @@ constexpr std::string_view NSP_EXTENSIONS[] = {
|
||||
constexpr std::string_view XCI_EXTENSIONS[] = {
|
||||
"xci", "xcz",
|
||||
};
|
||||
constexpr std::string_view NCA_EXTENSIONS[] = {
|
||||
"nca", "ncz",
|
||||
};
|
||||
// these are files that are already compressed or encrypted and should
|
||||
// be stored raw in a zip file.
|
||||
constexpr std::string_view COMPRESSED_EXTENSIONS[] = {
|
||||
@@ -106,6 +113,16 @@ constexpr std::string_view COMPRESSED_EXTENSIONS[] = {
|
||||
constexpr std::string_view ZIP_EXTENSIONS[] = {
|
||||
"zip",
|
||||
};
|
||||
// supported music playback extensions.
|
||||
constexpr std::string_view MUSIC_EXTENSIONS[] = {
|
||||
"bfstm", "bfwav", "wav", "mp3", "ogg", "adf",
|
||||
};
|
||||
// supported theme music playback extensions.
|
||||
constexpr std::span THEME_MUSIC_EXTENSIONS = MUSIC_EXTENSIONS;
|
||||
|
||||
constexpr std::string_view CDDVD_EXTENSIONS[] = {
|
||||
"iso", "cue",
|
||||
};
|
||||
|
||||
struct RomDatabaseEntry {
|
||||
// uses the naming scheme from retropie.
|
||||
@@ -473,8 +490,10 @@ FsView::FsView(Base* menu, const std::shared_ptr<fs::Fs>& fs, const fs::FsPath&
|
||||
}})
|
||||
);
|
||||
|
||||
log_write("setting side\n");
|
||||
SetSide(m_side);
|
||||
|
||||
log_write("getting path\n");
|
||||
auto buf = path;
|
||||
if (path.empty() && entry.IsSd()) {
|
||||
ini_gets("paths", "last_path", entry.root, buf, sizeof(buf), App::CONFIG_PATH);
|
||||
@@ -485,7 +504,9 @@ FsView::FsView(Base* menu, const std::shared_ptr<fs::Fs>& fs, const fs::FsPath&
|
||||
buf = entry.root;
|
||||
}
|
||||
|
||||
log_write("setting fs\n");
|
||||
SetFs(fs, buf, entry);
|
||||
log_write("set fs\n");
|
||||
}
|
||||
|
||||
FsView::FsView(FsView* view, ViewSide side) : FsView{view->m_menu, view->m_fs, view->m_path, view->m_fs_entry, side} {
|
||||
@@ -509,7 +530,7 @@ void FsView::Update(Controller* controller, TouchInfo* touch) {
|
||||
if (touch && m_index == i) {
|
||||
FireAction(Button::A);
|
||||
} else {
|
||||
App::PlaySoundEffect(SoundEffect_Focus);
|
||||
App::PlaySoundEffect(SoundEffect::Focus);
|
||||
SetIndex(i);
|
||||
}
|
||||
});
|
||||
@@ -574,8 +595,9 @@ void FsView::Draw(NVGcontext* vg, Theme* theme) {
|
||||
if (e.IsDir()) {
|
||||
// NOTE: this takes longer than 16ms when opening a new folder due to it
|
||||
// checking all 9 folders at once.
|
||||
if (!got_dir_count && e.file_count == -1 && e.dir_count == -1) {
|
||||
if (!got_dir_count && !e.done_stat && e.file_count == -1 && e.dir_count == -1) {
|
||||
got_dir_count = true;
|
||||
e.done_stat = true;
|
||||
m_fs->DirGetEntryCount(GetNewPath(e), &e.file_count, &e.dir_count);
|
||||
}
|
||||
|
||||
@@ -586,7 +608,8 @@ void FsView::Draw(NVGcontext* vg, Theme* theme) {
|
||||
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) + 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_TOP, theme->GetColour(text_id), "%zd dirs"_i18n.c_str(), e.dir_count);
|
||||
}
|
||||
} else if (e.IsFile()) {
|
||||
if (!e.time_stamp.is_valid) {
|
||||
if (!e.time_stamp.is_valid && !e.done_stat) {
|
||||
e.done_stat = true;
|
||||
const auto path = GetNewPath(e);
|
||||
if (m_fs->IsNative()) {
|
||||
m_fs->GetFileTimeStampRaw(path, &e.time_stamp);
|
||||
@@ -686,12 +709,44 @@ void FsView::OnClick() {
|
||||
nro_launch(GetNewPathCurrent());
|
||||
}
|
||||
});
|
||||
} else if (IsExtension(entry.GetExtension(), NCA_EXTENSIONS)) {
|
||||
MountFileFs(devoptab::MountNca, devoptab::UmountNca);
|
||||
} else if (IsExtension(entry.GetExtension(), NSP_EXTENSIONS)) {
|
||||
MountNspFs();
|
||||
MountFileFs(devoptab::MountNsp, devoptab::UmountNsp);
|
||||
} else if (IsExtension(entry.GetExtension(), XCI_EXTENSIONS)) {
|
||||
MountXciFs();
|
||||
MountFileFs(devoptab::MountXci, devoptab::UmountXci);
|
||||
} else if (IsExtension(entry.GetExtension(), "zip")) {
|
||||
MountZipFs();
|
||||
MountFileFs(devoptab::MountZip, devoptab::UmountZip);
|
||||
} else if (IsExtension(entry.GetExtension(), "bfsar")) {
|
||||
MountFileFs(devoptab::MountBfsar, devoptab::UmountBfsar);
|
||||
} else if (IsExtension(entry.GetExtension(), MUSIC_EXTENSIONS)) {
|
||||
App::Push<music::Menu>(GetFs(), GetNewPathCurrent());
|
||||
} else if (IsExtension(entry.GetExtension(), IMAGE_EXTENSIONS)) {
|
||||
App::Push<imageview::Menu>(GetFs(), GetNewPathCurrent());
|
||||
} else if (IsExtension(entry.GetExtension(), CDDVD_EXTENSIONS)) {
|
||||
std::shared_ptr<CUSBDVD> usbdvd;
|
||||
|
||||
if (entry.GetExtension() == "cue") {
|
||||
const auto cue_path = GetNewPathCurrent();
|
||||
fs::FsPath bin_path = cue_path;
|
||||
std::strcpy(std::strstr(bin_path, ".cue"), ".bin");
|
||||
if (m_fs->FileExists(bin_path)) {
|
||||
usbdvd = std::make_shared<CUSBDVD>(cue_path, bin_path);
|
||||
}
|
||||
} else {
|
||||
usbdvd = std::make_shared<CUSBDVD>(GetNewPathCurrent());
|
||||
}
|
||||
|
||||
if (usbdvd && usbdvd->usbdvd_drive_ctx.fs.mounted) {
|
||||
auto fs = std::make_shared<FsStdioWrapper>(usbdvd->usbdvd_drive_ctx.fs.mountpoint, [usbdvd](){
|
||||
// dummy func to keep shared_ptr alive until fs is closed.
|
||||
});
|
||||
|
||||
MountFsHelper(fs, usbdvd->usbdvd_drive_ctx.fs.disc_fstype);
|
||||
log_write("[USBDVD] mounted\n");
|
||||
} else {
|
||||
log_write("[USBDVD] failed to mount\n");
|
||||
}
|
||||
} else if (IsExtension(entry.GetExtension(), INSTALL_EXTENSIONS)) {
|
||||
InstallFiles();
|
||||
} else if (IsSd()) {
|
||||
@@ -1926,9 +1981,9 @@ void FsView::DisplayAdvancedOptions() {
|
||||
});
|
||||
}
|
||||
|
||||
if (IsSd() && m_entries_current.size() && !m_selected_count && GetEntry().IsFile() && GetEntry().file_size < 1024*64) {
|
||||
if (m_entries_current.size() && !m_selected_count && GetEntry().IsFile() && GetEntry().file_size < 1024*64) {
|
||||
options->Add<SidebarEntryCallback>("View as text (unfinished)"_i18n, [this](){
|
||||
App::Push<fileview::Menu>(GetNewPathCurrent());
|
||||
App::Push<fileview::Menu>(GetFs(), GetNewPathCurrent());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1938,6 +1993,13 @@ void FsView::DisplayAdvancedOptions() {
|
||||
});
|
||||
}
|
||||
|
||||
if (m_entries_current.size() && !m_selected_count && IsExtension(GetEntry().GetExtension(), THEME_MUSIC_EXTENSIONS)) {
|
||||
options->Add<SidebarEntryCallback>("Set as background music"_i18n, [this](){
|
||||
const auto rc = App::SetDefaultBackgroundMusic(GetFs(), GetNewPathCurrent());
|
||||
App::PushErrorBox(rc, "Failed to set default music path"_i18n);
|
||||
});
|
||||
}
|
||||
|
||||
if (m_entries_current.size() && !m_selected_count && GetEntry().IsFile()) {
|
||||
options->Add<SidebarEntryCallback>("Hash"_i18n, [this](){
|
||||
auto options = std::make_unique<Sidebar>("Hash Options"_i18n, Sidebar::Side::RIGHT);
|
||||
@@ -1955,6 +2017,15 @@ void FsView::DisplayAdvancedOptions() {
|
||||
options->Add<SidebarEntryCallback>("SHA256"_i18n, [this](){
|
||||
DisplayHash(hash::Type::Sha256);
|
||||
});
|
||||
options->Add<SidebarEntryCallback>("/dev/null (Speed Test)"_i18n, [this](){
|
||||
DisplayHash(hash::Type::Null);
|
||||
});
|
||||
options->Add<SidebarEntryCallback>("Deflate (Speed Test)"_i18n, [this](){
|
||||
DisplayHash(hash::Type::Deflate);
|
||||
});
|
||||
options->Add<SidebarEntryCallback>("ZSTD (Speed Test)"_i18n, [this](){
|
||||
DisplayHash(hash::Type::Zstd);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1964,42 +2035,14 @@ void FsView::DisplayAdvancedOptions() {
|
||||
});
|
||||
}
|
||||
|
||||
void FsView::MountNspFs() {
|
||||
void FsView::MountFileFs(const MountFsFunc& mount_func, const UmountFsFunc& umount_func) {
|
||||
fs::FsPath mount;
|
||||
const auto rc = devoptab::MountNsp(GetFs(), GetNewPathCurrent(), mount);
|
||||
App::PushErrorBox(rc, "Failed to mount NSP."_i18n);
|
||||
const auto rc = mount_func(GetFs(), GetNewPathCurrent(), mount);
|
||||
App::PushErrorBox(rc, "Failed to mount FS."_i18n);
|
||||
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
auto fs = std::make_shared<FsStdioWrapper>(mount, [mount](){
|
||||
devoptab::UmountNsp(mount);
|
||||
});
|
||||
|
||||
MountFsHelper(fs, GetEntry().GetName());
|
||||
}
|
||||
}
|
||||
|
||||
void FsView::MountXciFs() {
|
||||
fs::FsPath mount;
|
||||
const auto rc = devoptab::MountXci(GetFs(), GetNewPathCurrent(), mount);
|
||||
App::PushErrorBox(rc, "Failed to mount XCI."_i18n);
|
||||
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
auto fs = std::make_shared<FsStdioWrapper>(mount, [mount](){
|
||||
devoptab::UmountXci(mount);
|
||||
});
|
||||
|
||||
MountFsHelper(fs, GetEntry().GetName());
|
||||
}
|
||||
}
|
||||
|
||||
void FsView::MountZipFs() {
|
||||
fs::FsPath mount;
|
||||
const auto rc = devoptab::MountZip(GetFs(), GetNewPathCurrent(), mount);
|
||||
App::PushErrorBox(rc, "Failed to mount zip."_i18n);
|
||||
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
auto fs = std::make_shared<FsStdioWrapper>(mount, [mount](){
|
||||
devoptab::UmountZip(mount);
|
||||
auto fs = std::make_shared<FsStdioWrapper>(mount, [mount, umount_func](){
|
||||
umount_func(mount);
|
||||
});
|
||||
|
||||
MountFsHelper(fs, GetEntry().GetName());
|
||||
@@ -2007,32 +2050,15 @@ void FsView::MountZipFs() {
|
||||
}
|
||||
|
||||
Base::Base(u32 flags, u32 options)
|
||||
: Base{CreateFs(FS_ENTRY_DEFAULT), FS_ENTRY_DEFAULT, {}, false, flags, options} {
|
||||
: MenuBase{"FileBrowser"_i18n, flags}
|
||||
, m_options{options} {
|
||||
Init(CreateFs(FS_ENTRY_DEFAULT), FS_ENTRY_DEFAULT, {}, false);
|
||||
}
|
||||
|
||||
Base::Base(const std::shared_ptr<fs::Fs>& fs, const FsEntry& fs_entry, const fs::FsPath& path, bool is_custom, u32 flags, u32 options)
|
||||
: MenuBase{"FileBrowser"_i18n, flags}
|
||||
, m_options{options} {
|
||||
if (m_options & FsOption_CanSplit) {
|
||||
SetAction(Button::L3, Action{"Split"_i18n, [this](){
|
||||
SetSplitScreen(IsSplitScreen() ^ 1);
|
||||
}});
|
||||
}
|
||||
|
||||
if (!IsTab()) {
|
||||
SetAction(Button::SELECT, Action{"Close"_i18n, [this](){
|
||||
PromptIfShouldExit();
|
||||
}});
|
||||
}
|
||||
|
||||
if (is_custom) {
|
||||
m_custom_fs = fs;
|
||||
m_custom_fs_entry = fs_entry;
|
||||
}
|
||||
|
||||
view_left = std::make_unique<FsView>(this, fs, path, fs_entry, ViewSide::Left);
|
||||
view = view_left.get();
|
||||
ueventCreate(&g_change_uevent, true);
|
||||
Init(fs, fs_entry, path, is_custom);
|
||||
}
|
||||
|
||||
void Base::Update(Controller* controller, TouchInfo* touch) {
|
||||
@@ -2355,6 +2381,31 @@ auto Base::CreateFs(const FsEntry& fs_entry) -> std::shared_ptr<fs::Fs> {
|
||||
std::unreachable();
|
||||
}
|
||||
|
||||
void Base::Init(const std::shared_ptr<fs::Fs>& fs, const FsEntry& fs_entry, const fs::FsPath& path, bool is_custom) {
|
||||
if (m_options & FsOption_CanSplit) {
|
||||
SetAction(Button::L3, Action{"Split"_i18n, [this](){
|
||||
SetSplitScreen(IsSplitScreen() ^ 1);
|
||||
}});
|
||||
}
|
||||
|
||||
if (!IsTab()) {
|
||||
SetAction(Button::SELECT, Action{"Close"_i18n, [this](){
|
||||
PromptIfShouldExit();
|
||||
}});
|
||||
}
|
||||
|
||||
if (is_custom) {
|
||||
m_custom_fs = fs;
|
||||
m_custom_fs_entry = fs_entry;
|
||||
}
|
||||
|
||||
log_write("creating view\n");
|
||||
|
||||
view_left = std::make_unique<FsView>(this, fs, path, fs_entry, ViewSide::Left);
|
||||
view = view_left.get();
|
||||
ueventCreate(&g_change_uevent, true);
|
||||
}
|
||||
|
||||
void MountFsHelper(const std::shared_ptr<fs::Fs>& fs, const fs::FsPath& name) {
|
||||
const filebrowser::FsEntry fs_entry{
|
||||
.name = name,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#if ENABLE_NETWORK_INSTALL
|
||||
|
||||
#include "ui/menus/ftp_menu.hpp"
|
||||
#include "app.hpp"
|
||||
#include "defines.hpp"
|
||||
@@ -106,5 +104,3 @@ void Menu::OnDisableInstallMode() {
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui::menu::ftp
|
||||
|
||||
#endif
|
||||
|
||||
@@ -7,9 +7,13 @@
|
||||
#include "image.hpp"
|
||||
#include "swkbd.hpp"
|
||||
|
||||
#include "utils/utils.hpp"
|
||||
#include "utils/nsz_dumper.hpp"
|
||||
|
||||
#include "ui/menus/game_menu.hpp"
|
||||
#include "ui/menus/game_meta_menu.hpp"
|
||||
#include "ui/menus/save_menu.hpp"
|
||||
#include "ui/menus/gc_menu.hpp" // remove when gc event pr is merged.
|
||||
#include "ui/sidebar.hpp"
|
||||
#include "ui/error_box.hpp"
|
||||
#include "ui/option_box.hpp"
|
||||
@@ -50,6 +54,11 @@ struct NspSource final : dump::BaseSource {
|
||||
return rc;
|
||||
}
|
||||
|
||||
Result Read(const std::string& path, void* buf, s64 off, s64 size) {
|
||||
u64 bytes_read = 0;
|
||||
return Read(path, buf, off, size, &bytes_read);
|
||||
}
|
||||
|
||||
auto GetName(const std::string& path) const -> std::string {
|
||||
const auto it = std::ranges::find_if(m_entries, [&path](auto& e){
|
||||
return path.find(e.path.s) != path.npos;
|
||||
@@ -86,11 +95,57 @@ struct NspSource final : dump::BaseSource {
|
||||
return App::GetDefaultImage();
|
||||
}
|
||||
|
||||
Result GetEntryFromPath(const std::string& path, NspEntry& out) const {
|
||||
const auto it = std::ranges::find_if(m_entries, [&path](auto& e){
|
||||
return path.find(e.path.s) != path.npos;
|
||||
});
|
||||
R_UNLESS(it != m_entries.end(), Result_GameBadReadForDump);
|
||||
|
||||
out = *it;
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<NspEntry> m_entries{};
|
||||
bool m_is_file_based_emummc{};
|
||||
};
|
||||
|
||||
Result NszExport(ProgressBox* pbox, const keys::Keys& keys, dump::BaseSource* _source, dump::WriteSource* writer, const fs::FsPath& path) {
|
||||
auto source = (NspSource*)_source;
|
||||
|
||||
NspEntry entry;
|
||||
R_TRY(source->GetEntryFromPath(path, entry));
|
||||
|
||||
const auto nca_creator = [&entry](const nca::Header& header, const keys::KeyEntry& title_key, const utils::nsz::Collection& collection) {
|
||||
const auto content_id = ncm::GetContentIdFromStr(collection.name.c_str());
|
||||
return std::make_unique<nca::NcaReader>(
|
||||
header, &title_key, collection.size,
|
||||
std::make_shared<ncm::NcmSource>(&entry.cs, &content_id)
|
||||
);
|
||||
};
|
||||
|
||||
auto& collections = entry.collections;
|
||||
s64 read_offset = entry.nsp_data.size();
|
||||
s64 write_offset = entry.nsp_data.size();
|
||||
|
||||
R_TRY(utils::nsz::NszExport(pbox, nca_creator, read_offset, write_offset, collections, keys, source, writer, path));
|
||||
|
||||
// zero base the offsets.
|
||||
for (auto& collection : collections) {
|
||||
collection.offset -= entry.nsp_data.size();
|
||||
}
|
||||
|
||||
// build new nsp collection with the updated offsets and sizes.
|
||||
s64 nsp_size = 0;
|
||||
const auto nsp_data = yati::container::Nsp::Build(collections, nsp_size);
|
||||
R_TRY(writer->Write(nsp_data.data(), 0, nsp_data.size()));
|
||||
|
||||
// update with actual size.
|
||||
R_TRY(writer->SetSize(nsp_size));
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Notify(Result rc, const std::string& error_message) {
|
||||
if (R_FAILED(rc)) {
|
||||
App::Push<ui::ErrorBox>(rc,
|
||||
@@ -135,18 +190,6 @@ void LoadControlEntry(Entry& e, bool force_image_load = false) {
|
||||
}
|
||||
}
|
||||
|
||||
struct HashStr {
|
||||
char str[0x21];
|
||||
};
|
||||
|
||||
HashStr hexIdToStr(auto id) {
|
||||
HashStr str{};
|
||||
const auto id_lower = std::byteswap(*(u64*)id.c);
|
||||
const auto id_upper = std::byteswap(*(u64*)(id.c + 0x8));
|
||||
std::snprintf(str.str, 0x21, "%016lx%016lx", id_lower, id_upper);
|
||||
return str;
|
||||
}
|
||||
|
||||
void FreeEntry(NVGcontext* vg, Entry& e) {
|
||||
nvgDeleteImage(vg, e.image);
|
||||
e.image = 0;
|
||||
@@ -324,26 +367,15 @@ Menu::Menu(u32 flags) : grid::Menu{"Games"_i18n, flags} {
|
||||
);
|
||||
});
|
||||
|
||||
options->Add<SidebarEntryCallback>("Export NSP"_i18n, [this](){
|
||||
auto options = std::make_unique<Sidebar>("Select content to export"_i18n, Sidebar::Side::RIGHT);
|
||||
ON_SCOPE_EXIT(App::Push(std::move(options)));
|
||||
auto export_nsp = options->Add<SidebarEntryCallback>("Export NSP"_i18n, [this](){
|
||||
ExportOptions(false);
|
||||
});
|
||||
export_nsp->Depends(App::IsApplication, "Not supported in Applet Mode"_i18n);
|
||||
|
||||
options->Add<SidebarEntryCallback>("Export All"_i18n, [this](){
|
||||
DumpGames(title::ContentFlag_All);
|
||||
}, true);
|
||||
options->Add<SidebarEntryCallback>("Export Application"_i18n, [this](){
|
||||
DumpGames(title::ContentFlag_Application);
|
||||
}, true);
|
||||
options->Add<SidebarEntryCallback>("Export Patch"_i18n, [this](){
|
||||
DumpGames(title::ContentFlag_Patch);
|
||||
}, true);
|
||||
options->Add<SidebarEntryCallback>("Export AddOnContent"_i18n, [this](){
|
||||
DumpGames(title::ContentFlag_AddOnContent);
|
||||
}, true);
|
||||
options->Add<SidebarEntryCallback>("Export DataPatch"_i18n, [this](){
|
||||
DumpGames(title::ContentFlag_DataPatch);
|
||||
}, true);
|
||||
}, true);
|
||||
auto export_nsz = options->Add<SidebarEntryCallback>("Export NSZ"_i18n, [this](){
|
||||
ExportOptions(true);
|
||||
}, "Exports to NSZ (compressed NSP)"_i18n);
|
||||
export_nsz->Depends(App::IsApplication, "Not supported in Applet Mode"_i18n);
|
||||
|
||||
options->Add<SidebarEntryCallback>("Export options"_i18n, [this](){
|
||||
App::DisplayDumpOptions(false);
|
||||
@@ -418,6 +450,9 @@ Menu::Menu(u32 flags) : grid::Menu{"Games"_i18n, flags} {
|
||||
ns::Initialize();
|
||||
es::Initialize();
|
||||
title::Init();
|
||||
|
||||
fsOpenGameCardDetectionEventNotifier(std::addressof(m_gc_event_notifier));
|
||||
fsEventNotifierGetEventHandle(std::addressof(m_gc_event_notifier), std::addressof(m_gc_event), true);
|
||||
}
|
||||
|
||||
Menu::~Menu() {
|
||||
@@ -426,9 +461,15 @@ Menu::~Menu() {
|
||||
FreeEntries();
|
||||
ns::Exit();
|
||||
es::Exit();
|
||||
|
||||
eventClose(std::addressof(m_gc_event));
|
||||
fsEventNotifierClose(std::addressof(m_gc_event_notifier));
|
||||
}
|
||||
|
||||
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
// force update if gamecard state changed.
|
||||
m_dirty |= R_SUCCEEDED(eventWait(&m_gc_event, 0));
|
||||
|
||||
if (m_dirty) {
|
||||
App::Notify("Updating application record list"_i18n);
|
||||
SortAndFindLastFile(true);
|
||||
@@ -439,7 +480,7 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
if (touch && m_index == i) {
|
||||
FireAction(Button::A);
|
||||
} else {
|
||||
App::PlaySoundEffect(SoundEffect_Focus);
|
||||
App::PlaySoundEffect(SoundEffect::Focus);
|
||||
SetIndex(i);
|
||||
}
|
||||
});
|
||||
@@ -538,7 +579,7 @@ void Menu::ScanHomebrew() {
|
||||
continue;
|
||||
}
|
||||
|
||||
m_entries.emplace_back(e.application_id, e.type);
|
||||
m_entries.emplace_back(e.application_id, e.last_event);
|
||||
}
|
||||
|
||||
offset += record_count;
|
||||
@@ -640,16 +681,37 @@ void Menu::DeleteGames() {
|
||||
});
|
||||
}
|
||||
|
||||
void Menu::DumpGames(u32 flags) {
|
||||
void Menu::ExportOptions(bool to_nsz) {
|
||||
auto options = std::make_unique<Sidebar>("Select content to export"_i18n, Sidebar::Side::RIGHT);
|
||||
ON_SCOPE_EXIT(App::Push(std::move(options)));
|
||||
|
||||
options->Add<SidebarEntryCallback>("Export All"_i18n, [this, to_nsz](){
|
||||
DumpGames(title::ContentFlag_All, to_nsz);
|
||||
}, true);
|
||||
options->Add<SidebarEntryCallback>("Export Application"_i18n, [this, to_nsz](){
|
||||
DumpGames(title::ContentFlag_Application, to_nsz);
|
||||
}, true);
|
||||
options->Add<SidebarEntryCallback>("Export Patch"_i18n, [this, to_nsz](){
|
||||
DumpGames(title::ContentFlag_Patch, to_nsz);
|
||||
}, true);
|
||||
options->Add<SidebarEntryCallback>("Export AddOnContent"_i18n, [this, to_nsz](){
|
||||
DumpGames(title::ContentFlag_AddOnContent, to_nsz);
|
||||
}, true);
|
||||
options->Add<SidebarEntryCallback>("Export DataPatch"_i18n, [this, to_nsz](){
|
||||
DumpGames(title::ContentFlag_DataPatch, to_nsz);
|
||||
}, true);
|
||||
}
|
||||
|
||||
void Menu::DumpGames(u32 flags, bool to_nsz) {
|
||||
auto targets = GetSelectedEntries();
|
||||
ClearSelection();
|
||||
|
||||
std::vector<NspEntry> nsp_entries;
|
||||
for (auto& e : targets) {
|
||||
BuildNspEntries(e, flags, nsp_entries);
|
||||
BuildNspEntries(e, flags, nsp_entries, to_nsz);
|
||||
}
|
||||
|
||||
DumpNsp(nsp_entries);
|
||||
DumpNsp(nsp_entries, to_nsz);
|
||||
}
|
||||
|
||||
void Menu::CreateSaves(AccountUid uid) {
|
||||
@@ -762,7 +824,7 @@ void DeleteMetaEntries(u64 app_id, int image, const std::string& name, const tit
|
||||
});
|
||||
}
|
||||
|
||||
auto BuildNspPath(const Entry& e, const NsApplicationContentMetaStatus& status) -> fs::FsPath {
|
||||
auto BuildNspPath(const Entry& e, const NsApplicationContentMetaStatus& status, bool to_nsz) -> fs::FsPath {
|
||||
fs::FsPath name_buf = e.GetName();
|
||||
title::utilsReplaceIllegalCharacters(name_buf, true);
|
||||
|
||||
@@ -778,17 +840,19 @@ auto BuildNspPath(const Entry& e, const NsApplicationContentMetaStatus& status)
|
||||
}
|
||||
}
|
||||
|
||||
const auto ext = to_nsz ? "nsz" : "nsp";
|
||||
|
||||
fs::FsPath path;
|
||||
if (App::GetApp()->m_dump_app_folder.Get()) {
|
||||
std::snprintf(path, sizeof(path), "%s/%s %s[%016lX][v%u][%s].nsp", name_buf.s, name_buf.s, version, status.application_id, status.version, ncm::GetMetaTypeShortStr(status.meta_type));
|
||||
std::snprintf(path, sizeof(path), "%s/%s %s[%016lX][v%u][%s].%s", name_buf.s, name_buf.s, version, status.application_id, status.version, ncm::GetMetaTypeShortStr(status.meta_type), ext);
|
||||
} else {
|
||||
std::snprintf(path, sizeof(path), "%s %s[%016lX][v%u][%s].nsp", name_buf.s, version, status.application_id, status.version, ncm::GetMetaTypeShortStr(status.meta_type));
|
||||
std::snprintf(path, sizeof(path), "%s %s[%016lX][v%u][%s].%s", name_buf.s, version, status.application_id, status.version, ncm::GetMetaTypeShortStr(status.meta_type), ext);
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
Result BuildContentEntry(const NsApplicationContentMetaStatus& status, ContentInfoEntry& out) {
|
||||
Result BuildContentEntry(const NsApplicationContentMetaStatus& status, ContentInfoEntry& out, bool to_nsz) {
|
||||
NcmMetaData meta;
|
||||
R_TRY(GetNcmMetaFromMetaStatus(status, meta));
|
||||
|
||||
@@ -824,14 +888,14 @@ Result BuildContentEntry(const NsApplicationContentMetaStatus& status, ContentIn
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result BuildNspEntry(const Entry& e, const ContentInfoEntry& info, const keys::Keys& keys, NspEntry& out) {
|
||||
Result BuildNspEntry(const Entry& e, const ContentInfoEntry& info, const keys::Keys& keys, NspEntry& out, bool to_nsz) {
|
||||
out.application_name = e.GetName();
|
||||
out.path = BuildNspPath(e, info.status);
|
||||
out.path = BuildNspPath(e, info.status, to_nsz);
|
||||
s64 offset{};
|
||||
|
||||
for (auto& e : info.content_infos) {
|
||||
char nca_name[64];
|
||||
std::snprintf(nca_name, sizeof(nca_name), "%s%s", hexIdToStr(e.content_id).str, e.content_type == NcmContentType_Meta ? ".cnmt.nca" : ".nca");
|
||||
std::snprintf(nca_name, sizeof(nca_name), "%s%s", utils::hexIdToStr(e.content_id).str, e.content_type == NcmContentType_Meta ? ".cnmt.nca" : ".nca");
|
||||
|
||||
u64 size;
|
||||
ncmContentInfoSizeToU64(std::addressof(e), std::addressof(size));
|
||||
@@ -856,10 +920,10 @@ Result BuildNspEntry(const Entry& e, const ContentInfoEntry& info, const keys::K
|
||||
R_TRY(es::PatchTicket(entry.tik_data, entry.cert_data, key_gen, keys, App::GetApp()->m_dump_convert_to_common_ticket.Get()));
|
||||
|
||||
char tik_name[64];
|
||||
std::snprintf(tik_name, sizeof(tik_name), "%s%s", hexIdToStr(rights_id).str, ".tik");
|
||||
std::snprintf(tik_name, sizeof(tik_name), "%s%s", utils::hexIdToStr(rights_id).str, ".tik");
|
||||
|
||||
char cert_name[64];
|
||||
std::snprintf(cert_name, sizeof(cert_name), "%s%s", hexIdToStr(rights_id).str, ".cert");
|
||||
std::snprintf(cert_name, sizeof(cert_name), "%s%s", utils::hexIdToStr(rights_id).str, ".cert");
|
||||
|
||||
out.collections.emplace_back(tik_name, offset, entry.tik_data.size());
|
||||
offset += entry.tik_data.size();
|
||||
@@ -876,7 +940,7 @@ Result BuildNspEntry(const Entry& e, const ContentInfoEntry& info, const keys::K
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result BuildNspEntries(Entry& e, const title::MetaEntries& meta_entries, std::vector<NspEntry>& out) {
|
||||
Result BuildNspEntries(Entry& e, const title::MetaEntries& meta_entries, std::vector<NspEntry>& out, bool to_nsz) {
|
||||
LoadControlEntry(e);
|
||||
|
||||
keys::Keys keys;
|
||||
@@ -887,7 +951,7 @@ Result BuildNspEntries(Entry& e, const title::MetaEntries& meta_entries, std::ve
|
||||
R_TRY(BuildContentEntry(status, info));
|
||||
|
||||
NspEntry nsp;
|
||||
R_TRY(BuildNspEntry(e, info, keys, nsp));
|
||||
R_TRY(BuildNspEntry(e, info, keys, nsp, to_nsz));
|
||||
out.emplace_back(nsp).icon = e.image;
|
||||
}
|
||||
|
||||
@@ -895,21 +959,36 @@ Result BuildNspEntries(Entry& e, const title::MetaEntries& meta_entries, std::ve
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result BuildNspEntries(Entry& e, u32 flags, std::vector<NspEntry>& out) {
|
||||
Result BuildNspEntries(Entry& e, u32 flags, std::vector<NspEntry>& out, bool to_nsz) {
|
||||
title::MetaEntries meta_entries;
|
||||
R_TRY(GetMetaEntries(e, meta_entries, flags));
|
||||
|
||||
return BuildNspEntries(e, meta_entries, out);
|
||||
return BuildNspEntries(e, meta_entries, out, to_nsz);
|
||||
}
|
||||
|
||||
void DumpNsp(const std::vector<NspEntry>& entries) {
|
||||
void DumpNsp(const std::vector<NspEntry>& entries, bool to_nsz) {
|
||||
std::vector<fs::FsPath> paths;
|
||||
for (auto& e : entries) {
|
||||
paths.emplace_back(fs::AppendPath("/dumps/NSP", e.path));
|
||||
if (to_nsz) {
|
||||
paths.emplace_back(fs::AppendPath("/dumps/NSZ", e.path));
|
||||
} else {
|
||||
paths.emplace_back(fs::AppendPath("/dumps/NSP", e.path));
|
||||
}
|
||||
}
|
||||
|
||||
auto source = std::make_shared<NspSource>(entries);
|
||||
dump::Dump(source, paths);
|
||||
|
||||
if (to_nsz) {
|
||||
// todo: log keys error.
|
||||
keys::Keys keys;
|
||||
keys::parse_keys(keys, true);
|
||||
|
||||
dump::Dump(source, paths, [keys](ProgressBox* pbox, dump::BaseSource* source, dump::WriteSource* writer, const fs::FsPath& path) {
|
||||
return NszExport(pbox, keys, source, writer, path);
|
||||
});
|
||||
} else {
|
||||
dump::Dump(source, paths);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui::menu::game
|
||||
|
||||
@@ -86,7 +86,11 @@ Menu::Menu(Entry& entry) : MenuBase{entry.GetName(), MenuFlag_None}, m_entry{ent
|
||||
|
||||
if (!m_entries.empty()) {
|
||||
options->Add<SidebarEntryCallback>("Export NSP"_i18n, [this](){
|
||||
DumpGames();
|
||||
DumpGames(false);
|
||||
});
|
||||
|
||||
options->Add<SidebarEntryCallback>("Export NSZ"_i18n, [this](){
|
||||
DumpGames(true);
|
||||
});
|
||||
|
||||
options->Add<SidebarEntryCallback>("Export options"_i18n, [this](){
|
||||
@@ -153,7 +157,7 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
if (touch && m_index == i) {
|
||||
FireAction(Button::A);
|
||||
} else {
|
||||
App::PlaySoundEffect(SoundEffect_Focus);
|
||||
App::PlaySoundEffect(SoundEffect::Focus);
|
||||
SetIndex(i);
|
||||
}
|
||||
});
|
||||
@@ -345,14 +349,14 @@ Result Menu::GetNcmSizeOfMetaStatus(MetaEntry& entry) const {
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
void Menu::DumpGames() {
|
||||
void Menu::DumpGames(bool to_nsz) {
|
||||
const auto entries = GetSelectedEntries();
|
||||
App::PopToMenu();
|
||||
|
||||
std::vector<NspEntry> nsps;
|
||||
BuildNspEntries(m_entry, entries, nsps);
|
||||
BuildNspEntries(m_entry, entries, nsps, to_nsz);
|
||||
|
||||
DumpNsp(nsps);
|
||||
DumpNsp(nsps, to_nsz);
|
||||
}
|
||||
|
||||
void Menu::DeleteGames() {
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
#include "yati/nx/keys.hpp"
|
||||
#include "yati/nx/crypto.hpp"
|
||||
|
||||
#include "utils/utils.hpp"
|
||||
#include "utils/devoptab.hpp"
|
||||
|
||||
#include "title_info.hpp"
|
||||
#include "app.hpp"
|
||||
#include "dumper.hpp"
|
||||
@@ -26,18 +29,6 @@
|
||||
namespace sphaira::ui::menu::game::meta_nca {
|
||||
namespace {
|
||||
|
||||
struct HashStr {
|
||||
char str[0x21];
|
||||
};
|
||||
|
||||
HashStr hexIdToStr(auto id) {
|
||||
HashStr str{};
|
||||
const auto id_lower = std::byteswap(*(u64*)id.c);
|
||||
const auto id_upper = std::byteswap(*(u64*)(id.c + 0x8));
|
||||
std::snprintf(str.str, 0x21, "%016lx%016lx", id_lower, id_upper);
|
||||
return str;
|
||||
}
|
||||
|
||||
struct NcaHashSource final : hash::BaseSource {
|
||||
NcaHashSource(NcmContentStorage* cs, const NcaEntry& entry) : m_cs{cs}, m_entry{entry} {
|
||||
}
|
||||
@@ -67,7 +58,7 @@ struct NcaSource final : dump::BaseSource {
|
||||
|
||||
Result Read(const std::string& path, void* buf, s64 off, s64 size, u64* bytes_read) override {
|
||||
const auto it = std::ranges::find_if(m_entries, [&path](auto& e){
|
||||
return path.find(hexIdToStr(e.content_id).str) != path.npos;
|
||||
return path.find(utils::hexIdToStr(e.content_id).str) != path.npos;
|
||||
});
|
||||
R_UNLESS(it != m_entries.end(), Result_GameBadReadForDump);
|
||||
|
||||
@@ -85,11 +76,11 @@ struct NcaSource final : dump::BaseSource {
|
||||
|
||||
auto GetName(const std::string& path) const -> std::string {
|
||||
const auto it = std::ranges::find_if(m_entries, [&path](auto& e){
|
||||
return path.find(hexIdToStr(e.content_id).str) != path.npos;
|
||||
return path.find(utils::hexIdToStr(e.content_id).str) != path.npos;
|
||||
});
|
||||
|
||||
if (it != m_entries.end()) {
|
||||
return hexIdToStr(it->content_id).str;
|
||||
return utils::hexIdToStr(it->content_id).str;
|
||||
}
|
||||
|
||||
return {};
|
||||
@@ -97,7 +88,7 @@ struct NcaSource final : dump::BaseSource {
|
||||
|
||||
auto GetSize(const std::string& path) const -> s64 {
|
||||
const auto it = std::ranges::find_if(m_entries, [&path](auto& e){
|
||||
return path.find(hexIdToStr(e.content_id).str) != path.npos;
|
||||
return path.find(utils::hexIdToStr(e.content_id).str) != path.npos;
|
||||
});
|
||||
|
||||
if (it != m_entries.end()) {
|
||||
@@ -172,7 +163,8 @@ Menu::Menu(Entry& entry, const meta::MetaEntry& meta_entry)
|
||||
std::make_pair(Button::A, Action{"Mount Fs"_i18n, [this](){
|
||||
// todo: handle error here.
|
||||
if (!m_entries.empty() && !GetEntry().missing) {
|
||||
MountNcaFs();
|
||||
const auto rc = MountNcaFs();
|
||||
App::PushErrorBox(rc, "Failed to mount NCA"_i18n);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::B, Action{"Back"_i18n, [this](){
|
||||
@@ -187,16 +179,23 @@ Menu::Menu(Entry& entry, const meta::MetaEntry& meta_entry)
|
||||
DumpNcas();
|
||||
});
|
||||
|
||||
// todo:
|
||||
#if 0
|
||||
options->Add<SidebarEntryCallback>("Export NCA decrypted"_i18n, [this](){
|
||||
DumpNcas();
|
||||
}, "Exports the NCA with all fs sections decrypted (NCA header is still encrypted)."_i18n);
|
||||
#endif
|
||||
|
||||
options->Add<SidebarEntryCallback>("Verify NCA 256 hash"_i18n, [this](){
|
||||
static std::string hash_out;
|
||||
hash_out.clear();
|
||||
|
||||
App::Push<ProgressBox>(m_entry.image, "Hashing"_i18n, hexIdToStr(GetEntry().content_id).str, [this](auto pbox) -> Result{
|
||||
App::Push<ProgressBox>(m_entry.image, "Hashing"_i18n, utils::hexIdToStr(GetEntry().content_id).str, [this](auto pbox) -> Result{
|
||||
auto source = std::make_unique<NcaHashSource>(m_meta.cs, GetEntry());
|
||||
return hash::Hash(pbox, hash::Type::Sha256, source.get(), hash_out);
|
||||
}, [this](Result rc){
|
||||
App::PushErrorBox(rc, "Failed to hash file..."_i18n);
|
||||
const auto str = hexIdToStr(GetEntry().content_id);
|
||||
const auto str = utils::hexIdToStr(GetEntry().content_id);
|
||||
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
if (std::strncmp(hash_out.c_str(), str.str, std::strlen(str.str))) {
|
||||
@@ -227,6 +226,7 @@ Menu::Menu(Entry& entry, const meta::MetaEntry& meta_entry)
|
||||
parse_keys(keys, false);
|
||||
|
||||
if (R_FAILED(GetNcmMetaFromMetaStatus(m_meta_entry.status, m_meta))) {
|
||||
log_write("[NCA-MENU] failed to GetNcmMetaFromMetaStatus()\n");
|
||||
SetPop();
|
||||
return;
|
||||
}
|
||||
@@ -234,6 +234,7 @@ Menu::Menu(Entry& entry, const meta::MetaEntry& meta_entry)
|
||||
// get the content meta header.
|
||||
ncm::ContentMeta content_meta;
|
||||
if (R_FAILED(ncm::GetContentMeta(m_meta.db, &m_meta.key, content_meta))) {
|
||||
log_write("[NCA-MENU] failed to ncm::GetContentMeta()\n");
|
||||
SetPop();
|
||||
return;
|
||||
}
|
||||
@@ -241,6 +242,7 @@ Menu::Menu(Entry& entry, const meta::MetaEntry& meta_entry)
|
||||
// fetch all the content infos.
|
||||
std::vector<NcmContentInfo> infos;
|
||||
if (R_FAILED(ncm::GetContentInfos(m_meta.db, &m_meta.key, content_meta.header, infos))) {
|
||||
log_write("[NCA-MENU] failed to ncm::GetContentInfos()\n");
|
||||
SetPop();
|
||||
return;
|
||||
}
|
||||
@@ -252,12 +254,17 @@ Menu::Menu(Entry& entry, const meta::MetaEntry& meta_entry)
|
||||
ncmContentInfoSizeToU64(&info, &entry.size);
|
||||
|
||||
bool has = false;
|
||||
ncmContentMetaDatabaseHasContent(m_meta.db, &has, &m_meta.key, &info.content_id);
|
||||
if (R_FAILED(ncmContentMetaDatabaseHasContent(m_meta.db, &has, &m_meta.key, &info.content_id)) || !has) {
|
||||
log_write("[NCA-MENU] does not have nca!\n");
|
||||
}
|
||||
entry.missing = !has;
|
||||
|
||||
// decrypt header.
|
||||
if (has && R_SUCCEEDED(ncmContentStorageReadContentIdFile(m_meta.cs, &entry.header, sizeof(entry.header), &info.content_id, 0))) {
|
||||
// decrypt header.
|
||||
log_write("[NCA-MENU] reading to decrypt header\n");
|
||||
crypto::cryptoAes128Xts(&entry.header, &entry.header, keys.header_key, 0, 0x200, sizeof(entry.header), false);
|
||||
} else {
|
||||
log_write("[NCA-MENU] failed to read nca from ncm\n");
|
||||
}
|
||||
|
||||
m_entries.emplace_back(entry);
|
||||
@@ -283,7 +290,7 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
if (touch && m_index == i) {
|
||||
FireAction(Button::A);
|
||||
} else {
|
||||
App::PlaySoundEffect(SoundEffect_Focus);
|
||||
App::PlaySoundEffect(SoundEffect::Focus);
|
||||
SetIndex(i);
|
||||
}
|
||||
});
|
||||
@@ -337,7 +344,7 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
}
|
||||
|
||||
gfx::drawTextArgs(vg, x + text_xoffset, y + (h / 2.f), 20.f, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(text_id), "%s", ncm::GetContentTypeStr(e.content_type));
|
||||
gfx::drawTextArgs(vg, x + text_xoffset + 185, y + (h / 2.f), 20.f, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(text_id), "%s", hexIdToStr(e.content_id).str);
|
||||
gfx::drawTextArgs(vg, x + text_xoffset + 185, y + (h / 2.f), 20.f, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(text_id), "%s", utils::hexIdToStr(e.content_id).str);
|
||||
|
||||
if ((double)e.size / 1024.0 / 1024.0 <= 0.009) {
|
||||
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f), 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_MIDDLE, theme->GetColour(text_id), "%.2f KiB", (double)e.size / 1024.0);
|
||||
@@ -384,7 +391,7 @@ void Menu::DumpNcas() {
|
||||
std::vector<fs::FsPath> paths;
|
||||
for (auto& e : entries) {
|
||||
char nca_name[64];
|
||||
std::snprintf(nca_name, sizeof(nca_name), "%s%s", hexIdToStr(e.content_id).str, e.content_type == NcmContentType_Meta ? ".cnmt.nca" : ".nca");
|
||||
std::snprintf(nca_name, sizeof(nca_name), "%s%s", utils::hexIdToStr(e.content_id).str, e.content_type == NcmContentType_Meta ? ".cnmt.nca" : ".nca");
|
||||
|
||||
fs::FsPath path;
|
||||
std::snprintf(path, sizeof(path), "/dumps/NCA/%s %s[%016lX][v%u][%s]/%s", name_buf.s, version, m_meta_entry.status.application_id, m_meta_entry.status.version, ncm::GetMetaTypeShortStr(m_meta_entry.status.meta_type), nca_name);
|
||||
@@ -393,25 +400,36 @@ void Menu::DumpNcas() {
|
||||
}
|
||||
|
||||
auto source = std::make_shared<NcaSource>(m_meta.cs, m_entry.image, entries);
|
||||
dump::Dump(source, paths, [](Result){}, dump::DumpLocationFlag_All &~ dump::DumpLocationFlag_UsbS2S);
|
||||
dump::Dump(source, paths, nullptr, dump::DumpLocationFlag_All &~ dump::DumpLocationFlag_UsbS2S);
|
||||
}
|
||||
|
||||
Result Menu::MountNcaFs() {
|
||||
const auto& e = GetEntry();
|
||||
|
||||
// mount using devoptab instead if fails.
|
||||
FsFileSystemType type;
|
||||
R_TRY(GetFsFileSystemType(e.header.content_type, type));
|
||||
if (R_FAILED(GetFsFileSystemType(e.header.content_type, type))) {
|
||||
fs::FsPath root;
|
||||
R_TRY(devoptab::MountNcaNcm(m_meta.cs, &e.content_id, root));
|
||||
|
||||
// get fs path from ncm.
|
||||
u64 program_id;
|
||||
fs::FsPath path;
|
||||
R_TRY(ncm::GetFsPathFromContentId(m_meta.cs, m_meta.key, e.content_id, &program_id, &path));
|
||||
auto fs = std::make_shared<filebrowser::FsStdioWrapper>(root, [root](){
|
||||
devoptab::UmountNca(root);
|
||||
});
|
||||
|
||||
// ensure that mounting worked.
|
||||
auto fs = std::make_shared<fs::FsNativeId>(program_id, type, path);
|
||||
R_TRY(fs->GetFsOpenResult());
|
||||
filebrowser::MountFsHelper(fs, utils::hexIdToStr(e.content_id).str);
|
||||
} else {
|
||||
// get fs path from ncm.
|
||||
u64 program_id;
|
||||
fs::FsPath path;
|
||||
R_TRY(ncm::GetFsPathFromContentId(m_meta.cs, m_meta.key, e.content_id, &program_id, &path));
|
||||
|
||||
// ensure that mounting worked.
|
||||
auto fs = std::make_shared<fs::FsNativeId>(program_id, type, path);
|
||||
R_TRY(fs->GetFsOpenResult());
|
||||
|
||||
filebrowser::MountFsHelper(fs, utils::hexIdToStr(e.content_id).str);
|
||||
}
|
||||
|
||||
filebrowser::MountFsHelper(fs, hexIdToStr(e.content_id).str);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,11 @@
|
||||
|
||||
#include "yati/yati.hpp"
|
||||
#include "yati/nx/nca.hpp"
|
||||
#include "yati/container/xci.hpp"
|
||||
|
||||
#include "utils/utils.hpp"
|
||||
#include "utils/nsz_dumper.hpp"
|
||||
#include "utils/devoptab.hpp"
|
||||
|
||||
#include "app.hpp"
|
||||
#include "defines.hpp"
|
||||
@@ -17,16 +22,39 @@
|
||||
#include "dumper.hpp"
|
||||
#include "image.hpp"
|
||||
#include "title_info.hpp"
|
||||
#include "threaded_file_transfer.hpp"
|
||||
|
||||
#include <cstring>
|
||||
#include <algorithm>
|
||||
|
||||
// from Gamecard-Installer-NX
|
||||
extern "C" {
|
||||
|
||||
Result fsOpenGameCardStorage(FsStorage* out, const FsGameCardHandle* handle, FsGameCardPartitionRaw partition) {
|
||||
const struct {
|
||||
FsGameCardHandle handle;
|
||||
u32 partition;
|
||||
} in = { *handle, (u32)partition };
|
||||
|
||||
return serviceDispatchIn(fsGetServiceSession(), 30, in, .out_num_objects = 1, .out_objects = &out->s);
|
||||
}
|
||||
|
||||
Result fsOpenGameCardDetectionEventNotifier(FsEventNotifier* out) {
|
||||
return serviceDispatch(fsGetServiceSession(), 501,
|
||||
.out_num_objects = 1,
|
||||
.out_objects = &out->s
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
namespace sphaira::ui::menu::gc {
|
||||
namespace {
|
||||
|
||||
constexpr u32 XCI_MAGIC = std::byteswap(0x48454144);
|
||||
constexpr u32 REMOUNT_ATTEMPT_MAX = 8; // same as nxdumptool.
|
||||
constexpr const char* DUMP_BASE_PATH = "/dumps/Gamecard";
|
||||
constexpr const char* DUMP_GAMECARD_BASE_PATH = "/dumps/Gamecard";
|
||||
constexpr const char* DUMP_XCZ_BASE_PATH = "/dumps/XCZ";
|
||||
|
||||
enum DumpFileType {
|
||||
DumpFileType_XCI,
|
||||
@@ -35,6 +63,7 @@ enum DumpFileType {
|
||||
DumpFileType_UID,
|
||||
DumpFileType_Cert,
|
||||
DumpFileType_Initial,
|
||||
DumpFileType_XCZ,
|
||||
};
|
||||
|
||||
enum DumpFileFlag {
|
||||
@@ -50,9 +79,9 @@ enum DumpFileFlag {
|
||||
|
||||
const char *g_option_list[] = {
|
||||
"Install",
|
||||
"Export",
|
||||
"Mount",
|
||||
"Exit",
|
||||
"Export XCI (Gamecard)",
|
||||
"Export XCZ (Compressed XCI)",
|
||||
"Mount Fs",
|
||||
};
|
||||
|
||||
auto GetXciSizeFromRomSize(u8 rom_size) -> s64 {
|
||||
@@ -90,6 +119,7 @@ auto GetDumpTypeStr(u8 type) -> const char* {
|
||||
case DumpFileType_UID: return " (Card UID).bin";
|
||||
case DumpFileType_Cert: return " (Certificate).bin";
|
||||
case DumpFileType_Initial: return " (Initial Data).bin";
|
||||
case DumpFileType_XCZ: return ".xcz";
|
||||
}
|
||||
|
||||
return "";
|
||||
@@ -128,22 +158,27 @@ auto BuildFullDumpPath(DumpFileType type, std::span<const ApplicationEntry> entr
|
||||
const auto base_path = BuildXciBasePath(entries);
|
||||
fs::FsPath out;
|
||||
|
||||
if (use_folder) {
|
||||
if (App::GetApp()->m_dump_append_folder_with_xci.Get()) {
|
||||
out = base_path + ".xci/" + base_path + GetDumpTypeStr(type);
|
||||
} else {
|
||||
out = base_path + "/" + base_path + GetDumpTypeStr(type);
|
||||
}
|
||||
} else {
|
||||
if (type == DumpFileType_XCZ) {
|
||||
out = base_path + GetDumpTypeStr(type);
|
||||
}
|
||||
return fs::AppendPath(DUMP_XCZ_BASE_PATH, out);
|
||||
} else {
|
||||
if (use_folder) {
|
||||
if (App::GetApp()->m_dump_append_folder_with_xci.Get()) {
|
||||
out = base_path + ".xci/" + base_path + GetDumpTypeStr(type);
|
||||
} else {
|
||||
out = base_path + "/" + base_path + GetDumpTypeStr(type);
|
||||
}
|
||||
} else {
|
||||
out = base_path + GetDumpTypeStr(type);
|
||||
}
|
||||
|
||||
return fs::AppendPath(DUMP_BASE_PATH, out);
|
||||
return fs::AppendPath(DUMP_GAMECARD_BASE_PATH, out);
|
||||
}
|
||||
}
|
||||
|
||||
auto BuildFullDumpPath(DumpFileType type, std::span<const ApplicationEntry> entries) -> fs::FsPath {
|
||||
// check if the base path is too long.
|
||||
const auto max_len = fs::FsPathReal::FS_REAL_MAX_LENGTH - std::strlen(DUMP_BASE_PATH) - 30;
|
||||
const auto max_len = fs::FsPathReal::FS_REAL_MAX_LENGTH - std::strlen(DUMP_GAMECARD_BASE_PATH) - 30;
|
||||
auto use_folder = App::GetApp()->m_dump_app_folder.Get();
|
||||
|
||||
for (;;) {
|
||||
@@ -174,7 +209,7 @@ auto BuildFullDumpPath(DumpFileType type, std::span<const ApplicationEntry> entr
|
||||
// @Gc is the mount point, S is for secure partion, the remaining is the
|
||||
// the gamecard handle value in lower-case hex.
|
||||
auto BuildGcPath(const char* name, const FsGameCardHandle* handle, FsGameCardPartition partiton = FsGameCardPartition_Secure) -> fs::FsPath {
|
||||
static const char mount_parition[] = {
|
||||
static const char mount_partition[] = {
|
||||
[FsGameCardPartition_Update] = 'U',
|
||||
[FsGameCardPartition_Normal] = 'N',
|
||||
[FsGameCardPartition_Secure] = 'S',
|
||||
@@ -182,7 +217,7 @@ auto BuildGcPath(const char* name, const FsGameCardHandle* handle, FsGameCardPar
|
||||
};
|
||||
|
||||
fs::FsPath path;
|
||||
std::snprintf(path, sizeof(path), "@Gc%c%08x://%s", mount_parition[partiton], handle->value, name);
|
||||
std::snprintf(path, sizeof(path), "@Gc%c%08x://%s", mount_partition[partiton], handle->value, name);
|
||||
return path;
|
||||
}
|
||||
|
||||
@@ -200,7 +235,7 @@ struct XciSource final : dump::BaseSource {
|
||||
int icon{};
|
||||
|
||||
Result Read(const std::string& path, void* buf, s64 off, s64 size, u64* bytes_read) override {
|
||||
if (path.ends_with(GetDumpTypeStr(DumpFileType_XCI))) {
|
||||
if (path.ends_with(GetDumpTypeStr(DumpFileType_XCI)) || path.ends_with(GetDumpTypeStr(DumpFileType_XCZ))) {
|
||||
size = ClipSize(off, size, xci_size);
|
||||
*bytes_read = size;
|
||||
return menu->GcStorageRead(buf, off, size);
|
||||
@@ -231,7 +266,7 @@ struct XciSource final : dump::BaseSource {
|
||||
}
|
||||
|
||||
auto GetSize(const std::string& path) const -> s64 override {
|
||||
if (path.ends_with(GetDumpTypeStr(DumpFileType_XCI))) {
|
||||
if (path.ends_with(GetDumpTypeStr(DumpFileType_XCI)) || path.ends_with(GetDumpTypeStr(DumpFileType_XCZ))) {
|
||||
return xci_size;
|
||||
} else if (path.ends_with(GetDumpTypeStr(DumpFileType_Set))) {
|
||||
return id_set.size();
|
||||
@@ -259,33 +294,175 @@ private:
|
||||
}
|
||||
};
|
||||
|
||||
struct HashStr {
|
||||
char str[0x21];
|
||||
struct Test final : yati::source::Base {
|
||||
Test(Menu* menu) : m_menu{menu} {
|
||||
|
||||
}
|
||||
|
||||
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override {
|
||||
R_TRY(m_menu->GcStorageRead(buf, off, size));
|
||||
*bytes_read = size;
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
private:
|
||||
Menu* m_menu;
|
||||
};
|
||||
|
||||
HashStr hexIdToStr(auto id) {
|
||||
HashStr str{};
|
||||
const auto id_lower = std::byteswap(*(u64*)id.c);
|
||||
const auto id_upper = std::byteswap(*(u64*)(id.c + 0x8));
|
||||
std::snprintf(str.str, 0x21, "%016lx%016lx", id_lower, id_upper);
|
||||
return str;
|
||||
}
|
||||
struct NcaReader final : yati::source::Base {
|
||||
NcaReader(Test* source, s64 offset) : m_source{source}, m_offset{offset} {
|
||||
|
||||
// from Gamecard-Installer-NX
|
||||
Result fsOpenGameCardStorage(FsStorage* out, const FsGameCardHandle* handle, FsGameCardPartitionRaw partition) {
|
||||
const struct {
|
||||
FsGameCardHandle handle;
|
||||
u32 partition;
|
||||
} in = { *handle, (u32)partition };
|
||||
}
|
||||
|
||||
return serviceDispatchIn(fsGetServiceSession(), 30, in, .out_num_objects = 1, .out_objects = &out->s);
|
||||
}
|
||||
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override {
|
||||
return m_source->Read(buf, m_offset + off, size, bytes_read);
|
||||
}
|
||||
|
||||
Result fsOpenGameCardDetectionEventNotifier(FsEventNotifier* out) {
|
||||
return serviceDispatch(fsGetServiceSession(), 501,
|
||||
.out_num_objects = 1,
|
||||
.out_objects = &out->s
|
||||
);
|
||||
private:
|
||||
Test* m_source;
|
||||
const s64 m_offset;
|
||||
};
|
||||
|
||||
Result NszExport(ProgressBox* pbox, const keys::Keys& keys, dump::BaseSource* _source, dump::WriteSource* writer, const fs::FsPath& path) {
|
||||
auto source = (XciSource*)_source;
|
||||
|
||||
const auto threaded_write = [&](const std::string& name, s64& read_offset, s64& write_offset, s64 size) -> Result {
|
||||
if (size > 0) {
|
||||
pbox->NewTransfer(name);
|
||||
|
||||
R_TRY(thread::Transfer(pbox, size,
|
||||
[&](void* data, s64 off, s64 size, u64* bytes_read) -> Result {
|
||||
return source->Read(path, data, read_offset + off, size, bytes_read);
|
||||
},
|
||||
[&](const void* data, s64 off, s64 size) -> Result {
|
||||
return writer->Write(data, write_offset + off, size);
|
||||
}
|
||||
));
|
||||
|
||||
read_offset += size;
|
||||
write_offset += size;
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
};
|
||||
|
||||
// writes padding between partitions and files.
|
||||
const auto write_padding = [&](const std::string& name, s64& read_offset, s64& write_offset, s64 size) -> Result {
|
||||
return threaded_write("Writing padding - " + name, read_offset, write_offset, size);
|
||||
};
|
||||
|
||||
Test yati_source(source->menu);
|
||||
yati::container::Xci xci{&yati_source};
|
||||
|
||||
yati::container::Xci::Root root;
|
||||
R_TRY(xci.GetRoot(root));
|
||||
|
||||
//
|
||||
s64 read_offset = 0;
|
||||
s64 write_offset = 0;
|
||||
|
||||
for (u32 i = 0; i < std::size(root.partitions); i++) {
|
||||
auto& partition = root.partitions[i];
|
||||
auto& hfs0 = partition.hfs0;
|
||||
auto& collections = partition.collections;
|
||||
|
||||
log_write("\tpartition name: %s offset: %zu size: %zu\n", partition.name.c_str(), partition.hfs0_offset, partition.hfs0_size);
|
||||
|
||||
// read pading before hfs0
|
||||
R_TRY(write_padding("hfs0 before", read_offset, write_offset, partition.hfs0_offset - read_offset));
|
||||
|
||||
// offset to the hfs0.
|
||||
const auto hfs0_offset = write_offset;
|
||||
// offset to the data within the hfs0.
|
||||
const auto hfs0_data_offset = hfs0_offset + hfs0.GetHfs0Size();
|
||||
// offset to the hfs0 within the root hfs0.
|
||||
const auto root_hfs0_data_offset = write_offset - root.hfs0.data_offset;
|
||||
|
||||
// calculate the expected size of the partition.
|
||||
s64 expected_hfs0_data_size = 0;
|
||||
for (auto& collection : partition.collections) {
|
||||
expected_hfs0_data_size += collection.size;
|
||||
}
|
||||
|
||||
if (!partition.collections.empty()) {
|
||||
R_TRY(write_padding(partition.name, read_offset, write_offset, partition.collections[0].offset - read_offset));
|
||||
} else {
|
||||
// empty hfs0, write it as is.
|
||||
log_write("empty hfs0 offset: %zu size: %zu get size: %zu\n", hfs0.data_offset, partition.hfs0_size, hfs0.GetHfs0Size());
|
||||
R_UNLESS(partition.hfs0_size == hfs0.GetHfs0Size(), 21);
|
||||
// R_UNLESS(hfs0.data_offset == 0, 14);
|
||||
R_TRY(write_padding(partition.name, read_offset, write_offset, partition.hfs0_size));
|
||||
}
|
||||
|
||||
const auto nca_creator = [&yati_source](const nca::Header& header, const keys::KeyEntry& title_key, const utils::nsz::Collection& collection) {
|
||||
return std::make_unique<nca::NcaReader>(
|
||||
header, &title_key, collection.size,
|
||||
std::make_shared<NcaReader>(&yati_source, collection.offset)
|
||||
);
|
||||
};
|
||||
|
||||
// todo: update write offset.
|
||||
R_TRY(utils::nsz::NszExport(pbox, nca_creator, read_offset, write_offset, collections, keys, source, writer, path));
|
||||
|
||||
// update offset / size in file table and calculate new total data size.
|
||||
s64 new_hfs0_data_size = 0;
|
||||
for (u32 i = 0; i < std::size(collections); i++) {
|
||||
auto& collection = collections[i];
|
||||
auto& file_table = hfs0.file_table[i];
|
||||
|
||||
// const auto offset = collection.offset - hfs0_data_offset;
|
||||
// log_write("offset: %zu\n", offset);
|
||||
// log_write("collection.offset: %zu\n", collection.offset);
|
||||
// log_write("hfs0.data_offset: %zu\n", hfs0.data_offset);
|
||||
// log_write("file_table.data_offset: %zu\n", file_table.data_offset);
|
||||
|
||||
// R_UNLESS(file_table.data_offset == offset, 8);
|
||||
// R_UNLESS(file_table.data_size = collection.size, 9);
|
||||
|
||||
// update file and string table from collection.
|
||||
file_table.data_offset = collection.offset - hfs0_data_offset;
|
||||
file_table.data_size = collection.size;
|
||||
hfs0.string_table[i] = collection.name;
|
||||
new_hfs0_data_size += collection.size;
|
||||
}
|
||||
|
||||
// update offset and size of hfs0 in root file table.
|
||||
auto& root_file_table = root.hfs0.file_table[i];
|
||||
const auto hfs0_data_size = root_file_table.data_size - (expected_hfs0_data_size - new_hfs0_data_size);
|
||||
|
||||
log_write("hfs0.data_offset: %zu\n", hfs0.data_offset);
|
||||
log_write("old data offset: %zu\n", root_file_table.data_offset);
|
||||
log_write("new data offset: %zu\n\n", root_hfs0_data_offset);
|
||||
|
||||
log_write("old data size: %zu\n", root_file_table.data_size);
|
||||
log_write("new data size: %zu\n", hfs0_data_size);
|
||||
|
||||
// R_UNLESS(root_file_table.data_offset == root_hfs0_data_offset, 5);
|
||||
// R_UNLESS(root_file_table.data_size == hfs0_data_size, 6);
|
||||
|
||||
root_file_table.data_offset = root_hfs0_data_offset;
|
||||
root_file_table.data_size = hfs0_data_size;
|
||||
|
||||
// re-write updated hfs0 partition.
|
||||
// R_UNLESS(partition.hfs0_offset == hfs0_offset, 7);
|
||||
const auto hfs0_data = hfs0.GetHfs0Data();
|
||||
R_TRY(writer->Write(hfs0_data.data(), hfs0_offset, hfs0_data.size()));
|
||||
}
|
||||
|
||||
// add remaining padding, if needed.
|
||||
R_TRY(write_padding("hfs0 partition", read_offset, write_offset, read_offset % 512));
|
||||
|
||||
// re-write updated root partition.
|
||||
const auto root_data = root.hfs0.GetHfs0Data();
|
||||
R_TRY(writer->Write(root_data.data(), root.hfs0_offset, root_data.size()));
|
||||
|
||||
log_write("read_offset: %zu\n", read_offset);
|
||||
log_write("write_offset: %zu\n", write_offset);
|
||||
|
||||
// update with actual size.
|
||||
R_TRY(writer->SetSize(write_offset));
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
struct GcSource final : yati::source::Base {
|
||||
@@ -401,51 +578,49 @@ auto ApplicationEntry::GetSize() const -> s64 {
|
||||
Menu::Menu(u32 flags) : MenuBase{"GameCard"_i18n, flags} {
|
||||
this->SetActions(
|
||||
std::make_pair(Button::A, Action{"OK"_i18n, [this](){
|
||||
if (m_option_index == 3) {
|
||||
SetPop();
|
||||
} else {
|
||||
if (!m_mounted) {
|
||||
return;
|
||||
if (!m_mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_option_index == 0) {
|
||||
if (!App::GetInstallEnable()) {
|
||||
App::ShowEnableInstallPrompt();
|
||||
} else {
|
||||
log_write("[GC] doing install A\n");
|
||||
App::Push<ui::ProgressBox>(m_icon, "Installing "_i18n, m_entries[m_entry_index].lang_entry.name, [this](auto pbox) -> Result {
|
||||
auto source = std::make_unique<GcSource>(m_entries[m_entry_index], m_fs.get());
|
||||
return yati::InstallFromCollections(pbox, source.get(), source->m_collections, source->m_config);
|
||||
}, [this](Result rc){
|
||||
App::PushErrorBox(rc, "Gc install failed!"_i18n);
|
||||
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
App::Notify("Gc install success!"_i18n);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (m_option_index == 1) {
|
||||
auto options = std::make_unique<Sidebar>("Select content to dump"_i18n, Sidebar::Side::RIGHT);
|
||||
ON_SCOPE_EXIT(App::Push(std::move(options)));
|
||||
|
||||
if (m_option_index == 0) {
|
||||
if (!App::GetInstallEnable()) {
|
||||
App::ShowEnableInstallPrompt();
|
||||
} else {
|
||||
log_write("[GC] doing install A\n");
|
||||
App::Push<ui::ProgressBox>(m_icon, "Installing "_i18n, m_entries[m_entry_index].lang_entry.name, [this](auto pbox) -> Result {
|
||||
auto source = std::make_unique<GcSource>(m_entries[m_entry_index], m_fs.get());
|
||||
return yati::InstallFromCollections(pbox, source.get(), source->m_collections, source->m_config);
|
||||
}, [this](Result rc){
|
||||
App::PushErrorBox(rc, "Gc install failed!"_i18n);
|
||||
const auto add = [&](const std::string& name, u32 flags){
|
||||
options->Add<SidebarEntryCallback>(name, [this, flags](){
|
||||
DumpGames(flags);
|
||||
m_dirty = true;
|
||||
}, true);
|
||||
};
|
||||
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
App::Notify("Gc install success!"_i18n);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (m_option_index == 1) {
|
||||
auto options = std::make_unique<Sidebar>("Select content to dump"_i18n, Sidebar::Side::RIGHT);
|
||||
ON_SCOPE_EXIT(App::Push(std::move(options)));
|
||||
|
||||
const auto add = [&](const std::string& name, u32 flags){
|
||||
options->Add<SidebarEntryCallback>(name, [this, flags](){
|
||||
DumpGames(flags);
|
||||
m_dirty = true;
|
||||
}, true);
|
||||
};
|
||||
|
||||
add("Export All"_i18n, DumpFileFlag_All);
|
||||
add("Export All Bins"_i18n, DumpFileFlag_AllBin);
|
||||
add("Export XCI"_i18n, DumpFileFlag_XCI);
|
||||
add("Export Card ID Set"_i18n, DumpFileFlag_Set);
|
||||
add("Export Card UID"_i18n, DumpFileFlag_UID);
|
||||
add("Export Certificate"_i18n, DumpFileFlag_Cert);
|
||||
add("Export Initial Data"_i18n, DumpFileFlag_Initial);
|
||||
} else if (m_option_index == 2) {
|
||||
const auto rc = MountGcFs();
|
||||
App::PushErrorBox(rc, "Failed to mount GameCard filesystem"_i18n);
|
||||
}
|
||||
add("Export All"_i18n, DumpFileFlag_All);
|
||||
add("Export All Bins"_i18n, DumpFileFlag_AllBin);
|
||||
add("Export XCI"_i18n, DumpFileFlag_XCI);
|
||||
add("Export Card ID Set"_i18n, DumpFileFlag_Set);
|
||||
add("Export Card UID"_i18n, DumpFileFlag_UID);
|
||||
add("Export Certificate"_i18n, DumpFileFlag_Cert);
|
||||
add("Export Initial Data"_i18n, DumpFileFlag_Initial);
|
||||
} else if (m_option_index == 2) {
|
||||
DumpXcz(0);
|
||||
} else if (m_option_index == 3) {
|
||||
const auto rc = MountGcFs();
|
||||
App::PushErrorBox(rc, "Failed to mount GameCard filesystem"_i18n);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::B, Action{"Back"_i18n, [this](){
|
||||
@@ -497,7 +672,7 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
if (touch && m_option_index == i) {
|
||||
FireAction(Button::A);
|
||||
} else {
|
||||
App::PlaySoundEffect(SoundEffect_Focus);
|
||||
App::PlaySoundEffect(SoundEffect::Focus);
|
||||
m_option_index = i;
|
||||
}
|
||||
});
|
||||
@@ -551,7 +726,7 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
gfx::drawRect(vg, 490, text_y - 45.f / 2.f, 2, 45, theme->GetColour(ThemeEntryID_TEXT_SELECTED));
|
||||
colour = ThemeEntryID_TEXT_SELECTED;
|
||||
}
|
||||
if (i != 3 && !m_mounted) {
|
||||
if (!m_mounted) {
|
||||
colour = ThemeEntryID_TEXT_INFO;
|
||||
}
|
||||
|
||||
@@ -632,7 +807,7 @@ Result Menu::GcMount() {
|
||||
}
|
||||
|
||||
// find the nca file, this will never fail for gamecards, see above comment.
|
||||
const auto str = hexIdToStr(info.content_id);
|
||||
const auto str = utils::hexIdToStr(info.content_id);
|
||||
const auto it = std::find_if(buf.cbegin(), buf.cend(), [str](auto& e){
|
||||
return !std::strncmp(str.str, e.name, std::strlen(str.str));
|
||||
});
|
||||
@@ -735,12 +910,12 @@ Result Menu::GcMountStorage() {
|
||||
log_write("[GC] m_storage_full_size: %zd rom_size: 0x%X\n", m_storage_full_size, rom_size);
|
||||
R_UNLESS(m_storage_full_size > 0, Result_GcBadXciRomSize);
|
||||
|
||||
R_TRY(fsStorageGetSize(&m_storage, &m_parition_normal_size));
|
||||
R_TRY(fsStorageGetSize(&m_storage, &m_partition_normal_size));
|
||||
R_TRY(GcMountPartition(FsGameCardPartitionRaw_Secure));
|
||||
R_TRY(fsStorageGetSize(&m_storage, &m_parition_secure_size));
|
||||
R_TRY(fsStorageGetSize(&m_storage, &m_partition_secure_size));
|
||||
|
||||
m_storage_trimmed_size = sizeof(header) + trim_size * 512ULL;
|
||||
m_storage_total_size = m_parition_normal_size + m_parition_secure_size;
|
||||
m_storage_total_size = m_partition_normal_size + m_partition_secure_size;
|
||||
m_storage_mounted = true;
|
||||
|
||||
log_write("[GC] m_storage_trimmed_size: %zd\n", m_storage_trimmed_size);
|
||||
@@ -786,11 +961,11 @@ void Menu::GcUnmountPartition() {
|
||||
}
|
||||
|
||||
Result Menu::GcStorageReadInternal(void* buf, s64 off, s64 size, u64* bytes_read) {
|
||||
if (off < m_parition_normal_size) {
|
||||
size = std::min<s64>(size, m_parition_normal_size - off);
|
||||
if (off < m_partition_normal_size) {
|
||||
size = std::min<s64>(size, m_partition_normal_size - off);
|
||||
R_TRY(GcMountPartition(FsGameCardPartitionRaw_Normal));
|
||||
} else {
|
||||
off = off - m_parition_normal_size;
|
||||
off = off - m_partition_normal_size;
|
||||
R_TRY(GcMountPartition(FsGameCardPartitionRaw_Secure));
|
||||
}
|
||||
|
||||
@@ -833,9 +1008,7 @@ Result Menu::GcStorageRead(void* _buf, s64 off, s64 size) {
|
||||
|
||||
if (unaligned_size) {
|
||||
R_TRY(GcStorageReadInternal(data, off, sizeof(data), &bytes_read));
|
||||
|
||||
const auto csize = std::min<s64>(size, 0x200 - unaligned_size);
|
||||
std::memcpy(buf, data + unaligned_size, csize);
|
||||
std::memcpy(buf, data, unaligned_size);
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
@@ -867,7 +1040,7 @@ Result Menu::GcOnEvent(bool force) {
|
||||
log_write("trying to mount\n");
|
||||
m_mounted = R_SUCCEEDED(GcMount());
|
||||
if (m_mounted) {
|
||||
App::PlaySoundEffect(SoundEffect::SoundEffect_Startup);
|
||||
App::PlaySoundEffect(SoundEffect::Startup);
|
||||
}
|
||||
} else {
|
||||
log_write("trying to unmount\n");
|
||||
@@ -926,6 +1099,30 @@ void Menu::OnChangeIndex(s64 new_index) {
|
||||
}
|
||||
}
|
||||
|
||||
Result Menu::DumpXcz(u32 flags) {
|
||||
R_TRY(GcMountStorage());
|
||||
|
||||
auto source = std::make_shared<XciSource>();
|
||||
source->menu = this;
|
||||
source->application_name = m_entries[m_entry_index].lang_entry.name;
|
||||
source->icon = m_icon;
|
||||
|
||||
// todo: support for prepending cert area.
|
||||
std::vector<fs::FsPath> paths;
|
||||
source->xci_size = m_storage_trimmed_size;
|
||||
paths.emplace_back(BuildFullDumpPath(DumpFileType_XCZ, m_entries));
|
||||
|
||||
// todo: log keys error.
|
||||
keys::Keys keys;
|
||||
R_TRY(keys::parse_keys(keys, true));
|
||||
|
||||
dump::Dump(source, paths, [keys](ProgressBox* pbox, dump::BaseSource* source, dump::WriteSource* writer, const fs::FsPath& path) {
|
||||
return NszExport(pbox, keys, source, writer, path);
|
||||
});
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Menu::DumpGames(u32 flags) {
|
||||
// first, try and mount the storage.
|
||||
// this will fill out the xci header, verify and get sizes.
|
||||
@@ -986,7 +1183,17 @@ Result Menu::DumpGames(u32 flags) {
|
||||
paths.emplace_back(BuildFullDumpPath(DumpFileType_Initial, m_entries));
|
||||
}
|
||||
|
||||
dump::Dump(source, paths, [](Result){}, location_flags);
|
||||
if (0) {
|
||||
// todo: log keys error.
|
||||
keys::Keys keys;
|
||||
keys::parse_keys(keys, true);
|
||||
|
||||
dump::Dump(source, paths, [keys](ProgressBox* pbox, dump::BaseSource* source, dump::WriteSource* writer, const fs::FsPath& path) {
|
||||
return NszExport(pbox, keys, source, writer, path);
|
||||
});
|
||||
} else {
|
||||
dump::Dump(source, paths, nullptr, location_flags);
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
};
|
||||
@@ -1105,6 +1312,23 @@ Result Menu::GcGetSecurityInfo(GameCardSecurityInformation& out) {
|
||||
}
|
||||
|
||||
Result Menu::MountGcFs() {
|
||||
#if 1
|
||||
R_TRY(GcMountStorage());
|
||||
|
||||
const auto& e = m_entries[m_entry_index];
|
||||
auto source = std::make_shared<Test>(this);
|
||||
|
||||
fs::FsPath root;
|
||||
R_TRY(devoptab::MountXciSource(source, m_storage_trimmed_size, e.lang_entry.name, root));
|
||||
|
||||
auto fs = std::make_shared<filebrowser::FsStdioWrapper>(root, [root](){
|
||||
devoptab::UmountXci(root);
|
||||
});
|
||||
|
||||
filebrowser::MountFsHelper(fs, e.lang_entry.name);
|
||||
|
||||
#else
|
||||
// old code that only mounts secure partition.
|
||||
const auto& e = m_entries[m_entry_index];
|
||||
|
||||
auto fs = std::make_shared<fs::FsNative>(&m_fs->m_fs, false);
|
||||
@@ -1118,6 +1342,7 @@ Result Menu::MountGcFs() {
|
||||
};
|
||||
|
||||
App::Push<filebrowser::Menu>(fs, fs_entry, "/");
|
||||
#endif
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
|
||||
@@ -197,7 +197,7 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
if (touch && m_index == i) {
|
||||
FireAction(Button::A);
|
||||
} else {
|
||||
App::PlaySoundEffect(SoundEffect_Focus);
|
||||
App::PlaySoundEffect(SoundEffect::Focus);
|
||||
SetIndex(i);
|
||||
}
|
||||
});
|
||||
@@ -354,16 +354,16 @@ void DownloadEntries(const Entry& entry) {
|
||||
|
||||
PopupList::Items entry_items;
|
||||
for (const auto& e : gh_entries) {
|
||||
std::string str;
|
||||
std::string str = " [" + e.published_at.substr(0, 10) + "]";
|
||||
|
||||
if (!e.name.empty()) {
|
||||
str += e.name + " | ";
|
||||
str += " " + e.name;
|
||||
} else {
|
||||
str += e.tag_name + " | ";
|
||||
str += " " + e.tag_name;
|
||||
}
|
||||
if (e.prerelease) {
|
||||
str += " (Pre-Release)";
|
||||
}
|
||||
str += " [" + e.published_at.substr(0, 10) + "]";
|
||||
|
||||
entry_items.emplace_back(str);
|
||||
}
|
||||
@@ -396,8 +396,7 @@ void DownloadEntries(const Entry& entry) {
|
||||
}
|
||||
|
||||
if (!using_name || found) {
|
||||
std::string str = p.name + " | ";
|
||||
str += " [" + p.updated_at.substr(0, 10) + "]";
|
||||
std::string str = " [" + p.updated_at.substr(0, 10) + "]" + " " + p.name;
|
||||
|
||||
asset_items.emplace_back(str);
|
||||
api_assets.emplace_back(p);
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
#include "app.hpp"
|
||||
#include "log.hpp"
|
||||
#include "fs.hpp"
|
||||
|
||||
#include "ui/menus/homebrew.hpp"
|
||||
#include "ui/menus/filebrowser.hpp"
|
||||
|
||||
#include "ui/sidebar.hpp"
|
||||
#include "ui/error_box.hpp"
|
||||
#include "ui/option_box.hpp"
|
||||
#include "ui/progress_box.hpp"
|
||||
#include "ui/nvg_util.hpp"
|
||||
|
||||
#include "utils/devoptab.hpp"
|
||||
#include "utils/profile.hpp"
|
||||
|
||||
#include "owo.hpp"
|
||||
#include "defines.hpp"
|
||||
#include "i18n.hpp"
|
||||
@@ -84,7 +90,7 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
if (touch && m_index == i) {
|
||||
FireAction(Button::A);
|
||||
} else {
|
||||
App::PlaySoundEffect(SoundEffect_Focus);
|
||||
App::PlaySoundEffect(SoundEffect::Focus);
|
||||
SetIndex(i);
|
||||
}
|
||||
});
|
||||
@@ -192,10 +198,11 @@ void Menu::InstallHomebrew() {
|
||||
}
|
||||
|
||||
void Menu::ScanHomebrew() {
|
||||
TimeStamp ts;
|
||||
FreeEntries();
|
||||
nro_scan("/switch", m_entries);
|
||||
log_write("nros found: %zu time_taken: %.2f\n", m_entries.size(), ts.GetSecondsD());
|
||||
{
|
||||
SCOPED_TIMESTAMP("nro scan");
|
||||
nro_scan("/switch", m_entries);
|
||||
}
|
||||
|
||||
struct IniUser {
|
||||
std::vector<NroEntry>& entires;
|
||||
@@ -458,6 +465,13 @@ void Menu::DisplayOptions() {
|
||||
}, "Shows all hidden homebrew."_i18n);
|
||||
});
|
||||
|
||||
// for testing stuff.
|
||||
#if 0
|
||||
options->Add<SidebarEntrySlider>("Test", 1, 0, 2, 10, [](auto& v_out){
|
||||
|
||||
});
|
||||
#endif
|
||||
|
||||
if (!m_entries_current.empty()) {
|
||||
#if 0
|
||||
options->Add<SidebarEntryCallback>("Info"_i18n, [this](){
|
||||
@@ -472,14 +486,10 @@ void Menu::DisplayOptions() {
|
||||
}, "Hides the selected homebrew.\n\n"
|
||||
"To unhide homebrew, enable \"Show hidden\" in the sort options."_i18n);
|
||||
|
||||
auto mount_option = options->Add<SidebarEntryCallback>("Mount RomFS"_i18n, [this](){
|
||||
const auto rc = MountRomfsFs();
|
||||
App::PushErrorBox(rc, "Failed to mount NRO RomFS"_i18n);
|
||||
}, "Mounts the homebrew RomFS"_i18n);
|
||||
|
||||
mount_option->Depends([this](){
|
||||
return GetEntry().romfs_offset && GetEntry().romfs_size;
|
||||
}, "This homebrew does not have a RomFS"_i18n);
|
||||
options->Add<SidebarEntryCallback>("Mount NRO Fs"_i18n, [this](){
|
||||
const auto rc = MountNroFs();
|
||||
App::PushErrorBox(rc, "Failed to mount NRO FileSystem"_i18n);
|
||||
}, "Mounts the NRO FileSystem (icon, nacp and RomFS)."_i18n);
|
||||
|
||||
options->Add<SidebarEntryCallback>("Delete"_i18n, [this](){
|
||||
const auto buf = "Are you sure you want to delete "_i18n + GetEntry().path.toString() + "?";
|
||||
@@ -510,41 +520,17 @@ void Menu::DisplayOptions() {
|
||||
}
|
||||
}
|
||||
|
||||
struct NroRomFS final : fs::FsStdio {
|
||||
NroRomFS(const fs::FsPath& name, const fs::FsPath& root) : FsStdio{true, root}, m_name{name} {
|
||||
|
||||
}
|
||||
|
||||
~NroRomFS() {
|
||||
romfsUnmount(m_name);
|
||||
}
|
||||
|
||||
const fs::FsPath m_name;
|
||||
};
|
||||
|
||||
Result Menu::MountRomfsFs() {
|
||||
static const char* name = "nro_romfs";
|
||||
static const char* root = "nro_romfs:/";
|
||||
Result Menu::MountNroFs() {
|
||||
const auto& e = GetEntry();
|
||||
|
||||
// todo: add errors for when nro doesn't have romfs.
|
||||
R_UNLESS(e.romfs_offset, 0x1);
|
||||
R_UNLESS(e.romfs_size, 0x1);
|
||||
fs::FsPath root;
|
||||
R_TRY(devoptab::MountNro(App::GetApp()->m_fs.get(), e.path, root));
|
||||
|
||||
FsFile file;
|
||||
R_TRY(fsFsOpenFile(fsdevGetDeviceFileSystem("sdmc"), e.path, FsOpenMode_Read, &file));
|
||||
|
||||
const auto rc = romfsMountFromFile(file, e.romfs_offset, name);
|
||||
if (R_FAILED(rc)) {
|
||||
fsFileClose(&file);
|
||||
R_THROW(rc);
|
||||
}
|
||||
|
||||
auto fs = std::make_shared<filebrowser::FsStdioWrapper>(root, [](){
|
||||
romfsUnmount(name);
|
||||
auto fs = std::make_shared<filebrowser::FsStdioWrapper>(root, [root](){
|
||||
devoptab::UmountNro(root);
|
||||
});
|
||||
|
||||
filebrowser::MountFsHelper(fs, e.GetName());
|
||||
filebrowser::MountFsHelper(fs, root);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
|
||||
134
sphaira/source/ui/menus/image_viewer.cpp
Normal file
134
sphaira/source/ui/menus/image_viewer.cpp
Normal 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
|
||||
@@ -1,5 +1,3 @@
|
||||
#if ENABLE_NETWORK_INSTALL
|
||||
|
||||
#include "ui/menus/install_stream_menu_base.hpp"
|
||||
#include "yati/yati.hpp"
|
||||
#include "app.hpp"
|
||||
@@ -275,5 +273,3 @@ void Menu::OnInstallClose() {
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui::menu::stream
|
||||
|
||||
#endif
|
||||
|
||||
@@ -67,16 +67,17 @@ const MiscMenuEntry MISC_MENU_ENTRIES[] = {
|
||||
"You can backup and restore saves.\n\n"\
|
||||
"Experimental support for backing up system saves is possible." },
|
||||
|
||||
#if 0
|
||||
{ .name = "Themezer", .title = "Themezer", .func = MiscMenuFuncGenerator<ui::menu::themezer::Menu>, .flag = MiscMenuFlag_Shortcut, .info =
|
||||
"Download themes from themezer.net. "\
|
||||
"Themes are downloaded to /themes/sphaira\n"\
|
||||
"To install the themes, NXThemesInstaller needs to be installed (can be downloaded via the AppStore)." },
|
||||
#endif
|
||||
|
||||
{ .name = "GitHub", .title = "GitHub", .func = MiscMenuFuncGenerator<ui::menu::gh::Menu>, .flag = MiscMenuFlag_Shortcut, .info =
|
||||
"Download releases directly from GitHub. "\
|
||||
"Custom entries can be added to /config/sphaira/github" },
|
||||
|
||||
#if ENABLE_NETWORK_INSTALL
|
||||
{ .name = "FTP", .title = "FTP Install", .func = MiscMenuFuncGenerator<ui::menu::ftp::Menu>, .flag = MiscMenuFlag_Install, .info =
|
||||
"Install apps via FTP.\n\n"\
|
||||
"NOTE: This feature does not always work, use at your own risk. "\
|
||||
@@ -93,7 +94,6 @@ const MiscMenuEntry MISC_MENU_ENTRIES[] = {
|
||||
"NOTE: This feature does not always work, use at your own risk. "\
|
||||
"If you encounter an issue, do not open an issue, it will not be fixed." },
|
||||
|
||||
#endif
|
||||
{ .name = "GameCard", .title = "GameCard", .func = MiscMenuFuncGenerator<ui::menu::gc::Menu>, .flag = MiscMenuFlag_Shortcut, .info =
|
||||
"View info on the inserted Game Card (GC). "\
|
||||
"You can backup and install the inserted GC. "\
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#if ENABLE_NETWORK_INSTALL
|
||||
|
||||
#include "ui/menus/mtp_menu.hpp"
|
||||
#include "usb/usbds.hpp"
|
||||
#include "app.hpp"
|
||||
@@ -12,10 +10,10 @@
|
||||
namespace sphaira::ui::menu::mtp {
|
||||
|
||||
Menu::Menu(u32 flags) : stream::Menu{"MTP Install"_i18n, flags} {
|
||||
m_was_mtp_enabled = App::GetMtpEnable();
|
||||
m_was_mtp_enabled = haze::IsInit();
|
||||
if (!m_was_mtp_enabled) {
|
||||
log_write("[MTP] wasn't enabled, forcefully enabling\n");
|
||||
App::SetMtpEnable(true);
|
||||
haze::Init();
|
||||
}
|
||||
|
||||
haze::InitInstallMode(
|
||||
@@ -31,7 +29,7 @@ Menu::~Menu() {
|
||||
|
||||
if (!m_was_mtp_enabled) {
|
||||
log_write("[MTP] disabling on exit\n");
|
||||
App::SetMtpEnable(false);
|
||||
haze::Exit();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,5 +57,3 @@ void Menu::OnDisableInstallMode() {
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui::menu::mtp
|
||||
|
||||
#endif
|
||||
|
||||
@@ -42,6 +42,42 @@ constexpr const char* NX_SAVE_META_NAME = ".nx_save_meta.bin";
|
||||
|
||||
constinit UEvent g_change_uevent;
|
||||
|
||||
struct DumpSource final : dump::BaseSource {
|
||||
DumpSource(std::span<const std::reference_wrapper<Entry>> entries, std::span<const fs::FsPath> paths)
|
||||
: m_entries{entries}
|
||||
, m_paths{paths} {
|
||||
|
||||
}
|
||||
|
||||
Result Read(const std::string& path, void* buf, s64 off, s64 size, u64* bytes_read) override {
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
auto GetName(const std::string& path) const -> std::string override {
|
||||
return GetEntry(path).GetName();
|
||||
}
|
||||
|
||||
auto GetSize(const std::string& path) const -> s64 override {
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto GetIcon(const std::string& path) const -> int override {
|
||||
return GetEntry(path).image;
|
||||
}
|
||||
|
||||
auto GetEntry(const std::string& path) const -> Entry& {
|
||||
const auto itr = std::ranges::find_if(m_paths, [&path](auto& e){
|
||||
return path == e;
|
||||
});
|
||||
const auto index = std::distance(m_paths.begin(), itr);
|
||||
return m_entries[index];
|
||||
}
|
||||
|
||||
private:
|
||||
std::span<const std::reference_wrapper<Entry>> m_entries;
|
||||
std::span<const fs::FsPath> m_paths;
|
||||
};
|
||||
|
||||
// https://github.com/J-D-K/JKSV/issues/264#issuecomment-2618962807
|
||||
struct NXSaveMeta {
|
||||
u32 magic{}; // NX_SAVE_META_MAGIC
|
||||
@@ -256,18 +292,6 @@ void LoadControlEntry(Entry& e, bool force_image_load = false) {
|
||||
}
|
||||
}
|
||||
|
||||
struct HashStr {
|
||||
char str[0x21];
|
||||
};
|
||||
|
||||
HashStr hexIdToStr(auto id) {
|
||||
HashStr str{};
|
||||
const auto id_lower = std::byteswap(*(u64*)id.c);
|
||||
const auto id_upper = std::byteswap(*(u64*)(id.c + 0x8));
|
||||
std::snprintf(str.str, 0x21, "%016lx%016lx", id_lower, id_upper);
|
||||
return str;
|
||||
}
|
||||
|
||||
auto BuildSaveName(const Entry& e) -> fs::FsPath {
|
||||
fs::FsPath name_buf = e.GetName();
|
||||
title::utilsReplaceIllegalCharacters(name_buf, true);
|
||||
@@ -389,7 +413,7 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
if (touch && m_index == i) {
|
||||
FireAction(Button::A);
|
||||
} else {
|
||||
App::PlaySoundEffect(SoundEffect_Focus);
|
||||
App::PlaySoundEffect(SoundEffect::Focus);
|
||||
SetIndex(i);
|
||||
}
|
||||
});
|
||||
@@ -691,14 +715,9 @@ void Menu::DisplayOptions() {
|
||||
}
|
||||
|
||||
void Menu::BackupSaves(std::vector<std::reference_wrapper<Entry>>& entries) {
|
||||
dump::DumpGetLocation("Select backup location"_i18n, dump::DumpLocationFlag_SdCard|dump::DumpLocationFlag_Stdio, [this, entries](const dump::DumpLocation& location){
|
||||
dump::DumpGetLocation("Select backup location"_i18n, dump::DumpLocationFlag_SdCard|dump::DumpLocationFlag_Stdio|dump::DumpLocationFlag_Usb, [this, entries](const dump::DumpLocation& location){
|
||||
App::Push<ProgressBox>(0, "Backup"_i18n, "", [this, entries, location](auto pbox) -> Result {
|
||||
for (auto& e : entries) {
|
||||
// the entry may not have loaded yet.
|
||||
LoadControlEntry(e);
|
||||
R_TRY(BackupSaveInternal(pbox, location, e, m_compress_save_backup.Get()));
|
||||
}
|
||||
R_SUCCEED();
|
||||
return BackupSaveInternal(pbox, location, entries, m_compress_save_backup.Get());
|
||||
}, [](Result rc){
|
||||
App::PushErrorBox(rc, "Backup failed!"_i18n);
|
||||
|
||||
@@ -936,172 +955,167 @@ Result Menu::RestoreSaveInternal(ProgressBox* pbox, const Entry& e, const fs::Fs
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Menu::BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& location, const Entry& e, bool compressed, bool is_auto) const {
|
||||
std::unique_ptr<fs::Fs> fs;
|
||||
if (location.entry.type == dump::DumpLocationType_Stdio) {
|
||||
fs = std::make_unique<fs::FsStdio>(true, location.stdio[location.entry.index].mount);
|
||||
} else if (location.entry.type == dump::DumpLocationType_SdCard) {
|
||||
fs = std::make_unique<fs::FsNativeSd>();
|
||||
} else {
|
||||
std::unreachable();
|
||||
Result Menu::BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& location, std::span<const std::reference_wrapper<Entry>> entries, bool compressed, bool is_auto) const {
|
||||
std::vector<fs::FsPath> paths;
|
||||
for (auto& e : entries) {
|
||||
// ensure that we have title name and icon loaded.
|
||||
LoadControlEntry(e);
|
||||
paths.emplace_back(BuildSavePath(e, is_auto));
|
||||
}
|
||||
|
||||
pbox->SetTitle(e.GetName());
|
||||
if (e.image) {
|
||||
pbox->SetImage(e.image);
|
||||
} else if (auto data = title::Get(e.application_id); data && !data->icon.empty()) {
|
||||
pbox->SetImageDataConst(data->icon);
|
||||
} else {
|
||||
pbox->SetImage(0);
|
||||
}
|
||||
auto source = std::make_shared<DumpSource>(entries, paths);
|
||||
|
||||
const auto save_data_space_id = (FsSaveDataSpaceId)e.save_data_space_id;
|
||||
return dump::Dump(pbox, source, location, paths, [&](ui::ProgressBox* pbox, dump::BaseSource* _source, dump::WriteSource* writer, const fs::FsPath& path) -> Result {
|
||||
const auto source = (DumpSource*)_source;
|
||||
const auto& e = source->GetEntry(path);
|
||||
|
||||
// try and get the journal and data size.
|
||||
FsSaveDataExtraData extra{};
|
||||
R_TRY(fsReadSaveDataFileSystemExtraDataBySaveDataSpaceId(&extra, sizeof(extra), save_data_space_id, e.save_data_id));
|
||||
pbox->SetTitle(e.GetName());
|
||||
if (e.image) {
|
||||
pbox->SetImage(e.image);
|
||||
} else if (auto data = title::Get(e.application_id); data && !data->icon.empty()) {
|
||||
pbox->SetImageDataConst(data->icon);
|
||||
} else {
|
||||
pbox->SetImage(0);
|
||||
}
|
||||
|
||||
FsSaveDataAttribute attr{};
|
||||
attr.application_id = e.application_id;
|
||||
attr.uid = e.uid;
|
||||
attr.system_save_data_id = e.system_save_data_id;
|
||||
attr.save_data_type = e.save_data_type;
|
||||
attr.save_data_rank = e.save_data_rank;
|
||||
attr.save_data_index = e.save_data_index;
|
||||
const auto save_data_space_id = (FsSaveDataSpaceId)e.save_data_space_id;
|
||||
|
||||
// try and open the save file system
|
||||
fs::FsNativeSave save_fs{(FsSaveDataType)e.save_data_type, save_data_space_id, &attr, true};
|
||||
R_TRY(save_fs.GetFsOpenResult());
|
||||
// try and get the journal and data size.
|
||||
FsSaveDataExtraData extra{};
|
||||
R_TRY(fsReadSaveDataFileSystemExtraDataBySaveDataSpaceId(&extra, sizeof(extra), save_data_space_id, e.save_data_id));
|
||||
|
||||
// get a list of collections.
|
||||
filebrowser::FsDirCollections collections;
|
||||
R_TRY(filebrowser::FsView::get_collections(&save_fs, "/", "", collections));
|
||||
FsSaveDataAttribute attr{};
|
||||
attr.application_id = e.application_id;
|
||||
attr.uid = e.uid;
|
||||
attr.system_save_data_id = e.system_save_data_id;
|
||||
attr.save_data_type = e.save_data_type;
|
||||
attr.save_data_rank = e.save_data_rank;
|
||||
attr.save_data_index = e.save_data_index;
|
||||
|
||||
// the save file may be empty, this isn't an error, but we exit early.
|
||||
R_UNLESS(!collections.empty(), 0x0);
|
||||
// try and open the save file system
|
||||
fs::FsNativeSave save_fs{(FsSaveDataType)e.save_data_type, save_data_space_id, &attr, true};
|
||||
R_TRY(save_fs.GetFsOpenResult());
|
||||
|
||||
const auto t = (time_t)extra.timestamp;
|
||||
const auto tm = std::localtime(&t);
|
||||
// get a list of collections.
|
||||
filebrowser::FsDirCollections collections;
|
||||
R_TRY(filebrowser::FsView::get_collections(&save_fs, "/", "", collections));
|
||||
|
||||
// pre-calculate the time rather than calculate it in the loop.
|
||||
zip_fileinfo zip_info_default{};
|
||||
zip_info_default.tmz_date.tm_sec = tm->tm_sec;
|
||||
zip_info_default.tmz_date.tm_min = tm->tm_min;
|
||||
zip_info_default.tmz_date.tm_hour = tm->tm_hour;
|
||||
zip_info_default.tmz_date.tm_mday = tm->tm_mday;
|
||||
zip_info_default.tmz_date.tm_mon = tm->tm_mon;
|
||||
zip_info_default.tmz_date.tm_year = tm->tm_year;
|
||||
// the save file may be empty, this isn't an error, but we exit early.
|
||||
R_UNLESS(!collections.empty(), 0x0);
|
||||
|
||||
const auto path = fs::AppendPath(fs->Root(), BuildSavePath(e, is_auto));
|
||||
const auto temp_path = path + ".temp";
|
||||
const auto t = (time_t)extra.timestamp;
|
||||
const auto tm = std::localtime(&t);
|
||||
|
||||
fs->CreateDirectoryRecursivelyWithPath(temp_path);
|
||||
ON_SCOPE_EXIT(fs->DeleteFile(temp_path));
|
||||
// pre-calculate the time rather than calculate it in the loop.
|
||||
zip_fileinfo zip_info_default{};
|
||||
zip_info_default.tmz_date.tm_sec = tm->tm_sec;
|
||||
zip_info_default.tmz_date.tm_min = tm->tm_min;
|
||||
zip_info_default.tmz_date.tm_hour = tm->tm_hour;
|
||||
zip_info_default.tmz_date.tm_mday = tm->tm_mday;
|
||||
zip_info_default.tmz_date.tm_mon = tm->tm_mon;
|
||||
zip_info_default.tmz_date.tm_year = tm->tm_year;
|
||||
|
||||
// zip to memory if less than 1GB and not applet mode.
|
||||
// TODO: use my mmz code from ftpsrv to stream zip creation.
|
||||
// this will allow for zipping to memory and flushing every X bytes
|
||||
// such as flushing every 8MB.
|
||||
const auto file_download = App::IsApplet() || e.size >= 1024ULL * 1024ULL * 1024ULL;
|
||||
// zip to memory if less than 1GB and not applet mode.
|
||||
// TODO: use my mmz code from ftpsrv to stream zip creation.
|
||||
// this will allow for zipping to memory and flushing every X bytes
|
||||
// such as flushing every 8MB.
|
||||
const auto file_download = App::IsApplet() || e.size >= 1024ULL * 1024ULL * 1024ULL;
|
||||
|
||||
mz::MzMem mz_mem{};
|
||||
zlib_filefunc64_def file_func;
|
||||
if (!file_download) {
|
||||
mz::FileFuncMem(&mz_mem, &file_func);
|
||||
} else {
|
||||
mz::FileFuncStdio(&file_func);
|
||||
}
|
||||
mz::MzMem mz_mem{};
|
||||
zlib_filefunc64_def file_func;
|
||||
if (!file_download) {
|
||||
mz::FileFuncMem(&mz_mem, &file_func);
|
||||
} else {
|
||||
dump::FileFuncWriter(writer, &file_func);
|
||||
}
|
||||
|
||||
{
|
||||
auto zfile = zipOpen2_64(temp_path, APPEND_STATUS_CREATE, nullptr, &file_func);
|
||||
R_UNLESS(zfile, Result_ZipOpen2_64);
|
||||
ON_SCOPE_EXIT(zipClose(zfile, "sphaira v" APP_VERSION_HASH));
|
||||
|
||||
// add save meta.
|
||||
{
|
||||
const NXSaveMeta meta{
|
||||
.magic = NX_SAVE_META_MAGIC,
|
||||
.version = NX_SAVE_META_VERSION,
|
||||
.attr = extra.attr,
|
||||
.owner_id = extra.owner_id,
|
||||
.timestamp = extra.timestamp,
|
||||
.flags = extra.flags,
|
||||
.unk_x54 = extra.unk_x54,
|
||||
.data_size = extra.data_size,
|
||||
.journal_size = extra.journal_size,
|
||||
.commit_id = extra.commit_id,
|
||||
.raw_size = e.size,
|
||||
auto zfile = zipOpen2_64(path, APPEND_STATUS_CREATE, nullptr, &file_func);
|
||||
R_UNLESS(zfile, Result_ZipOpen2_64);
|
||||
ON_SCOPE_EXIT(zipClose(zfile, "sphaira v" APP_VERSION_HASH));
|
||||
|
||||
// add save meta.
|
||||
{
|
||||
const NXSaveMeta meta{
|
||||
.magic = NX_SAVE_META_MAGIC,
|
||||
.version = NX_SAVE_META_VERSION,
|
||||
.attr = extra.attr,
|
||||
.owner_id = extra.owner_id,
|
||||
.timestamp = extra.timestamp,
|
||||
.flags = extra.flags,
|
||||
.unk_x54 = extra.unk_x54,
|
||||
.data_size = extra.data_size,
|
||||
.journal_size = extra.journal_size,
|
||||
.commit_id = extra.commit_id,
|
||||
.raw_size = e.size,
|
||||
};
|
||||
|
||||
R_UNLESS(ZIP_OK == zipOpenNewFileInZip(zfile, NX_SAVE_META_NAME, &zip_info_default, NULL, 0, NULL, 0, NULL, Z_DEFLATED, Z_NO_COMPRESSION), Result_ZipOpenNewFileInZip);
|
||||
ON_SCOPE_EXIT(zipCloseFileInZip(zfile));
|
||||
R_UNLESS(ZIP_OK == zipWriteInFileInZip(zfile, &meta, sizeof(meta)), Result_ZipWriteInFileInZip);
|
||||
}
|
||||
|
||||
const auto zip_add = [&](const fs::FsPath& file_path) -> Result {
|
||||
const char* file_name_in_zip = file_path.s;
|
||||
|
||||
// strip root path (/ or ums0:)
|
||||
if (!std::strncmp(file_name_in_zip, save_fs.Root(), std::strlen(save_fs.Root()))) {
|
||||
file_name_in_zip += std::strlen(save_fs.Root());
|
||||
}
|
||||
|
||||
// root paths are banned in zips, they will warn when extracting otherwise.
|
||||
while (file_name_in_zip[0] == '/') {
|
||||
file_name_in_zip++;
|
||||
}
|
||||
|
||||
pbox->NewTransfer(file_name_in_zip);
|
||||
|
||||
const auto level = compressed ? Z_DEFAULT_COMPRESSION : Z_NO_COMPRESSION;
|
||||
if (ZIP_OK != zipOpenNewFileInZip(zfile, file_name_in_zip, &zip_info_default, NULL, 0, NULL, 0, NULL, Z_DEFLATED, level)) {
|
||||
log_write("failed to add zip for %s\n", file_path.s);
|
||||
R_THROW(Result_ZipOpenNewFileInZip);
|
||||
}
|
||||
ON_SCOPE_EXIT(zipCloseFileInZip(zfile));
|
||||
|
||||
return thread::TransferZip(pbox, zfile, &save_fs, file_path);
|
||||
};
|
||||
|
||||
R_UNLESS(ZIP_OK == zipOpenNewFileInZip(zfile, NX_SAVE_META_NAME, &zip_info_default, NULL, 0, NULL, 0, NULL, Z_DEFLATED, Z_NO_COMPRESSION), Result_ZipOpenNewFileInZip);
|
||||
ON_SCOPE_EXIT(zipCloseFileInZip(zfile));
|
||||
R_UNLESS(ZIP_OK == zipWriteInFileInZip(zfile, &meta, sizeof(meta)), Result_ZipWriteInFileInZip);
|
||||
}
|
||||
|
||||
const auto zip_add = [&](const fs::FsPath& file_path) -> Result {
|
||||
const char* file_name_in_zip = file_path.s;
|
||||
|
||||
// strip root path (/ or ums0:)
|
||||
if (!std::strncmp(file_name_in_zip, save_fs.Root(), std::strlen(save_fs.Root()))) {
|
||||
file_name_in_zip += std::strlen(save_fs.Root());
|
||||
}
|
||||
|
||||
// root paths are banned in zips, they will warn when extracting otherwise.
|
||||
while (file_name_in_zip[0] == '/') {
|
||||
file_name_in_zip++;
|
||||
}
|
||||
|
||||
pbox->NewTransfer(file_name_in_zip);
|
||||
|
||||
const auto level = compressed ? Z_DEFAULT_COMPRESSION : Z_NO_COMPRESSION;
|
||||
if (ZIP_OK != zipOpenNewFileInZip(zfile, file_name_in_zip, &zip_info_default, NULL, 0, NULL, 0, NULL, Z_DEFLATED, level)) {
|
||||
log_write("failed to add zip for %s\n", file_path.s);
|
||||
R_THROW(Result_ZipOpenNewFileInZip);
|
||||
}
|
||||
ON_SCOPE_EXIT(zipCloseFileInZip(zfile));
|
||||
|
||||
return thread::TransferZip(pbox, zfile, &save_fs, file_path);
|
||||
};
|
||||
|
||||
// loop through every save file and store to zip.
|
||||
for (const auto& collection : collections) {
|
||||
for (const auto& file : collection.files) {
|
||||
const auto file_path = fs::AppendPath(collection.path, file.name);
|
||||
R_TRY(zip_add(file_path));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if we dumped the save to ram, flush the data to file.
|
||||
const auto is_file_based_emummc = App::IsFileBaseEmummc();
|
||||
if (!file_download) {
|
||||
pbox->NewTransfer("Flushing zip to file");
|
||||
R_TRY(fs->CreateFile(temp_path, mz_mem.buf.size(), 0));
|
||||
|
||||
fs::File file;
|
||||
R_TRY(fs->OpenFile(temp_path, FsOpenMode_Write, &file));
|
||||
|
||||
R_TRY(thread::Transfer(pbox, mz_mem.buf.size(),
|
||||
[&](void* data, s64 off, s64 size, u64* bytes_read) -> Result {
|
||||
size = std::min<s64>(size, mz_mem.buf.size() - off);
|
||||
std::memcpy(data, mz_mem.buf.data() + off, size);
|
||||
*bytes_read = size;
|
||||
R_SUCCEED();
|
||||
},
|
||||
[&](const void* data, s64 off, s64 size) -> Result {
|
||||
const auto rc = file.Write(off, data, size, FsWriteOption_None);
|
||||
if (is_file_based_emummc) {
|
||||
svcSleepThread(2e+6); // 2ms
|
||||
// loop through every save file and store to zip.
|
||||
for (const auto& collection : collections) {
|
||||
for (const auto& file : collection.files) {
|
||||
const auto file_path = fs::AppendPath(collection.path, file.name);
|
||||
R_TRY(zip_add(file_path));
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fs->DeleteFile(path);
|
||||
R_TRY(fs->RenameFile(temp_path, path));
|
||||
// if we dumped the save to ram, flush the data to file.
|
||||
if (!file_download) {
|
||||
pbox->NewTransfer("Flushing zip to file");
|
||||
R_TRY(writer->SetSize(mz_mem.buf.size()));
|
||||
|
||||
R_SUCCEED();
|
||||
R_TRY(thread::Transfer(pbox, mz_mem.buf.size(),
|
||||
[&](void* data, s64 off, s64 size, u64* bytes_read) -> Result {
|
||||
size = std::min<s64>(size, mz_mem.buf.size() - off);
|
||||
std::memcpy(data, mz_mem.buf.data() + off, size);
|
||||
*bytes_read = size;
|
||||
R_SUCCEED();
|
||||
},
|
||||
[&](const void* data, s64 off, s64 size) -> Result {
|
||||
return writer->Write(data, off, size);
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
});
|
||||
}
|
||||
|
||||
Result Menu::BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& location, Entry& e, bool compressed, bool is_auto) const {
|
||||
std::vector<std::reference_wrapper<Entry>> entries;
|
||||
entries.emplace_back(e);
|
||||
|
||||
return BackupSaveInternal(pbox, location, entries, compressed, is_auto);
|
||||
}
|
||||
|
||||
Result Menu::MountSaveFs() {
|
||||
@@ -1109,7 +1123,7 @@ Result Menu::MountSaveFs() {
|
||||
|
||||
if (e.system_save_data_id) {
|
||||
fs::FsPath root;
|
||||
R_TRY(devoptab::MountFromSavePath(e.system_save_data_id, root));
|
||||
R_TRY(devoptab::MountSaveSystem(e.system_save_data_id, root));
|
||||
|
||||
auto fs = std::make_shared<filebrowser::FsStdioWrapper>(root, [&e](){
|
||||
devoptab::UnmountSave(e.system_save_data_id);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#if 0
|
||||
#include "ui/menus/themezer.hpp"
|
||||
#include "ui/menus/ghdl.hpp"
|
||||
#include "ui/progress_box.hpp"
|
||||
@@ -444,7 +445,7 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
if (touch && m_index == i) {
|
||||
FireAction(Button::A);
|
||||
} else {
|
||||
App::PlaySoundEffect(SoundEffect_Focus);
|
||||
App::PlaySoundEffect(SoundEffect::Focus);
|
||||
SetIndex(i);
|
||||
}
|
||||
});
|
||||
@@ -723,3 +724,4 @@ void Menu::DisplayOptions() {
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui::menu::themezer
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#if ENABLE_NETWORK_INSTALL
|
||||
|
||||
#include "ui/menus/usb_menu.hpp"
|
||||
#include "yati/yati.hpp"
|
||||
#include "app.hpp"
|
||||
@@ -7,14 +5,18 @@
|
||||
#include "log.hpp"
|
||||
#include "ui/nvg_util.hpp"
|
||||
#include "i18n.hpp"
|
||||
#include "haze_helper.hpp"
|
||||
|
||||
#include "utils/thread.hpp"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace sphaira::ui::menu::usb {
|
||||
namespace {
|
||||
|
||||
constexpr u64 CONNECTION_TIMEOUT = UINT64_MAX;
|
||||
constexpr u64 TRANSFER_TIMEOUT = UINT64_MAX;
|
||||
constexpr u64 FINISHED_TIMEOUT = 1e+9 * 3; // 3 seconds.
|
||||
constexpr u64 CONNECTION_TIMEOUT = 3e+9;
|
||||
constexpr u64 TRANSFER_TIMEOUT = 3e+9;
|
||||
constexpr u64 FINISHED_TIMEOUT = 3e+9; // 3 seconds.
|
||||
|
||||
void thread_func(void* user) {
|
||||
auto app = static_cast<Menu*>(user);
|
||||
@@ -33,10 +35,10 @@ Menu::Menu(u32 flags) : MenuBase{"USB"_i18n, flags} {
|
||||
}});
|
||||
|
||||
// if mtp is enabled, disable it for now.
|
||||
m_was_mtp_enabled = App::GetMtpEnable();
|
||||
m_was_mtp_enabled = haze::IsInit();
|
||||
if (m_was_mtp_enabled) {
|
||||
App::Notify("Disable MTP for usb install"_i18n);
|
||||
App::SetMtpEnable(false);
|
||||
haze::Exit();
|
||||
}
|
||||
|
||||
// 3 second timeout for transfers.
|
||||
@@ -47,15 +49,18 @@ Menu::Menu(u32 flags) : MenuBase{"USB"_i18n, flags} {
|
||||
}
|
||||
|
||||
if (m_state != State::Failed) {
|
||||
threadCreate(&m_thread, thread_func, this, nullptr, 1024*32, PRIO_PREEMPTIVE, 1);
|
||||
utils::CreateThread(&m_thread, thread_func, this, 1024*32);
|
||||
threadStart(&m_thread);
|
||||
}
|
||||
}
|
||||
|
||||
Menu::~Menu() {
|
||||
// signal for thread to exit and wait.
|
||||
m_stop_source.request_stop();
|
||||
m_usb_source->SignalCancel();
|
||||
if (R_FAILED(waitSingleHandle(m_thread.handle, 0))) {
|
||||
m_usb_source->SignalCancel();
|
||||
m_stop_source.request_stop();
|
||||
}
|
||||
|
||||
threadWaitForExit(&m_thread);
|
||||
threadClose(&m_thread);
|
||||
|
||||
@@ -65,7 +70,7 @@ Menu::~Menu() {
|
||||
|
||||
if (m_was_mtp_enabled) {
|
||||
App::Notify("Re-enabled MTP"_i18n);
|
||||
App::SetMtpEnable(true);
|
||||
haze::Init();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,20 +97,23 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
m_state = State::Progress;
|
||||
log_write("got connection\n");
|
||||
App::Push<ui::ProgressBox>(0, "Installing "_i18n, "", [this](auto pbox) -> Result {
|
||||
ON_SCOPE_EXIT(m_usb_source->Finished(FINISHED_TIMEOUT));
|
||||
|
||||
// if we are doing s2s install, skip verifying the nca contents.
|
||||
yati::ConfigOverride config_override{};
|
||||
if (m_usb_source->IsStream()) {
|
||||
config_override.skip_nca_hash_verify = true;
|
||||
config_override.skip_rsa_header_fixed_key_verify = true;
|
||||
config_override.skip_rsa_npdm_fixed_key_verify = true;
|
||||
}
|
||||
|
||||
log_write("inside progress box\n");
|
||||
for (const auto& file_name : m_names) {
|
||||
for (u32 i = 0; i < std::size(m_names); i++) {
|
||||
const auto& file_name = m_names[i];
|
||||
|
||||
s64 file_size;
|
||||
R_TRY(m_usb_source->OpenFile(i, file_size));
|
||||
ON_SCOPE_EXIT(m_usb_source->CloseFile());
|
||||
|
||||
// if we are doing s2s install, skip verifying the nca contents.
|
||||
yati::ConfigOverride config_override{};
|
||||
if (m_usb_source->IsStream()) {
|
||||
config_override.skip_nca_hash_verify = true;
|
||||
config_override.skip_rsa_header_fixed_key_verify = true;
|
||||
config_override.skip_rsa_npdm_fixed_key_verify = true;
|
||||
}
|
||||
|
||||
pbox->SetTitle(file_name);
|
||||
m_usb_source->SetFileNameForTranfser(file_name);
|
||||
const auto rc = yati::InstallFromSource(pbox, m_usb_source.get(), file_name, config_override);
|
||||
if (R_FAILED(rc)) {
|
||||
m_usb_source->SignalCancel();
|
||||
@@ -191,5 +199,3 @@ void Menu::ThreadFunction() {
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui::menu::usb
|
||||
|
||||
#endif
|
||||
|
||||
277
sphaira/source/ui/music_player.cpp
Normal file
277
sphaira/source/ui/music_player.cpp
Normal 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
|
||||
@@ -6,7 +6,11 @@
|
||||
#include "log.hpp"
|
||||
#include "threaded_file_transfer.hpp"
|
||||
#include "i18n.hpp"
|
||||
|
||||
#include "utils/thread.hpp"
|
||||
|
||||
#include <cstring>
|
||||
#include <cmath>
|
||||
|
||||
namespace sphaira::ui {
|
||||
namespace {
|
||||
@@ -17,13 +21,44 @@ void threadFunc(void* arg) {
|
||||
d->pbox->RequestExit();
|
||||
}
|
||||
|
||||
// https://github.com/memononen/nanovg/blob/f93799c078fa11ed61c078c65a53914c8782c00b/example/demo.c#L500
|
||||
void drawSpinner(NVGcontext* vg, Theme* theme, float cx, float cy, float r, float t)
|
||||
{
|
||||
float a0 = 0.0f + t*6;
|
||||
float a1 = NVG_PI + t*6;
|
||||
float r0 = r;
|
||||
float r1 = r * 0.75f;
|
||||
float ax,ay, bx,by;
|
||||
NVGpaint paint;
|
||||
|
||||
nvgSave(vg);
|
||||
|
||||
auto colourb = theme->GetColour(ThemeEntryID_PROGRESSBAR);
|
||||
colourb.a = 0.5;
|
||||
|
||||
nvgBeginPath(vg);
|
||||
nvgArc(vg, cx,cy, r0, a0, a1, NVG_CW);
|
||||
nvgArc(vg, cx,cy, r1, a1, a0, NVG_CCW);
|
||||
nvgClosePath(vg);
|
||||
ax = cx + cosf(a0) * (r0+r1)*0.5f;
|
||||
ay = cy + sinf(a0) * (r0+r1)*0.5f;
|
||||
bx = cx + cosf(a1) * (r0+r1)*0.5f;
|
||||
by = cy + sinf(a1) * (r0+r1)*0.5f;
|
||||
paint = nvgLinearGradient(vg, ax,ay, bx,by, nvgRGBA(0,0,0,0), colourb);
|
||||
nvgFillPaint(vg, paint);
|
||||
nvgFill(vg);
|
||||
|
||||
nvgRestore(vg);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ProgressBox::ProgressBox(int image, const std::string& action, const std::string& title, const ProgressBoxCallback& callback, const ProgressBoxDoneCallback& done, int cpuid, int prio, int stack_size)
|
||||
ProgressBox::ProgressBox(int image, const std::string& action, const std::string& title, const ProgressBoxCallback& callback, const ProgressBoxDoneCallback& done)
|
||||
: m_done{done}
|
||||
, m_action{action}
|
||||
, m_title{title}
|
||||
, m_image{image} {
|
||||
App::SetAutoSleepDisabled(true);
|
||||
if (App::GetApp()->m_progress_boost_mode.Get()) {
|
||||
App::SetBoostMode(true);
|
||||
}
|
||||
@@ -45,10 +80,9 @@ ProgressBox::ProgressBox(int image, const std::string& action, const std::string
|
||||
// create cancel event.
|
||||
ueventCreate(&m_uevent, false);
|
||||
|
||||
m_cpuid = cpuid;
|
||||
m_thread_data.pbox = this;
|
||||
m_thread_data.callback = callback;
|
||||
if (R_FAILED(threadCreate(&m_thread, threadFunc, &m_thread_data, nullptr, stack_size, prio, cpuid))) {
|
||||
if (R_FAILED(utils::CreateThread(&m_thread, threadFunc, &m_thread_data))) {
|
||||
log_write("failed to create thead\n");
|
||||
}
|
||||
if (R_FAILED(threadStart(&m_thread))) {
|
||||
@@ -68,9 +102,12 @@ ProgressBox::~ProgressBox() {
|
||||
}
|
||||
|
||||
FreeImage();
|
||||
m_done(m_thread_data.result);
|
||||
if (m_done) {
|
||||
m_done(m_thread_data.result);
|
||||
}
|
||||
|
||||
App::SetBoostMode(false);
|
||||
App::SetAutoSleepDisabled(false);
|
||||
}
|
||||
|
||||
auto ProgressBox::Update(Controller* controller, TouchInfo* touch) -> void {
|
||||
@@ -122,7 +159,7 @@ auto ProgressBox::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||
const auto center_x = m_pos.x + m_pos.w/2;
|
||||
const auto end_y = m_pos.y + m_pos.h;
|
||||
const auto progress_bar_w = m_pos.w - 230;
|
||||
const Vec4 prog_bar = { center_x - progress_bar_w / 2, end_y - 100, progress_bar_w, 12 };
|
||||
const Vec4 prog_bar = { center_x - progress_bar_w / 2, end_y - 95, progress_bar_w, 12 };
|
||||
|
||||
nvgSave(vg);
|
||||
nvgIntersectScissor(vg, GetX(), GetY(), GetW(), GetH());
|
||||
@@ -140,7 +177,10 @@ auto ProgressBox::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||
gfx::drawRect(vg, prog_bar, theme->GetColour(ThemeEntryID_PROGRESSBAR_BACKGROUND), rounding);
|
||||
const u32 percentage = ((double)offset / (double)size) * 100.0;
|
||||
gfx::drawRect(vg, prog_bar.x, prog_bar.y, ((float)offset / (float)size) * prog_bar.w, prog_bar.h, theme->GetColour(ThemeEntryID_PROGRESSBAR), rounding);
|
||||
gfx::drawTextArgs(vg, prog_bar.x + prog_bar.w + pad, prog_bar.y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%u%%", percentage);
|
||||
gfx::drawTextArgs(vg, prog_bar.x + prog_bar.w + pad, prog_bar.y + prog_bar.h / 2, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT), "%u%%", percentage);
|
||||
|
||||
const auto rad = 15;
|
||||
drawSpinner(vg, theme, prog_bar.x - pad - rad, prog_bar.y + prog_bar.h / 2, rad, armTicksToNs(armGetSystemTick()) / 1e+9);
|
||||
|
||||
const double speed_mb = (double)speed / (1024.0 * 1024.0);
|
||||
const double speed_kb = (double)speed / (1024.0);
|
||||
@@ -219,6 +259,17 @@ auto ProgressBox::NewTransfer(const std::string& transfer) -> ProgressBox& {
|
||||
return *this;
|
||||
}
|
||||
|
||||
auto ProgressBox::ResetTranfser() -> ProgressBox& {
|
||||
mutexLock(&m_mutex);
|
||||
m_size = 0;
|
||||
m_offset = 0;
|
||||
m_last_offset = 0;
|
||||
m_timestamp.Update();
|
||||
mutexUnlock(&m_mutex);
|
||||
Yield();
|
||||
return *this;
|
||||
}
|
||||
|
||||
auto ProgressBox::UpdateTransfer(s64 offset, s64 size) -> ProgressBox& {
|
||||
mutexLock(&m_mutex);
|
||||
m_size = size;
|
||||
|
||||
@@ -27,7 +27,7 @@ ScrollableText::ScrollableText(const std::string& text, float x, float y, float
|
||||
}
|
||||
m_y_off -= m_step;
|
||||
m_index++;
|
||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
||||
App::PlaySoundEffect(SoundEffect::Scroll);
|
||||
}}),
|
||||
std::make_pair(Button::LS_UP, Action{[this](){
|
||||
if (m_y_off == m_y_off_base) {
|
||||
@@ -35,7 +35,7 @@ ScrollableText::ScrollableText(const std::string& text, float x, float y, float
|
||||
}
|
||||
m_y_off += m_step;
|
||||
m_index--;
|
||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
||||
App::PlaySoundEffect(SoundEffect::Scroll);
|
||||
}})
|
||||
);
|
||||
|
||||
|
||||
@@ -154,6 +154,52 @@ void SidebarEntryBool::Draw(NVGcontext* vg, Theme* theme, const Vec4& root_pos,
|
||||
SidebarEntryBase::DrawEntry(vg, theme, m_title, m_option ? m_true_str : m_false_str, m_option);
|
||||
}
|
||||
|
||||
SidebarEntrySlider::SidebarEntrySlider(const std::string& title, float value, float min, float max, int steps, const Callback& cb, const std::string& info)
|
||||
: SidebarEntryBase{title, info}
|
||||
, m_value{value}
|
||||
, m_min{min}
|
||||
, m_max{max}
|
||||
, m_steps{steps}
|
||||
, m_callback{cb} {
|
||||
SetAction(Button::LEFT, Action{[this](){
|
||||
if (!IsEnabled()) {
|
||||
DependsClick();
|
||||
} else {
|
||||
m_value = std::clamp(m_value - m_inc, m_min, m_max);
|
||||
// m_callback(m_option);
|
||||
} }
|
||||
});
|
||||
SetAction(Button::RIGHT, Action{[this](){
|
||||
if (!IsEnabled()) {
|
||||
DependsClick();
|
||||
} else {
|
||||
m_value = std::clamp(m_value + m_inc, m_min, m_max);
|
||||
// m_callback(m_option);
|
||||
} }
|
||||
});
|
||||
|
||||
m_duration = m_max - m_min;
|
||||
m_inc = m_duration / (float)(m_steps);
|
||||
}
|
||||
|
||||
void SidebarEntrySlider::Draw(NVGcontext* vg, Theme* theme, const Vec4& root_pos, bool left) {
|
||||
SidebarEntryBase::Draw(vg, theme, root_pos, left);
|
||||
|
||||
const float barh = 7;
|
||||
const Vec4 bar{m_pos.x + 15.f, m_pos.y + (m_pos.h / 2.f) - barh / 2, m_pos.w - 15.f * 2, barh};
|
||||
|
||||
gfx::drawRect(vg, bar, theme->GetColour(ThemeEntryID_PROGRESSBAR_BACKGROUND), 3);
|
||||
auto inner = bar;
|
||||
inner.w *= m_value / m_duration;
|
||||
gfx::drawRect(vg, inner, theme->GetColour(ThemeEntryID_PROGRESSBAR), 3);
|
||||
|
||||
for (int i = 0; i <= m_steps; i++) {
|
||||
const auto loop = m_inc * (float)i;
|
||||
const auto marker = Vec4{bar.x + (bar.w * loop / m_duration), bar.y - 4.f, 3.f, bar.h + 8.f};
|
||||
gfx::drawRect(vg, marker, theme->GetColour(ThemeEntryID_TEXT_INFO));
|
||||
}
|
||||
}
|
||||
|
||||
SidebarEntryCallback::SidebarEntryCallback(const std::string& title, const Callback& cb, bool pop_on_click, const std::string& info)
|
||||
: SidebarEntryBase{title, info}
|
||||
, m_callback{cb}
|
||||
@@ -422,6 +468,13 @@ void Sidebar::SetupButtons() {
|
||||
SetPop();
|
||||
}})
|
||||
);
|
||||
|
||||
// disable jump page if the item is using left/right buttons.
|
||||
if (HasAction(Button::LEFT) || HasAction(Button::RIGHT)) {
|
||||
m_list->SetPageJump(false);
|
||||
} else {
|
||||
m_list->SetPageJump(true);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui
|
||||
|
||||
@@ -33,7 +33,7 @@ void Widget::Update(Controller* controller, TouchInfo* touch) {
|
||||
for (const auto& [button, action] : m_actions) {
|
||||
if ((action.m_type & ActionType::DOWN) && controller->GotDown(button)) {
|
||||
if (static_cast<u64>(button) & static_cast<u64>(Button::ANY_BUTTON)) {
|
||||
App::PlaySoundEffect(SoundEffect_Focus);
|
||||
App::PlaySoundEffect(SoundEffect::Focus);
|
||||
}
|
||||
action.Invoke(true);
|
||||
break;
|
||||
@@ -83,7 +83,7 @@ void Widget::RemoveAction(Button button) {
|
||||
auto Widget::FireAction(Button b, u8 type) -> bool {
|
||||
for (const auto& [button, action] : m_actions) {
|
||||
if (button == b && (action.m_type & type)) {
|
||||
App::PlaySoundEffect(SoundEffect_Focus);
|
||||
App::PlaySoundEffect(SoundEffect::Focus);
|
||||
action.Invoke(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
|
||||
// The USB transfer code was taken from Haze (part of Atmosphere).
|
||||
|
||||
#if ENABLE_NETWORK_INSTALL
|
||||
|
||||
#include "usb/base.hpp"
|
||||
#include "log.hpp"
|
||||
#include "defines.hpp"
|
||||
@@ -100,5 +98,3 @@ Result Base::TransferAll(bool read, void *data, u32 size, u64 timeout) {
|
||||
}
|
||||
|
||||
} // namespace sphaira::usb
|
||||
|
||||
#endif
|
||||
|
||||
79
sphaira/source/usb/usb_dumper.cpp
Normal file
79
sphaira/source/usb/usb_dumper.cpp
Normal 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
|
||||
120
sphaira/source/usb/usb_installer.cpp
Normal file
120
sphaira/source/usb/usb_installer.cpp
Normal 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
|
||||
@@ -1,16 +1,11 @@
|
||||
// The USB protocol was taken from Tinfoil, by Adubbz.
|
||||
#if ENABLE_NETWORK_INSTALL
|
||||
|
||||
#include "usb/usb_uploader.hpp"
|
||||
#include "usb/tinfoil.hpp"
|
||||
#include "usb/usb_api.hpp"
|
||||
#include "log.hpp"
|
||||
#include "defines.hpp"
|
||||
|
||||
namespace sphaira::usb::upload {
|
||||
namespace {
|
||||
|
||||
namespace tinfoil = usb::tinfoil;
|
||||
|
||||
const UsbHsInterfaceFilter FILTER{
|
||||
.Flags = UsbHsInterfaceFilterFlags_idVendor |
|
||||
UsbHsInterfaceFilterFlags_idProduct |
|
||||
@@ -36,6 +31,8 @@ const UsbHsInterfaceFilter FILTER{
|
||||
|
||||
constexpr u8 INDEX = 0;
|
||||
|
||||
using namespace usb::api;
|
||||
|
||||
} // namespace
|
||||
|
||||
Usb::Usb(u64 transfer_timeout) {
|
||||
@@ -46,69 +43,90 @@ Usb::Usb(u64 transfer_timeout) {
|
||||
Usb::~Usb() {
|
||||
}
|
||||
|
||||
Result Usb::WaitForConnection(u64 timeout, u8 flags, std::span<const std::string> names) {
|
||||
Result Usb::WaitForConnection(u64 timeout, std::span<const std::string> names) {
|
||||
R_TRY(m_usb->IsUsbConnected(timeout));
|
||||
|
||||
// build name table.
|
||||
std::string names_list;
|
||||
for (auto& name : names) {
|
||||
names_list += name + '\n';
|
||||
}
|
||||
|
||||
tinfoil::TUSHeader header{};
|
||||
header.magic = tinfoil::Magic_List0;
|
||||
header.nspListSize = names_list.length();
|
||||
header.flags = flags;
|
||||
// send.
|
||||
SendHeader send_header;
|
||||
R_TRY(m_usb->TransferAll(true, &send_header, sizeof(send_header), timeout));
|
||||
R_TRY(send_header.Verify());
|
||||
|
||||
R_TRY(m_usb->TransferAll(false, &header, sizeof(header), timeout));
|
||||
// send table info.
|
||||
R_TRY(SendResult(RESULT_OK, names_list.length()));
|
||||
|
||||
// send name table.
|
||||
R_TRY(m_usb->TransferAll(false, names_list.data(), names_list.length(), timeout));
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Usb::PollCommands() {
|
||||
tinfoil::USBCmdHeader header;
|
||||
R_TRY(m_usb->TransferAll(true, &header, sizeof(header)));
|
||||
R_UNLESS(header.magic == tinfoil::Magic_Command0, Result_UsbUploadBadMagic);
|
||||
SendHeader send_header;
|
||||
R_TRY(m_usb->TransferAll(true, &send_header, sizeof(send_header)));
|
||||
R_TRY(send_header.Verify());
|
||||
|
||||
if (header.cmdId == tinfoil::USBCmdId::EXIT) {
|
||||
if (send_header.cmd == CMD_QUIT) {
|
||||
R_TRY(SendResult(RESULT_OK));
|
||||
R_THROW(Result_UsbUploadExit);
|
||||
} else if (header.cmdId == tinfoil::USBCmdId::FILE_RANGE) {
|
||||
return FileRangeCmd(header.dataSize);
|
||||
} else if (send_header.cmd == CMD_OPEN) {
|
||||
s64 file_size;
|
||||
u16 flags;
|
||||
R_TRY(Open(send_header.arg3, file_size, flags));
|
||||
|
||||
const auto size_lsb = file_size & 0xFFFFFFFF;
|
||||
const auto size_msb = ((file_size >> 32) & 0xFFFF) | (flags << 16);
|
||||
return SendResult(RESULT_OK, size_msb, size_lsb);
|
||||
} else {
|
||||
R_TRY(SendResult(RESULT_ERROR));
|
||||
R_THROW(Result_UsbUploadBadCommand);
|
||||
}
|
||||
}
|
||||
|
||||
Result Usb::FileRangeCmd(u64 data_size) {
|
||||
tinfoil::FileRangeCmdHeader header;
|
||||
R_TRY(m_usb->TransferAll(true, &header, sizeof(header)));
|
||||
Result Usb::file_transfer_loop() {
|
||||
log_write("doing file transfer\n");
|
||||
|
||||
std::string path(header.nspNameLen, '\0');
|
||||
R_TRY(m_usb->TransferAll(true, path.data(), header.nspNameLen));
|
||||
// get offset + size.
|
||||
SendDataHeader send_header;
|
||||
R_TRY(m_usb->TransferAll(true, &send_header, sizeof(send_header)));
|
||||
|
||||
// send response header.
|
||||
R_TRY(m_usb->TransferAll(false, &header, sizeof(header)));
|
||||
|
||||
s64 curr_off = 0x0;
|
||||
s64 end_off = header.size;
|
||||
s64 read_size = header.size;
|
||||
|
||||
m_buf.resize(header.size);
|
||||
|
||||
while (curr_off < end_off) {
|
||||
if (curr_off + read_size >= end_off) {
|
||||
read_size = end_off - curr_off;
|
||||
}
|
||||
|
||||
u64 bytes_read;
|
||||
R_TRY(Read(path, m_buf.data(), header.offset + curr_off, read_size, &bytes_read));
|
||||
R_TRY(m_usb->TransferAll(false, m_buf.data(), bytes_read));
|
||||
curr_off += bytes_read;
|
||||
// check if we should finish now.
|
||||
if (send_header.offset == 0 && send_header.size == 0) {
|
||||
log_write("finished\n");
|
||||
R_TRY(SendResult(RESULT_OK));
|
||||
return Result_UsbUploadExit;
|
||||
}
|
||||
|
||||
// read file and calculate the hash.
|
||||
u64 bytes_read;
|
||||
m_buf.resize(send_header.size);
|
||||
log_write("reading buffer: %zu\n", m_buf.size());
|
||||
|
||||
R_TRY(Read(m_buf.data(), send_header.offset, m_buf.size(), &bytes_read));
|
||||
const auto crc32 = crc32Calculate(m_buf.data(), m_buf.size());
|
||||
|
||||
log_write("read the buffer: %zu\n", bytes_read);
|
||||
// respond back with the length of the data and the crc32.
|
||||
R_TRY(SendResult(RESULT_OK, m_buf.size(), crc32));
|
||||
|
||||
log_write("sent result with crc\n");
|
||||
|
||||
// send the data.
|
||||
R_TRY(m_usb->TransferAll(false, m_buf.data(), m_buf.size()));
|
||||
|
||||
log_write("sent the data\n");
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
} // namespace sphaira::usb::upload
|
||||
Result Usb::SendResult(u32 result, u32 arg3, u32 arg4) {
|
||||
ResultHeader recv_header{MAGIC, result, arg3, arg4};
|
||||
return m_usb->TransferAll(false, &recv_header, sizeof(recv_header));
|
||||
}
|
||||
|
||||
#endif
|
||||
} // namespace sphaira::usb::upload
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
#if ENABLE_NETWORK_INSTALL
|
||||
|
||||
#include "usb/usbds.hpp"
|
||||
#include "log.hpp"
|
||||
#include "defines.hpp"
|
||||
#include <ranges>
|
||||
#include <cstring>
|
||||
|
||||
Result usbDsGetSpeed(UsbDeviceSpeed *out) {
|
||||
if (hosversionBefore(8,0,0)) {
|
||||
return MAKERESULT(Module_Libnx, LibnxError_IncompatSysVer);
|
||||
}
|
||||
|
||||
serviceAssumeDomain(usbDsGetServiceSession());
|
||||
return serviceDispatchOut(usbDsGetServiceSession(), hosversionAtLeast(11,0,0) ? 11 : 12, *out);
|
||||
}
|
||||
|
||||
auto GetUsbDsStateStr(UsbState state) -> const char* {
|
||||
switch (state) {
|
||||
case UsbState_Detached: return "Detached";
|
||||
@@ -30,8 +19,7 @@ auto GetUsbDsStateStr(UsbState state) -> const char* {
|
||||
}
|
||||
|
||||
auto GetUsbDsSpeedStr(UsbDeviceSpeed speed) -> const char* {
|
||||
// todo: remove this cast when libnx pr is merged.
|
||||
switch ((u32)speed) {
|
||||
switch (speed) {
|
||||
case UsbDeviceSpeed_None: return "None";
|
||||
case UsbDeviceSpeed_Low: return "USB 1.0 Low Speed";
|
||||
case UsbDeviceSpeed_Full: return "USB 1.1 Full Speed";
|
||||
@@ -204,6 +192,12 @@ Result UsbDs::Init() {
|
||||
R_TRY(usbDsInterface_EnableInterface(m_interface));
|
||||
R_TRY(usbDsEnable());
|
||||
|
||||
// not needed because the api sends the size of the sent data
|
||||
// before each write, so there's not need to send a zlt.
|
||||
#if 0
|
||||
R_TRY(usbDsEndpoint_SetZlt(m_endpoints[UsbSessionEndpoint_In], true));
|
||||
#endif
|
||||
|
||||
log_write("success USB init\n");
|
||||
R_SUCCEED();
|
||||
}
|
||||
@@ -321,15 +315,6 @@ Result UsbDs::WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) {
|
||||
}
|
||||
|
||||
Result UsbDs::TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 remaining, u32 size, u32 *out_urb_id) {
|
||||
if (ep == UsbSessionEndpoint_In) {
|
||||
if (size && remaining == size && !(size % (u32)m_max_packet_size)) {
|
||||
log_write("[USBDS] SetZlt(true)\n");
|
||||
R_TRY(usbDsEndpoint_SetZlt(m_endpoints[ep], true));
|
||||
} else {
|
||||
R_TRY(usbDsEndpoint_SetZlt(m_endpoints[ep], false));
|
||||
}
|
||||
}
|
||||
|
||||
return usbDsEndpoint_PostBufferAsync(m_endpoints[ep], buffer, size, out_urb_id);
|
||||
}
|
||||
|
||||
@@ -344,5 +329,3 @@ Result UsbDs::GetTransferResult(UsbSessionEndpoint ep, u32 urb_id, u32 *out_requ
|
||||
}
|
||||
|
||||
} // namespace sphaira::usb
|
||||
|
||||
#endif
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user