48 Commits
dev ... 1.0.0

Author SHA1 Message Date
ITotalJustice
7f2d0e72f2 fix workflow 2025-11-18 18:29:38 +00:00
ITotalJustice
c9552f9785 bump version for new release 0.13.3 -> 1.0.0. 2025-11-18 18:11:59 +00:00
ITotalJustice
444ff3e2d1 swkdb: add support for setting the header. save: add support for setting the name for the save file. 2025-10-09 14:45:08 +01:00
ITotalJustice
7d56c8a381 increase list scroll speed. add list jump start/end. add L2 + scroll to select multiple enrties. 2025-10-07 07:05:38 +01:00
ITotalJustice
da051f8d8f add support for replacing the homebrew menu tab with another menu. 2025-10-03 09:58:48 +01:00
ITotalJustice
81e6bc5833 disable sftp as it was unused and very slow compared to other clients. 2025-10-03 09:07:12 +01:00
ITotalJustice
ca5ea827b2 devoptab: fix nginx listing, fix modifying entry overriding the scheme, fix smb failing to parse url if path isn't set. ftpsrv: workaround clients sending PASS for anon. 2025-10-03 07:31:10 +01:00
ITotalJustice
b700fff9ac devoptab: fix create new mount entries failing if the mount folder didn't already exist. 2025-09-29 02:19:25 +01:00
ITotalJustice
81741364a7 homebrew: fix crashing due to using the wrong array size when scrolling. 2025-09-28 23:04:16 +01:00
ITotalJustice
faebc42f0d fs: fix stdio dir count not filtering types. game/bfsar: fix dir listing loop exiting early due to post increment in the loop. 2025-09-27 03:37:29 +01:00
ITotalJustice
63e11ca377 remove unused 12h clock option. add option to hide ip address. 2025-09-21 22:13:53 +01:00
ITotalJustice
54a2215e04 support for filtering mtp/ftp mount options. use builtin config for ftp port,user,pass. 2025-09-21 21:56:36 +01:00
ITotalJustice
5edc3869cd display storage sizes, properly colour info text, and more (see below)
- display internal and sd card storage sizes.
- removed battery info.
- removed current time info.
- fix dumping save to sd card due to not opening the file with append.
- change all sizes to display GB instead of GiB.
- change progress bar units to 1000 rather than 1024.
- all info text, such as sizes and timestamps now use the info text colouring.
- shorten the ncm content type names.
2025-09-21 18:54:08 +01:00
ITotalJustice
a772d660f3 use spinner instead of default icon for homebrew + games menus.
more menus will use it soon.
need to have a way to show the spinner when loading, then revert to the default icon if
failed to load, or an icon doesn't exist.

otherwise, the user may think that the icon is still loading and wait for it.
2025-09-21 04:08:36 +01:00
ITotalJustice
3c504cc85d devoptab: add mounts (wrapper around all mounts, exposed via MTP/FTP). lots of fixes (see below).
- updated libhaze to 81154c1.
- increase ftpsrv stack size as it would crash when modifying custom mounts.
- fix warning for unused log data in haze.
- fix eof read for nsp/xci source by instead returning 0 for bytes read, rather than error.
- add support for lstat the root of a mount.
- handle zero size reads when reading games via devoptab.
2025-09-21 03:51:13 +01:00
ITotalJustice
0a2c16db0c mtp: bump to 6e24502, fixes freezing if write blocks for too long, simplify stream install for mtp and ftp.
see: https://github.com/ITotalJustice/libhaze/issues/1
2025-09-20 20:27:02 +01:00
ITotalJustice
2bd84c8d5a add version overrides for builds. 2025-09-19 19:43:38 +01:00
ITotalJustice
7cd668efb7 keyboard: swap Z/X 2025-09-18 17:14:37 +01:00
ITotalJustice
a6265c3089 add keyboard navigation support. 2025-09-18 17:06:22 +01:00
ITotalJustice
a2300c1a96 fix crashes when signalling a event thats not created yet.
this caused a crash after installing a game if the games menu wasn't init.
the same would happen if the games menu signalled the save menu.
2025-09-18 15:35:24 +01:00
ITotalJustice
3dae3f9173 devoptab/curl: fix rare deadlock casued by sleeping/blocking in curl callback functions.
it seems that curl does not like long blocking in the r/w callbacks.
blocking for too seems to cause a deadlock as the server stops send/recv anymore data.

to fix this, i now use curls pause api.
this api is not thread safe, so it's a little more involved than it needs to be.

however this fixes the deadlock as curls pause actually reduces the download/upload speed
to the minimum. it also reduces exit latency as now exiting is handled in the progress callback
as well, which is called far more often than r/w.
2025-09-16 04:15:56 +01:00
ITotalJustice
63c420d5d8 devoptab: set default url scheme and port in creator. make form sidebar slightly wider and always show on the left side. 2025-09-15 21:51:06 +01:00
ITotalJustice
a94c6bb581 devoptab: add games. add MTP and FTP game exporting. update ftpsrv (see below). fix "fix_path()" parsing.
ftpsrv was updated to support MLST and MLSD, as well as fixing SIZE (was fixed to 32bit).
2025-09-15 21:18:53 +01:00
ITotalJustice
9fe0044a65 devoptab: only push popuplist if the items array is non-empty. curl: guess the url scheme rather than force https. 2025-09-14 15:14:35 +01:00
ITotalJustice
c05ce5eff4 yati: signal change to games menu when a new game is installed. 2025-09-14 14:52:21 +01:00
ITotalJustice
a019103ed5 mui: create menus info text from the menus array, rather than hardcoding them. 2025-09-14 14:37:00 +01:00
ITotalJustice
50e55f4fca mtp: support overriding vid/pid. 2025-09-14 14:30:46 +01:00
ITotalJustice
0706683690 mui: rename misc to menus, change menu options order so that menus is at the top, improve some info boxes text. 2025-09-14 14:16:50 +01:00
ITotalJustice
9cdb77bafa devoptab: add mount creator. 2025-09-14 14:04:20 +01:00
ITotalJustice
b476c54825 devoptab: add workaround for dkp nullptr bug.
manually set the array at startup to avoid nullptr access.
2025-09-13 13:28:55 +01:00
ITotalJustice
8b2e541b1d lots of changes, see description.
- enable sftp by default.
- add more descriptive stdio errors.
- disable devoptab timeout by default.
- handle errors for devoptab seek.
- add r/d/w progress events for threaded_file_transfer
- remove system album from file browser, only show sd card.
- do not clear game selection in games menu. useful for selecting games to backup, then delete after.
- change smb2 r/w to only send max amount, matches nfs behaviour. not sure if its needed as smb probably handles it for us.
2025-09-13 13:16:18 +01:00
ITotalJustice
931531e799 devoptab: add SFTP. fs: disable stdio buffering. cmake: add options to disable components of sphaira, add new "lite" build with minimal features. 2025-09-09 18:39:03 +01:00
ITotalJustice
1695d69aa3 audio: enable flac, make thread safe, fix crash on exit if audio wasn't init. 2025-09-09 10:39:52 +01:00
ITotalJustice
217bd3bed3 mui: add list index to sidebar and popup_list, and better center the index text. 2025-09-08 01:47:41 +01:00
ITotalJustice
384e8794bf devoptab: refactor all custom mounts to inherit from helper struct. 2025-09-08 01:34:20 +01:00
ITotalJustice
61b398a89a fatfs: use devoptab mounting. devoptab: add config for hidding from fs and dump, fix http being writeable. 2025-09-07 17:35:37 +01:00
ITotalJustice
ba78fd0dc5 devoptab: add vfs, change mount.ini path location. 2025-09-07 15:43:01 +01:00
ITotalJustice
43969a773e fs: fix CreateDirectoryRecursivelyWithPath() for root files. save: fix restore detection. devoptab: return proper errno codes. 2025-09-07 14:40:45 +01:00
ITotalJustice
6e1eabbe0f devoptab: deprecate locations.ini in favour of hdd/network mounts, better handle folder creation errors. 2025-09-07 13:30:53 +01:00
ITotalJustice
b99d1e5dea devoptab: add webdav, refactor network devices, multi thread r/w to improve perf and support webdav uploads. 2025-09-07 12:40:45 +01:00
ITotalJustice
6ce566aea5 http: optimise the dir_list parsing, only parse tables. filebrowser: option to disable stat per fs.
this improves network fs speed by disabling stat for http entriely, and only enabling file stat for everything else.
this can be overriden in the config.
2025-09-05 14:10:06 +01:00
ITotalJustice
a4209961e2 devoptab: add ftp mount with random read and streaming write support.
read still needs some work. opening a file should open a thread where data is read/written async.
this avoids the huge number of roundtrips per r/w.

eg, a read currently has to REST, open the data socket, send the file data, send ABOR to close and then close the data socket.

using the thread approach would just send the file over the already open data socket.
writes will also benefit from the above, just instead of REST it would be APPE.

seeks would need to do the above ABORT, close and re-open. however most reads in sphaira are not random access. so this isn't an issue.
2025-09-04 22:29:35 +01:00
ITotalJustice
181ff3f2bf devoptab: fix http config parsing, add more options to network mounts (timeout,uid,port). fs: update path size.
fs path was changed to 255 in the past because that is the real path size that fs can handle.
however, this restriction does not apply to nfs, samba, http - and it is very easy to exceed this 255 path length.

to fix this, i have increased the path len to 1024. this allows fs to continue to work as the buffer is
big enough, but also gives network mounts enough space to work with.
2025-09-04 12:18:34 +01:00
ITotalJustice
b85b522643 app: remove ams erpt disable as it is possible to cause ams to fatal if a crash report fails to write. 2025-09-04 09:40:39 +01:00
ITotalJustice
5158e264c0 devoptab: add http, nfs and smb mount. nca: zero init ncz structs. fs: fix FsPath compare. 2025-09-03 18:56:54 +01:00
ITotalJustice
fd67da0527 webusb: add support for exporting. usb: block requests with no timeout, using pbox to cancel if the user presses B. 2025-09-02 04:24:45 +01:00
ITotalJustice
7bdec8457f tests: move location of usb tests and update workflows for the new paths. 2025-08-31 07:37:54 +01:00
ITotalJustice
bc75c9a89f workflow: temp disable main workflow on dev until libnx pushes a new release. 2025-08-31 07:30:13 +01:00
106 changed files with 21346 additions and 3254 deletions

View File

@@ -1,6 +1,12 @@
name: build
on: [push, pull_request]
on:
push:
branches-ignore:
- dev
pull_request:
branches-ignore:
- dev
jobs:
build:

View File

@@ -3,7 +3,7 @@ name: USB Export Python Tests
on:
push:
paths: &python_usb_export_paths
- 'tools/test_usb_export.py'
- 'tools/tests/test_usb_export.py'
- 'tools/usb_export.py'
- 'tools/usb_common.py'
- 'tools/requirements.txt'
@@ -30,4 +30,4 @@ jobs:
- name: Run tests
run: |
python3 tools/test_usb_export.py
python3 tools/tests/test_usb_export.py

View File

@@ -3,7 +3,7 @@ name: USB Install Python Tests
on:
push:
paths: &python_usb_install_paths
- 'tools/test_usb_install.py'
- 'tools/tests/test_usb_install.py'
- 'tools/usb_install.py'
- 'tools/usb_common.py'
- 'tools/requirements.txt'
@@ -30,4 +30,4 @@ jobs:
- name: Run tests
run: |
python3 tools/test_usb_install.py
python3 tools/tests/test_usb_install.py

3
.gitignore vendored
View File

@@ -27,3 +27,6 @@ out
usb_test/
__pycache__
usb_*.spec
CMakeUserPresets.json
build_patreon.sh

View File

@@ -21,6 +21,35 @@
"LTO": true
}
},
{
"name": "Lite",
"displayName": "Lite",
"inherits":["core"],
"cacheVariables": {
"CMAKE_BUILD_TYPE": "MinSizeRel",
"LTO": true,
"ENABLE_NVJPG": false,
"ENABLE_NSZ": false,
"ENABLE_LIBUSBHSFS": false,
"ENABLE_LIBUSBDVD": false,
"ENABLE_FTPSRV": false,
"ENABLE_LIBHAZE": false,
"ENABLE_AUDIO_MP3": false,
"ENABLE_AUDIO_OGG": false,
"ENABLE_AUDIO_WAV": false,
"ENABLE_AUDIO_FLAC": false,
"ENABLE_DEVOPTAB_HTTP": false,
"ENABLE_DEVOPTAB_NFS": false,
"ENABLE_DEVOPTAB_SMB2": false,
"ENABLE_DEVOPTAB_FTP": false,
"ENABLE_DEVOPTAB_SFTP": false,
"ENABLE_DEVOPTAB_WEBDAV": false
}
},
{
"name": "Dev",
"displayName": "Dev",
@@ -38,6 +67,11 @@
"configurePreset": "Release",
"jobs": 16
},
{
"name": "Lite",
"configurePreset": "Lite",
"jobs": 16
},
{
"name": "Dev",
"configurePreset": "Dev",

View File

@@ -282,7 +282,6 @@
"Convert to standard crypto": "Converter para crypto padrão",
"Lower master key": "Reduzir master keys",
"Lower system version": "Reduzir versão do sistema",
"Disable erpt_reports": "Desabilitar \"erpt_reports\"",
"Homebrew": "Homebrews",
"Apps": "Homebrews",

View File

@@ -1,6 +1,34 @@
cmake_minimum_required(VERSION 3.13)
set(sphaira_VERSION 0.13.3)
# generic options.
option(ENABLE_NVJPG "" OFF)
option(ENABLE_NSZ "enables exporting to nsz" ON)
# lib options.
option(ENABLE_LIBUSBHSFS "enables FAT/exFAT hdd mounting" ON)
option(ENABLE_LIBUSBDVD "enables cd/dvd/iso/cue mounting" ON)
option(ENABLE_FTPSRV "enables MTP server support" ON)
option(ENABLE_LIBHAZE "enables MTP server support" ON)
# audio options.
option(ENABLE_AUDIO_MP3 "" ON)
option(ENABLE_AUDIO_OGG "" ON)
option(ENABLE_AUDIO_WAV "" ON)
option(ENABLE_AUDIO_FLAC "" ON)
# devoptab options.
option(ENABLE_DEVOPTAB_HTTP "" ON)
option(ENABLE_DEVOPTAB_NFS "" ON)
option(ENABLE_DEVOPTAB_SMB2 "" ON)
option(ENABLE_DEVOPTAB_FTP "" ON)
option(ENABLE_DEVOPTAB_WEBDAV "" ON)
# disable by default because we are CPU bound for upload/download.
# max speed is 8MiB/s, which is fine for wifi, but awful for ethernet.
# other clients get 36-40MiB/s.
# it also adds 230k to binary size, and i don't think anyone will use it.
option(ENABLE_DEVOPTAB_SFTP "" OFF)
set(sphaira_VERSION 1.0.0)
project(sphaira
VERSION ${sphaira_VERSION}
@@ -30,7 +58,11 @@ execute_process(
OUTPUT_STRIP_TRAILING_WHITESPACE
)
set(sphaira_VERSION_HASH "${sphaira_VERSION} [${GIT_COMMIT}]")
if (DEFINED sphaira_VERSION_OVERRIDE)
set(sphaira_DISPLAY_VERSION "${sphaira_VERSION_OVERRIDE} [${GIT_COMMIT}]")
else()
set(sphaira_DISPLAY_VERSION "${sphaira_VERSION} [${GIT_COMMIT}]")
endif()
add_executable(sphaira
source/ui/menus/appstore.cpp
@@ -46,8 +78,6 @@ add_executable(sphaira
source/ui/menus/themezer.cpp
source/ui/menus/ghdl.cpp
source/ui/menus/usb_menu.cpp
source/ui/menus/ftp_menu.cpp
source/ui/menus/mtp_menu.cpp
source/ui/menus/gc_menu.cpp
source/ui/menus/game_menu.cpp
source/ui/menus/game_meta_menu.cpp
@@ -85,17 +115,12 @@ add_executable(sphaira
source/web.cpp
source/hasher.cpp
source/i18n.cpp
source/ftpsrv_helper.cpp
source/haze_helper.cpp
source/threaded_file_transfer.cpp
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
@@ -105,6 +130,11 @@ add_executable(sphaira
source/utils/devoptab_xci.cpp
source/utils/devoptab_zip.cpp
source/utils/devoptab_bfsar.cpp
source/utils/devoptab_vfs.cpp
source/utils/devoptab_fatfs.cpp
source/utils/devoptab_game.cpp
source/utils/devoptab_mounts.cpp
source/utils/devoptab.cpp
source/usb/base.cpp
source/usb/usbds.cpp
@@ -133,7 +163,7 @@ add_executable(sphaira
target_compile_definitions(sphaira PRIVATE
-DAPP_VERSION="${sphaira_VERSION}"
-DAPP_VERSION_HASH="${sphaira_VERSION_HASH}"
-DAPP_DISPLAY_VERSION="${sphaira_DISPLAY_VERSION}"
-DCURL_NO_OLDIES=1
-DDEV_BUILD=$<BOOL:${DEV_BUILD}>
-DZSTD_STATIC_LINKING_ONLY=1
@@ -182,17 +212,6 @@ target_compile_options(sphaira PRIVATE
include(FetchContent)
set(FETCHCONTENT_QUIET FALSE)
FetchContent_Declare(ftpsrv
GIT_REPOSITORY https://github.com/ITotalJustice/ftpsrv.git
GIT_TAG 85b3cf0
SOURCE_SUBDIR NONE
)
FetchContent_Declare(libhaze
GIT_REPOSITORY https://github.com/ITotalJustice/libhaze.git
GIT_TAG f0b2a14
)
FetchContent_Declare(libpulsar
GIT_REPOSITORY https://github.com/ITotalJustice/switch-libpulsar.git
GIT_TAG ac7bc97
@@ -224,11 +243,6 @@ FetchContent_Declare(zstd
SOURCE_SUBDIR build/cmake
)
FetchContent_Declare(libusbhsfs
GIT_REPOSITORY https://github.com/ITotalJustice/libusbhsfs.git
GIT_TAG 625269b
)
FetchContent_Declare(libnxtc
GIT_REPOSITORY https://github.com/ITotalJustice/libnxtc.git
GIT_TAG 88ce3d8
@@ -245,35 +259,279 @@ FetchContent_Declare(dr_libs
SOURCE_SUBDIR NONE
)
FetchContent_Declare(id3v2lib
GIT_REPOSITORY https://github.com/larsbs/id3v2lib.git
GIT_TAG 141ffb8
)
if (ENABLE_NVJPG)
FetchContent_Declare(nvjpg
GIT_REPOSITORY https://github.com/ITotalJustice/oss-nvjpg.git
GIT_TAG 45680e7
)
FetchContent_Declare(libusbdvd
GIT_REPOSITORY https://github.com/proconsule/libusbdvd.git
GIT_TAG 3cb0613
)
FetchContent_MakeAvailable(nvjpg)
set(USE_NEW_ZSTD ON)
# has issues with some homebrew and game icons (oxenfree, overwatch2).
set(USE_NVJPG OFF)
add_library(nvjpg
${nvjpg_SOURCE_DIR}/lib/decoder.cpp
${nvjpg_SOURCE_DIR}/lib/image.cpp
${nvjpg_SOURCE_DIR}/lib/surface.cpp
)
target_include_directories(nvjpg PUBLIC ${nvjpg_SOURCE_DIR}/include)
set_target_properties(nvjpg PROPERTIES CXX_STANDARD 26)
target_link_libraries(nvjpg PRIVATE nvjpg)
target_compile_definitions(sphaira PRIVATE ENABLE_NVJPG)
endif()
if (ENABLE_NSZ)
target_sources(sphaira PRIVATE source/utils/nsz_dumper.cpp)
target_compile_definitions(sphaira PRIVATE ENABLE_NSZ)
endif()
if (ENABLE_LIBUSBHSFS)
# enable this if you want ntfs and ext4 support, at the cost of a huge final binary size.
set(USBHSFS_GPL OFF)
set(USBHSFS_SXOS_DISABLE ON)
FetchContent_Declare(libusbhsfs
GIT_REPOSITORY https://github.com/ITotalJustice/libusbhsfs.git
GIT_TAG 625269b
)
FetchContent_MakeAvailable(libusbhsfs)
target_compile_definitions(sphaira PRIVATE ENABLE_LIBUSBHSFS)
target_link_libraries(sphaira PRIVATE libusbhsfs)
else()
target_sources(sphaira PRIVATE source/ff16/ffunicode.c)
endif()
if (ENABLE_LIBUSBDVD)
FetchContent_Declare(libusbdvd
GIT_REPOSITORY https://github.com/proconsule/libusbdvd.git
GIT_TAG 3cb0613
)
FetchContent_MakeAvailable(libusbdvd)
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)
target_compile_definitions(sphaira PRIVATE ENABLE_LIBUSBDVD)
target_link_libraries(sphaira PRIVATE libusbdvd)
target_sources(sphaira PRIVATE source/usbdvd.cpp)
endif()
if (ENABLE_FTPSRV)
FetchContent_Declare(ftpsrv
GIT_REPOSITORY https://github.com/ITotalJustice/ftpsrv.git
GIT_TAG 7c82402
SOURCE_SUBDIR NONE
)
FetchContent_MakeAvailable(ftpsrv)
set(FTPSRV_LIB_BUILD TRUE)
set(FTPSRV_LIB_VFS_CUSTOM ${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs_nx.h)
set(FTPSRV_LIB_PATH_SIZE 0x301)
set(FTPSRV_LIB_SESSIONS 16)
set(FTPSRV_LIB_BUF_SIZE 1024*64)
set(FTPSRV_LIB_CUSTOM_DEFINES
USE_VFS_SAVE=$<BOOL:FALSE>
USE_VFS_STORAGE=$<BOOL:TRUE>
# disabled as it may conflict with the gamecard menu.
USE_VFS_GC=$<BOOL:FALSE>
USE_VFS_USBHSFS=$<BOOL:FALSE>
VFS_NX_BUFFER_IO=$<BOOL:TRUE>
# let sphaira handle init / closing of the hdd.
USE_VFS_USBHSFS_INIT=$<BOOL:FALSE>
# disable romfs mounting as otherwise we cannot write / modify sphaira.nro
USE_VFS_ROMFS=$<BOOL:FALSE>
FTP_SOCKET_HEADER="${ftpsrv_SOURCE_DIR}/src/platform/nx/socket_nx.h"
)
add_subdirectory(${ftpsrv_SOURCE_DIR} binary_dir)
add_library(ftpsrv_helper
${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs_nx.c
${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs/vfs_nx_none.c
${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs/vfs_nx_root.c
${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs/vfs_nx_fs.c
${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs/vfs_nx_storage.c
${ftpsrv_SOURCE_DIR}/src/platform/nx/utils.c
)
target_link_libraries(ftpsrv_helper PUBLIC ftpsrv)
target_include_directories(ftpsrv_helper PUBLIC ${ftpsrv_SOURCE_DIR}/src/platform)
target_compile_definitions(sphaira PRIVATE ENABLE_FTPSRV)
target_link_libraries(sphaira PRIVATE ftpsrv_helper)
target_sources(sphaira PRIVATE
source/ftpsrv_helper.cpp
source/ui/menus/ftp_menu.cpp
)
endif()
if (ENABLE_LIBHAZE)
FetchContent_Declare(libhaze
GIT_REPOSITORY https://github.com/ITotalJustice/libhaze.git
GIT_TAG 81154c1
)
FetchContent_MakeAvailable(libhaze)
target_compile_definitions(sphaira PRIVATE ENABLE_LIBHAZE)
target_link_libraries(sphaira PRIVATE libhaze)
target_sources(sphaira PRIVATE
source/haze_helper.cpp
source/ui/menus/mtp_menu.cpp
)
endif()
if (ENABLE_DEVOPTAB_HTTP)
target_compile_definitions(sphaira PRIVATE ENABLE_DEVOPTAB_HTTP)
target_sources(sphaira PRIVATE source/utils/devoptab_http.cpp)
endif()
if (ENABLE_DEVOPTAB_NFS)
FetchContent_Declare(libnfs
GIT_REPOSITORY https://github.com/ITotalJustice/libnfs.git
GIT_TAG 65f3e11
)
FetchContent_MakeAvailable(libnfs)
target_compile_definitions(sphaira PRIVATE ENABLE_DEVOPTAB_NFS)
target_link_libraries(sphaira PRIVATE nfs)
target_sources(sphaira PRIVATE source/utils/devoptab_nfs.cpp)
# todo: fix this upstream as nfs should export these folders.
target_include_directories(sphaira PRIVATE
${libnfs_SOURCE_DIR}/include
${libnfs_SOURCE_DIR}/include/nfsc
${libnfs_SOURCE_DIR}/nfs
)
endif()
if (ENABLE_DEVOPTAB_SMB2)
FetchContent_Declare(libsmb2
GIT_REPOSITORY https://github.com/ITotalJustice/libsmb2.git
GIT_TAG 867beea
)
FetchContent_MakeAvailable(libsmb2)
target_compile_definitions(sphaira PRIVATE ENABLE_DEVOPTAB_SMB2)
target_link_libraries(sphaira PRIVATE smb2)
target_sources(sphaira PRIVATE source/utils/devoptab_smb2.cpp)
endif()
if (ENABLE_DEVOPTAB_FTP)
target_compile_definitions(sphaira PRIVATE ENABLE_DEVOPTAB_FTP)
target_sources(sphaira PRIVATE source/utils/devoptab_ftp.cpp)
endif()
if (ENABLE_DEVOPTAB_SFTP)
# set to build from source, otherwise it will link against the older dkp libssh2.
if (1)
set(CRYPTO_BACKEND mbedTLS)
set(ENABLE_ZLIB_COMPRESSION ON)
set(ENABLE_DEBUG_LOGGING OFF)
set(BUILD_EXAMPLES OFF)
set(BUILD_TESTING OFF)
set(LINT OFF)
FetchContent_Declare(libssh2
GIT_REPOSITORY https://github.com/libssh2/libssh2.git
# GIT_TAG a0dafb3 # latest commit, works fine, but i'll stick to main release.
GIT_TAG libssh2-1.11.1
)
FetchContent_MakeAvailable(libssh2)
target_link_libraries(sphaira PRIVATE libssh2::libssh2)
else()
include(FindPkgConfig)
pkg_check_modules(LIBSSH2 libssh2 REQUIRED)
target_include_directories(sphaira PRIVATE ${LIBSSH2_INCLUDE_DIRS})
target_link_libraries(sphaira PRIVATE ${LIBSSH2_LIBRARIES})
endif()
target_compile_definitions(sphaira PRIVATE ENABLE_DEVOPTAB_SFTP)
target_sources(sphaira PRIVATE source/utils/devoptab_sftp.cpp)
endif()
if (ENABLE_DEVOPTAB_WEBDAV)
set(PUGIXML_NO_EXCEPTIONS ON)
set(PUGIXML_WCHAR_MODE OFF)
FetchContent_Declare(pugixml
GIT_REPOSITORY https://github.com/zeux/pugixml.git
GIT_TAG v1.15
)
FetchContent_MakeAvailable(pugixml)
target_compile_definitions(sphaira PRIVATE ENABLE_DEVOPTAB_WEBDAV)
target_link_libraries(sphaira PRIVATE pugixml)
target_sources(sphaira PRIVATE source/utils/devoptab_webdav.cpp)
endif()
if (ENABLE_AUDIO_MP3)
FetchContent_Declare(id3v2lib
GIT_REPOSITORY https://github.com/larsbs/id3v2lib.git
GIT_TAG 141ffb8
)
FetchContent_MakeAvailable(id3v2lib)
target_link_libraries(sphaira PRIVATE id3v2lib)
target_compile_definitions(sphaira PRIVATE ENABLE_AUDIO_MP3)
endif()
if (ENABLE_AUDIO_OGG)
target_compile_definitions(sphaira PRIVATE ENABLE_AUDIO_OGG)
endif()
if (ENABLE_AUDIO_WAV)
target_compile_definitions(sphaira PRIVATE ENABLE_AUDIO_WAV)
endif()
if (ENABLE_AUDIO_FLAC)
target_compile_definitions(sphaira PRIVATE ENABLE_AUDIO_FLAC)
endif()
# ztsd
set(ZSTD_BUILD_STATIC ON)
set(ZSTD_BUILD_SHARED OFF)
set(ZSTD_BUILD_COMPRESSION ON)
set(ZSTD_MULTITHREAD_SUPPORT ON)
set(ZSTD_BUILD_COMPRESSION ${ENABLE_NSZ})
set(ZSTD_MULTITHREAD_SUPPORT ${ENABLE_NSZ})
set(ZSTD_BUILD_DECOMPRESSION ON)
set(ZSTD_BUILD_DICTBUILDER OFF)
set(ZSTD_LEGACY_SUPPORT OFF)
set(ZSTD_BUILD_PROGRAMS OFF)
set(ZSTD_BUILD_TESTS OFF)
# minini
set(MININI_LIB_NAME minIni)
set(MININI_USE_STDIO ON)
set(MININI_USE_NX OFF)
set(MININI_USE_FLOAT ON)
# nanovg
if (CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")
set(NANOVG_DEBUG ON)
endif()
@@ -287,6 +545,7 @@ set(NANOVG_NO_HDR ON)
set(NANOVG_NO_PIC ON)
set(NANOVG_NO_PNM ON)
# yyjson
set(YYJSON_INSTALL OFF)
set(YYJSON_DISABLE_READER OFF)
set(YYJSON_DISABLE_WRITER OFF)
@@ -296,63 +555,17 @@ set(YYJSON_DISABLE_NON_STANDARD ON)
set(YYJSON_DISABLE_UTF8_VALIDATION ON)
set(YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS OFF)
# enable this if you want ntfs and ext4 support, at the cost of a huge final binary size.
set(USBHSFS_GPL OFF)
set(USBHSFS_SXOS_DISABLE ON)
FetchContent_MakeAvailable(
ftpsrv
libhaze
libpulsar
nanovg
stb
minIni
yyjson
zstd
libusbhsfs
libnxtc
nvjpg
dr_libs
id3v2lib
libusbdvd
)
set(FTPSRV_LIB_BUILD TRUE)
set(FTPSRV_LIB_VFS_CUSTOM ${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs_nx.h)
set(FTPSRV_LIB_PATH_SIZE 0x301)
set(FTPSRV_LIB_SESSIONS 16)
set(FTPSRV_LIB_BUF_SIZE 1024*64)
set(FTPSRV_LIB_CUSTOM_DEFINES
USE_VFS_SAVE=$<BOOL:FALSE>
USE_VFS_STORAGE=$<BOOL:TRUE>
# disabled as it may conflict with the gamecard menu.
USE_VFS_GC=$<BOOL:FALSE>
USE_VFS_USBHSFS=$<BOOL:TRUE>
VFS_NX_BUFFER_IO=$<BOOL:TRUE>
# let sphaira handle init / closing of the hdd.
USE_VFS_USBHSFS_INIT=$<BOOL:FALSE>
# disable romfs mounting as otherwise we cannot write / modify sphaira.nro
USE_VFS_ROMFS=$<BOOL:FALSE>
FTP_SOCKET_HEADER="${ftpsrv_SOURCE_DIR}/src/platform/nx/socket_nx.h"
)
add_subdirectory(${ftpsrv_SOURCE_DIR} binary_dir)
add_library(ftpsrv_helper
${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs_nx.c
${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs/vfs_nx_none.c
${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs/vfs_nx_root.c
${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs/vfs_nx_fs.c
${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs/vfs_nx_storage.c
${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs/vfs_nx_stdio.c
${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs/vfs_nx_hdd.c
${ftpsrv_SOURCE_DIR}/src/platform/nx/utils.c
)
target_link_libraries(ftpsrv_helper PUBLIC ftpsrv libusbhsfs)
target_include_directories(ftpsrv_helper PUBLIC ${ftpsrv_SOURCE_DIR}/src/platform)
add_library(stb INTERFACE)
target_include_directories(stb INTERFACE ${stb_SOURCE_DIR})
@@ -366,38 +579,6 @@ 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
${nvjpg_SOURCE_DIR}/lib/image.cpp
${nvjpg_SOURCE_DIR}/lib/surface.cpp
)
target_include_directories(nvjpg PUBLIC ${nvjpg_SOURCE_DIR}/include)
set_target_properties(nvjpg PROPERTIES CXX_STANDARD 26)
endif()
find_package(ZLIB REQUIRED)
find_library(minizip_lib minizip REQUIRED)
find_path(minizip_inc minizip REQUIRED)
@@ -406,11 +587,6 @@ find_package(CURL REQUIRED)
find_path(mbedtls_inc mbedtls REQUIRED)
find_library(mbedcrypto_lib mbedcrypto REQUIRED)
if (NOT USE_NEW_ZSTD)
find_path(zstd_inc zstd.h REQUIRED)
find_library(zstd_lib zstd REQUIRED)
endif()
add_library(fatfs
source/ff16/diskio.c
source/ff16/ff.c
@@ -426,19 +602,15 @@ set_target_properties(sphaira PROPERTIES
)
target_link_libraries(sphaira PRIVATE
ftpsrv_helper
libhaze
libpulsar
minIni
nanovg
stb
yyjson
# libusbhsfs
libnxtc
fatfs
dr_libs
id3v2lib
libusbdvd
libzstd_static
${minizip_lib}
ZLIB::ZLIB
@@ -446,20 +618,6 @@ target_link_libraries(sphaira PRIVATE
${mbedcrypto_lib}
)
if (USE_NEW_ZSTD)
message(STATUS "USING UPSTREAM ZSTD")
target_link_libraries(sphaira PRIVATE libzstd_static)
else()
message(STATUS "USING LOCAL ZSTD")
target_link_libraries(sphaira PRIVATE ${zstd_lib})
target_include_directories(sphaira PRIVATE ${zstd_inc})
endif()
if (USE_NVJPG)
target_link_libraries(sphaira PRIVATE nvjpg)
target_compile_definitions(sphaira PRIVATE USE_NVJPG)
endif()
target_include_directories(sphaira PRIVATE
include
${minizip_inc}

View File

@@ -95,7 +95,6 @@ public:
static auto GetInstallEmummcEnable() -> bool;
static auto GetInstallSdEnable() -> bool;
static auto GetThemeMusicEnable() -> bool;
static auto Get12HourTimeEnable() -> bool;
static auto GetLanguage() -> long;
static auto GetTextScrollSpeed() -> long;
@@ -127,10 +126,13 @@ public:
static void DisplayThemeOptions(bool left_side = true);
// todo:
static void DisplayNetworkOptions(bool left_side = true);
static void DisplayMiscOptions(bool left_side = true);
static void DisplayMenuOptions(bool left_side = true);
static void DisplayAdvancedOptions(bool left_side = true);
static void DisplayInstallOptions(bool left_side = true);
static void DisplayDumpOptions(bool left_side = true);
static void DisplayFtpOptions(bool left_side = true);
static void DisplayMtpOptions(bool left_side = true);
static void DisplayHddOptions(bool left_side = true);
// helper for sidebar options to toggle install on/off
static void ShowEnableInstallPromptOption(option::OptionBool& option, bool& enable);
@@ -155,6 +157,9 @@ public:
static Result SetDefaultBackgroundMusic(fs::Fs* fs, const fs::FsPath& path);
static void SetBackgroundMusicPause(bool pause);
static Result GetSdSize(s64* free, s64* total);
static Result GetEmmcSize(s64* free, s64* total);
// helper that converts 1.2.3 to a u32 used for comparisons.
static auto GetVersionFromString(const char* str) -> u32;
static auto IsVersionNewer(const char* current, const char* new_version) -> u32;
@@ -267,6 +272,7 @@ public:
PadState m_pad{};
TouchInfo m_touch_info{};
Controller m_controller{};
KeyboardState m_keyboard{};
std::vector<ThemeMeta> m_theme_meta_entries;
Vec2 m_scale{1, 1};
@@ -296,8 +302,9 @@ public:
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};
option::OptionBool m_show_ip_addr{INI_SECTION, "show_ip_addr", true};
option::OptionLong m_language{INI_SECTION, "language", 0}; // auto
option::OptionString m_center_menu{INI_SECTION, "center_side_menu", "Homebrew"};
option::OptionString m_left_menu{INI_SECTION, "left_side_menu", "FileBrowser"};
option::OptionString m_right_menu{INI_SECTION, "right_side_menu", "Appstore"};
option::OptionBool m_progress_boost_mode{INI_SECTION, "progress_boost_mode", true};
@@ -338,6 +345,39 @@ public:
// todo: move this into it's own menu
option::OptionLong m_text_scroll_speed{"accessibility", "text_scroll_speed", 1}; // normal
// ftp options.
option::OptionLong m_ftp_port{"ftp", "port", 5000};
option::OptionBool m_ftp_anon{"ftp", "anon", true};
option::OptionString m_ftp_user{"ftp", "user", ""};
option::OptionString m_ftp_pass{"ftp", "pass", ""};
option::OptionBool m_ftp_show_album{"ftp", "show_album", true};
option::OptionBool m_ftp_show_ams_contents{"ftp", "show_ams_contents", false};
option::OptionBool m_ftp_show_bis_storage{"ftp", "show_bis_storage", false};
option::OptionBool m_ftp_show_bis_fs{"ftp", "show_bis_fs", false};
option::OptionBool m_ftp_show_content_system{"ftp", "show_content_system", false};
option::OptionBool m_ftp_show_content_user{"ftp", "show_content_user", false};
option::OptionBool m_ftp_show_content_sd{"ftp", "show_content_sd", false};
// option::OptionBool m_ftp_show_content_sd0{"ftp", "show_content_sd0", false};
// option::OptionBool m_ftp_show_custom_system{"ftp", "show_custom_system", false};
// option::OptionBool m_ftp_show_custom_sd{"ftp", "show_custom_sd", false};
option::OptionBool m_ftp_show_games{"ftp", "show_games", true};
option::OptionBool m_ftp_show_install{"ftp", "show_install", true};
option::OptionBool m_ftp_show_mounts{"ftp", "show_mounts", false};
option::OptionBool m_ftp_show_switch{"ftp", "show_switch", false};
// mtp options.
option::OptionLong m_mtp_vid{"mtp", "vid", 0x057e}; // nintendo (hidden from ui)
option::OptionLong m_mtp_pid{"mtp", "pid", 0x201d}; // switch (hidden from ui)
option::OptionBool m_mtp_allocate_file{"mtp", "allocate_file", true};
option::OptionBool m_mtp_show_album{"mtp", "show_album", true};
option::OptionBool m_mtp_show_content_sd{"mtp", "show_content_sd", false};
option::OptionBool m_mtp_show_content_system{"mtp", "show_content_system", false};
option::OptionBool m_mtp_show_content_user{"mtp", "show_content_user", false};
option::OptionBool m_mtp_show_games{"mtp", "show_games", true};
option::OptionBool m_mtp_show_install{"mtp", "show_install", true};
option::OptionBool m_mtp_show_mounts{"mtp", "show_mounts", false};
option::OptionBool m_mtp_show_speedtest{"mtp", "show_speedtest", false};
std::shared_ptr<fs::FsNativeSd> m_fs{};
audio::SongID m_background_music{};

View File

@@ -511,7 +511,22 @@ enum class SphairaResult : Result {
FsNewPathEmpty,
FsLoadingCancelled,
FsBrokenRoot,
FsUnknownStdioError,
FsStdioFailedToSeek,
FsStdioFailedToRead,
FsStdioFailedToWrite,
FsStdioFailedToOpenFile,
FsStdioFailedToCreate,
FsStdioFailedToTruncate,
FsStdioFailedToFlush,
FsStdioFailedToCreateDirectory,
FsStdioFailedToDeleteFile,
FsStdioFailedToDeleteDirectory,
FsStdioFailedToOpenDirectory,
FsStdioFailedToRename,
FsStdioFailedToStat,
FsReadOnly,
FsNotActive,
FsFailedStdioStat,
@@ -680,6 +695,19 @@ enum : Result {
MAKE_SPHAIRA_RESULT_ENUM(FsLoadingCancelled),
MAKE_SPHAIRA_RESULT_ENUM(FsBrokenRoot),
MAKE_SPHAIRA_RESULT_ENUM(FsUnknownStdioError),
MAKE_SPHAIRA_RESULT_ENUM(FsStdioFailedToSeek),
MAKE_SPHAIRA_RESULT_ENUM(FsStdioFailedToRead),
MAKE_SPHAIRA_RESULT_ENUM(FsStdioFailedToWrite),
MAKE_SPHAIRA_RESULT_ENUM(FsStdioFailedToOpenFile),
MAKE_SPHAIRA_RESULT_ENUM(FsStdioFailedToCreate),
MAKE_SPHAIRA_RESULT_ENUM(FsStdioFailedToTruncate),
MAKE_SPHAIRA_RESULT_ENUM(FsStdioFailedToFlush),
MAKE_SPHAIRA_RESULT_ENUM(FsStdioFailedToCreateDirectory),
MAKE_SPHAIRA_RESULT_ENUM(FsStdioFailedToDeleteFile),
MAKE_SPHAIRA_RESULT_ENUM(FsStdioFailedToDeleteDirectory),
MAKE_SPHAIRA_RESULT_ENUM(FsStdioFailedToOpenDirectory),
MAKE_SPHAIRA_RESULT_ENUM(FsStdioFailedToRename),
MAKE_SPHAIRA_RESULT_ENUM(FsStdioFailedToStat),
MAKE_SPHAIRA_RESULT_ENUM(FsReadOnly),
MAKE_SPHAIRA_RESULT_ENUM(FsNotActive),
MAKE_SPHAIRA_RESULT_ENUM(FsFailedStdioStat),
@@ -823,11 +851,83 @@ enum : Result {
#define CONCATENATE(s1, s2) CONCATENATE_IMPL(s1, s2)
#define ANONYMOUS_VARIABLE(pref) CONCATENATE(pref, __COUNTER__)
#define ON_SCOPE_EXIT(_f) std::experimental::scope_exit ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE_){[&] { _f; }};
template<typename Function>
struct ScopeGuard {
ScopeGuard(Function&& function) : m_function(std::forward<Function>(function)) {
}
~ScopeGuard() {
m_function();
}
ScopeGuard(const ScopeGuard&) = delete;
void operator=(const ScopeGuard&) = delete;
private:
const Function m_function;
};
struct ScopedMutex {
ScopedMutex(Mutex* mutex) : m_mutex{mutex} {
mutexLock(m_mutex);
}
~ScopedMutex() {
mutexUnlock(m_mutex);
}
ScopedMutex(const ScopedMutex&) = delete;
void operator=(const ScopedMutex&) = delete;
private:
Mutex* const m_mutex;
};
struct ScopedRMutex {
ScopedRMutex(RMutex* _mutex) : mutex{_mutex} {
rmutexLock(mutex);
}
~ScopedRMutex() {
rmutexUnlock(mutex);
}
ScopedRMutex(const ScopedRMutex&) = delete;
void operator=(const ScopedRMutex&) = delete;
private:
RMutex* const mutex;
};
struct ScopedRwLock {
ScopedRwLock(RwLock* _lock, bool _write) : lock{_lock}, write{_write} {
if (write) {
rwlockWriteLock(lock);
} else {
rwlockReadLock(lock);
}
}
~ScopedRwLock() {
if (write) {
rwlockWriteUnlock(lock);
} else {
rwlockReadUnlock(lock);
}
}
ScopedRwLock(const ScopedRwLock&) = delete;
void operator=(const ScopedRwLock&) = delete;
private:
RwLock* const lock;
bool const write;
};
// #define ON_SCOPE_EXIT(_f) std::experimental::scope_exit ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE_){[&] { _f; }};
#define ON_SCOPE_EXIT(_f) ScopeGuard ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE_){[&] { _f; }};
#define SCOPED_MUTEX(_m) ScopedMutex ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE_){_m}
#define SCOPED_RMUTEX(_m) ScopedRMutex ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE_){_m}
#define SCOPED_RWLOCK(_m, _write) ScopedRwLock ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE_){_m, _write}
// #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; } }};
// mutex helpers.
#define SCOPED_MUTEX(mutex) \
mutexLock(mutex); \
ON_SCOPE_EXIT(mutexUnlock(mutex))

View File

@@ -23,8 +23,6 @@ enum DumpLocationType {
DumpLocationType_DevNull,
// dump to stdio, ideal for custom mount points using devoptab, such as hdd.
DumpLocationType_Stdio,
// dump to custom locations found in locations.ini.
DumpLocationType_Network,
};
enum DumpLocationFlag {
@@ -33,8 +31,7 @@ enum DumpLocationFlag {
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_Usb | DumpLocationFlag_UsbS2S | DumpLocationFlag_DevNull | DumpLocationFlag_Stdio | DumpLocationFlag_Network,
DumpLocationFlag_All = DumpLocationFlag_SdCard | DumpLocationFlag_Usb | DumpLocationFlag_UsbS2S | DumpLocationFlag_DevNull | DumpLocationFlag_Stdio,
};
struct DumpEntry {
@@ -44,7 +41,6 @@ struct DumpEntry {
struct DumpLocation {
DumpEntry entry{};
location::Entries network{};
location::StdioEntries stdio{};
};

View File

@@ -1,10 +0,0 @@
#pragma once
#include <switch.h>
namespace sphaira::fatfs {
Result MountAll();
void UnmountAll();
} // namespace sphaira::fatfs

View File

@@ -4,12 +4,26 @@
#include <dirent.h>
#include <cstring>
#include <vector>
#include <span>
#include <string>
#include <string_view>
#include <sys/syslimits.h>
#include "defines.hpp"
namespace fs {
enum OpenMode : u32 {
OpenMode_Read = FsOpenMode_Read,
OpenMode_Write = FsOpenMode_Write,
OpenMode_Append = FsOpenMode_Append,
// enables buffering for stdio based files.
OpenMode_EnableBuffer = 1 << 16,
OpenMode_ReadBuffered = OpenMode_Read | OpenMode_EnableBuffer,
OpenMode_WriteBuffered = OpenMode_Write | OpenMode_EnableBuffer,
OpenMode_AppendBuffered = OpenMode_Append | OpenMode_EnableBuffer,
};
struct FsPath {
FsPath() = default;
@@ -138,20 +152,24 @@ struct FsPath {
return *this;
}
static constexpr bool path_equal(std::string_view a, std::string_view b) {
return a.length() == b.length() && !strncasecmp(a.data(), b.data(), a.length());
}
constexpr bool operator==(const FsPath& v) const noexcept {
return !strcasecmp(*this, v);
return path_equal(*this, v);
}
constexpr bool operator==(const char* v) const noexcept {
return !strcasecmp(*this, v);
return path_equal(*this, v);
}
constexpr bool operator==(const std::string& v) const noexcept {
return !strncasecmp(*this, v.data(), v.length());
return path_equal(*this, v);
}
constexpr bool operator==(const std::string_view v) const noexcept {
return !strncasecmp(*this, v.data(), v.length());
return path_equal(*this, v);
}
static consteval bool Test(const auto& str) {
@@ -164,7 +182,7 @@ struct FsPath {
return path[0] == str[0];
}
char s[FS_MAX_PATH]{};
char s[PATH_MAX]{};
};
inline FsPath operator+(const char* v, const FsPath& fp) {
@@ -183,8 +201,12 @@ inline FsPath operator+(const std::string_view& v, const FsPath& fp) {
}
// Fs seems to be limted to file paths of 255 characters.
// i've disabled this as network mounts will often have very long paths
// that do not have this limit.
// a proper fix would be to return an error if the path is too long and the path
// is native.
struct FsPathReal {
static constexpr inline size_t FS_REAL_MAX_LENGTH = 255;
static constexpr inline size_t FS_REAL_MAX_LENGTH = PATH_MAX;
constexpr FsPathReal(const FsPath& str) : FsPathReal{str.s} { }
explicit constexpr FsPathReal(const char* str) {
@@ -211,7 +233,7 @@ struct FsPathReal {
constexpr operator const char*() const { return s; }
constexpr operator std::string_view() const { return s; }
char s[FS_MAX_PATH];
char s[PATH_MAX];
};
// fwd
@@ -229,7 +251,6 @@ struct File {
fs::Fs* m_fs{};
FsFile m_native{};
std::FILE* m_stdio{};
s64 m_stdio_off{};
u32 m_mode{};
};
@@ -299,7 +320,7 @@ Result IsDirEmpty(fs::Fs* m_fs, const fs::FsPath& path, bool* out);
// helpers.
Result read_entire_file(Fs* fs, const FsPath& path, std::vector<u8>& out);
Result write_entire_file(Fs* fs, const FsPath& path, const std::vector<u8>& in, bool ignore_read_only = true);
Result write_entire_file(Fs* fs, const FsPath& path, std::span<const u8> in, bool ignore_read_only = true);
Result copy_entire_file(Fs* fs, const FsPath& dst, const FsPath& src, bool ignore_read_only = true);
struct Fs {
@@ -346,7 +367,7 @@ struct Fs {
Result read_entire_file(const FsPath& path, std::vector<u8>& out) {
return fs::read_entire_file(this, path, out);
}
Result write_entire_file(const FsPath& path, const std::vector<u8>& in) {
Result write_entire_file(const FsPath& path, std::span<const u8> in) {
return fs::write_entire_file(this, path, in, m_ignore_read_only);
}
Result copy_entire_file(const FsPath& dst, const FsPath& src) {

View File

@@ -15,8 +15,6 @@ enum class Type {
Sha1,
Sha256,
Null,
Deflate,
Zstd,
};
struct BaseSource {

View File

@@ -2,7 +2,7 @@
#include <functional>
namespace sphaira::haze {
namespace sphaira::libhaze {
bool Init();
bool IsInit();
@@ -15,4 +15,4 @@ using OnInstallClose = std::function<void()>;
void InitInstallMode(const OnInstallStart& on_start, const OnInstallWrite& on_write, const OnInstallClose& on_close);
void DisableInstallMode();
} // namespace sphaira::haze
} // namespace sphaira::libhaze

View File

@@ -3,23 +3,13 @@
#include <string>
#include <vector>
#include <switch.h>
// to import FsEntryFlags.
// todo: this should be part of a smaller header, such as filesystem_types.hpp
#include "ui/menus/filebrowser.hpp"
namespace sphaira::location {
struct Entry {
std::string name{};
std::string url{};
std::string user{};
std::string pass{};
std::string bearer{};
std::string pub_key{};
std::string priv_key{};
u16 port{};
};
using Entries = std::vector<Entry>;
auto Load() -> Entries;
void Add(const Entry& e);
using FsEntryFlag = ui::menu::filebrowser::FsEntryFlag;
// helper for hdd devices.
// this doesn't really belong in this header, however
@@ -29,14 +19,19 @@ struct StdioEntry {
std::string mount{};
// ums0: (USB Flash Disk)
std::string name{};
// set if read-only.
bool write_protect;
// FsEntryFlag
u32 flags{};
// optional dump path inside the mount point.
std::string dump_path{};
// set to hide for filebrowser.
bool fs_hidden{};
// set to hide in dump list.
bool dump_hidden{};
};
using StdioEntries = std::vector<StdioEntry>;
// set write=true to filter out write protected devices.
auto GetStdio(bool write) -> StdioEntries;
auto GetFat() -> StdioEntries;
} // namespace sphaira::location

View File

@@ -2,10 +2,11 @@
#include <switch.h>
#include <string>
#include <sys/syslimits.h>
namespace sphaira::swkbd {
Result ShowText(std::string& out, const char* guide = nullptr, const char* initial = nullptr, s64 len_min = -1, s64 len_max = FS_MAX_PATH);
Result ShowNumPad(s64& out, const char* guide = nullptr, const char* initial = nullptr, s64 len_min = -1, s64 len_max = FS_MAX_PATH);
Result ShowText(std::string& out, const char* header = nullptr, const char* guide = nullptr, const char* initial = nullptr, s64 len_min = -1, s64 len_max = PATH_MAX);
Result ShowNumPad(s64& out, const char* header = nullptr, const char* guide = nullptr, const char* initial = nullptr, s64 len_min = -1, s64 len_max = PATH_MAX);
} // namespace sphaira::swkbd

View File

@@ -59,6 +59,7 @@ void Clear();
// adds new entry to queue.
void PushAsync(u64 app_id);
void PushAsync(const std::span<const NsApplicationRecord> app_ids);
// gets entry without removing it from the queue.
auto GetAsync(u64 app_id) -> ThreadResultData*;
// single threaded title info fetch.

View File

@@ -7,6 +7,7 @@
#include "fs.hpp"
#include "option.hpp"
#include "hasher.hpp"
#include "nro.hpp"
#include <span>
namespace sphaira::ui::menu::filebrowser {
@@ -16,18 +17,16 @@ enum FsOption : u32 {
// can split screen.
FsOption_CanSplit = BIT(0),
// can upload files.
FsOption_CanUpload = BIT(1),
// can selected multiple files.
FsOption_CanSelect = BIT(2),
FsOption_CanSelect = BIT(1),
// shows the option to install.
FsOption_CanInstall = BIT(3),
FsOption_CanInstall = BIT(2),
// loads file assoc.
FsOption_LoadAssoc = BIT(4),
FsOption_LoadAssoc = BIT(3),
// do not prompt on exit even if not tabbed.
FsOption_DoNotPrompt = BIT(5),
FsOption_DoNotPrompt = BIT(4),
FsOption_Normal = FsOption_LoadAssoc | FsOption_CanInstall | FsOption_CanSplit | FsOption_CanUpload | FsOption_CanSelect,
FsOption_Normal = FsOption_LoadAssoc | FsOption_CanInstall | FsOption_CanSplit | FsOption_CanSelect,
FsOption_All = FsOption_DoNotPrompt | FsOption_Normal,
FsOption_Picker = FsOption_NONE,
};
@@ -39,7 +38,12 @@ enum FsEntryFlag {
// supports file assoc.
FsEntryFlag_Assoc = 1 << 1,
// this is an sd card, files can be launched from here.
FsEntryFlag_IsSd = 1 << 2,
FsEntryFlag_IsSd = 1 << 2, // todo: remove this.
// do not stat files in this entry (faster for network mount).
FsEntryFlag_NoStatFile = 1 << 3,
FsEntryFlag_NoStatDir = 1 << 4,
FsEntryFlag_NoRandomReads = 1 << 5,
FsEntryFlag_NoRandomWrites = 1 << 6,
};
enum class FsType {
@@ -90,6 +94,22 @@ struct FsEntry {
return flags & FsEntryFlag_IsSd;
}
auto IsNoStatFile() const -> bool {
return flags & FsEntryFlag_NoStatFile;
}
auto IsNoStatDir() const -> bool {
return flags & FsEntryFlag_NoStatDir;
}
auto IsNoRandomReads() const -> bool {
return flags & FsEntryFlag_NoRandomReads;
}
auto IsNoRandomWrites() const -> bool {
return flags & FsEntryFlag_NoRandomWrites;
}
auto IsSame(const FsEntry& e) const {
return root == e.root && type == e.type;
}
@@ -460,6 +480,15 @@ protected:
std::vector<std::string> m_filter{};
// local copy of nro entries that is loaded in LoadAssocEntriesPath()
// if homebrew::GetNroEntries() returns nothing, usually due to
// the menu not being loaded.
// this is a bit of a hack to support replacing the homebrew menu tab,
// sphaira wasn't really designed for this.
// however this will work for now, until i add support for additional
// nro scan mounts, at which point this won't scale.
std::vector<NroEntry> m_nro_entries{};
option::OptionLong m_sort{INI_SECTION, "sort", SortType::SortType_Alphabetical};
option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Descending};
option::OptionBool m_show_hidden{INI_SECTION, "show_hidden", false};

View File

@@ -43,6 +43,8 @@ enum OrderType {
using LayoutType = grid::LayoutType;
void SignalChange();
struct Menu final : grid::Menu {
Menu(u32 flags);
~Menu();

View File

@@ -34,7 +34,7 @@ auto GetNroEntries() -> std::span<const NroEntry>;
void SignalChange();
struct Menu final : grid::Menu {
Menu();
Menu(u32 flags);
~Menu();
auto GetShortTitle() const -> const char* override { return "Apps"; };

View File

@@ -42,7 +42,7 @@ struct MiscMenuEntry {
}
};
auto GetMiscMenuEntries() -> std::span<const MiscMenuEntry>;
auto GetMenuMenuEntries() -> std::span<const MiscMenuEntry>;
// this holds 2 menus and allows for switching between them
struct MainMenu final : Widget {

View File

@@ -13,12 +13,14 @@ enum MenuFlag {
struct PolledData {
struct tm tm{};
u32 battery_percetange{};
PsmChargerType charger_type{};
NifmInternetConnectionType type{};
NifmInternetConnectionStatus status{};
u32 strength{};
u32 ip{};
s64 sd_free{1};
s64 sd_total{1};
s64 emmc_free{1};
s64 emmc_total{1};
};
struct MenuBase : Widget {

View File

@@ -12,6 +12,14 @@
namespace sphaira::ui::menu::save {
enum BackupFlag {
BackupFlag_None = 0,
// option to allow the user to set the save file name.
BackupFlag_SetName = 1 << 0,
// set if this is a auto backup (on restore).
BackupFlag_IsAuto = 1 << 1,
};
struct Entry final : FsSaveDataInfo {
NacpLanguageEntry lang{};
int image{};
@@ -82,13 +90,13 @@ private:
void DisplayOptions();
void BackupSaves(std::vector<std::reference_wrapper<Entry>>& entries);
void BackupSaves(std::vector<std::reference_wrapper<Entry>>& entries, u32 flags);
void RestoreSave();
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, 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;
auto BuildSavePath(const Entry& e, u32 flags) const -> fs::FsPath;
Result RestoreSaveInternal(ProgressBox* pbox, const Entry& e, const fs::FsPath& path);
Result BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& location, Entry& e, u32 flags);
Result BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& location, std::span<const std::reference_wrapper<Entry>> entries, u32 flags);
Result MountSaveFs();

View File

@@ -42,6 +42,8 @@ void drawScrollbar2(NVGcontext*, const Theme*, s64 index_off, s64 count, s64 row
void drawAppLable(NVGcontext* vg, const Theme*, ScrollingText& st, float x, float y, float w, const char* name);
void drawSpinner(NVGcontext* vg, const Theme*, float cx, float cy, float r, float t);
void updateHighlightAnimation();
void getHighlightAnimation(float* gradientX, float* gradientY, float* color);

View File

@@ -11,6 +11,7 @@ namespace sphaira::ui {
struct ProgressBox;
using ProgressBoxCallback = std::function<Result(ProgressBox*)>;
using ProgressBoxDoneCallback = std::function<void(Result rc)>;
// using CancelCallback = std::function<void()>;
struct ProgressBox final : Widget {
ProgressBox(
@@ -39,6 +40,9 @@ struct ProgressBox final : Widget {
auto ShouldExit() -> bool;
auto ShouldExitResult() -> Result;
void AddCancelEvent(UEvent* event);
void RemoveCancelEvent(const UEvent* event);
// helper functions
auto CopyFile(fs::Fs* fs_src, fs::Fs* fs_dst, const fs::FsPath& src, const fs::FsPath& dst, bool single_threaded = false) -> Result;
auto CopyFile(fs::Fs* fs, const fs::FsPath& src, const fs::FsPath& dst, bool single_threaded = false) -> Result;
@@ -82,6 +86,7 @@ private:
Thread m_thread{};
ThreadData m_thread_data{};
ProgressBoxDoneCallback m_done{};
std::vector<UEvent*> m_cancel_events{};
// shared data start.
std::string m_action{};

View File

@@ -7,6 +7,7 @@
#include <memory>
#include <concepts>
#include <utility>
#include <sys/syslimits.h>
namespace sphaira::ui {
@@ -43,6 +44,14 @@ public:
m_depends_click = depends_click;
}
void SetDirty(bool dirty = true) {
m_dirty = dirty;
}
auto IsDirty() const -> bool {
return m_dirty;
}
protected:
auto IsEnabled() const -> bool {
if (m_depends_callback) {
@@ -68,6 +77,7 @@ private:
DependsClickCallback m_depends_click{};
ScrollingText m_scolling_title{};
ScrollingText m_scolling_value{};
bool m_dirty{};
};
template<typename T>
@@ -174,12 +184,27 @@ private:
class SidebarEntryTextInput final : public SidebarEntryTextBase {
public:
explicit SidebarEntryTextInput(const std::string& title, const std::string& value, const std::string& guide = {}, s64 len_min = -1, s64 len_max = FS_MAX_PATH, const std::string& info = "");
using Callback = std::function<void(SidebarEntryTextInput* input)>;
public:
// uses normal keyboard.
explicit SidebarEntryTextInput(const std::string& title, const std::string& value, const std::string& header = {}, const std::string& guide = {}, s64 len_min = -1, s64 len_max = PATH_MAX, const std::string& info = "", const Callback& callback = nullptr);
// uses numpad.
explicit SidebarEntryTextInput(const std::string& title, s64 value, const std::string& header = {}, const std::string& guide = {}, s64 len_min = -1, s64 len_max = PATH_MAX, const std::string& info = "", const Callback& callback = nullptr);
auto GetNumValue() const -> s64 {
return std::stoul(GetValue());
}
void SetNumValue(s64 value) {
SetValue(std::to_string(value));
}
private:
const std::string m_header;
const std::string m_guide;
const s64 m_len_min;
const s64 m_len_max;
const Callback m_callback;
};
class SidebarEntryFilePicker final : public SidebarEntryTextBase {
@@ -199,12 +224,12 @@ class Sidebar : public Widget {
public:
enum class Side { LEFT, RIGHT };
using Items = std::vector<std::unique_ptr<SidebarEntryBase>>;
using OnExitWhenChangedCallback = std::function<void()>;
public:
explicit Sidebar(const std::string& title, Side side, Items&& items);
explicit Sidebar(const std::string& title, Side side);
explicit Sidebar(const std::string& title, const std::string& sub, Side side, Items&& items);
explicit Sidebar(const std::string& title, const std::string& sub, Side side);
explicit Sidebar(const std::string& title, Side side, float width = 450.f);
explicit Sidebar(const std::string& title, const std::string& sub, Side side, float width = 450.f);
~Sidebar();
auto Update(Controller* controller, TouchInfo* touch) -> void override;
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
@@ -218,6 +243,12 @@ public:
return (T*)Add(std::make_unique<T>(std::forward<Args>(args)...));
}
// sets a callback that is called on exit when the any options were changed.
// the change detection isn't perfect, it just checks if the A button was pressed...
void SetOnExitWhenChanged(const OnExitWhenChangedCallback& cb) {
m_on_exit_when_changed = cb;
}
private:
void SetIndex(s64 index);
void SetupButtons();
@@ -226,19 +257,28 @@ private:
const std::string m_title;
const std::string m_sub;
const Side m_side;
Items m_items;
Items m_items{};
s64 m_index{};
std::unique_ptr<List> m_list;
std::unique_ptr<List> m_list{};
Vec4 m_top_bar{};
Vec4 m_bottom_bar{};
Vec2 m_title_pos{};
Vec4 m_base_pos{};
OnExitWhenChangedCallback m_on_exit_when_changed{};
static constexpr float m_title_size{28.f};
// static constexpr Vec2 box_size{380.f, 70.f};
static constexpr Vec2 m_box_size{400.f, 70.f};
};
class FormSidebar : public Sidebar {
public:
explicit FormSidebar(const std::string& title) : Sidebar{title, Side::LEFT, 540.f} {
// explicit FormSidebar(const std::string& title) : Sidebar{title, Side::LEFT} {
}
};
} // namespace sphaira::ui

View File

@@ -366,6 +366,81 @@ struct Action final {
std::string m_hint{};
};
struct GenericHidState {
GenericHidState() {
Reset();
}
void Reset() {
buttons_cur = 0;
buttons_old = 0;
}
u64 GetButtons() const {
return buttons_cur;
}
u64 GetButtonsDown() const {
return buttons_cur & ~buttons_old;
}
u64 GetButtonsUp() const {
return ~buttons_cur & buttons_old;
}
virtual void Update() = 0;
protected:
u64 buttons_cur;
u64 buttons_old;
};
struct KeyboardState final : GenericHidState {
struct MapEntry {
HidKeyboardKey key;
u64 button;
};
using Map = std::span<const MapEntry>;
void Init(Map map) {
m_map = map;
Reset();
}
void Update() override {
buttons_old = buttons_cur;
buttons_cur = 0;
if (!hidGetKeyboardStates(&m_state, 1)) {
return;
}
const auto ctrl = m_state.modifiers & HidKeyboardModifier_Control;
const auto shift = m_state.modifiers & HidKeyboardModifier_Shift;
for (const auto& map : m_map) {
if (hidKeyboardStateGetKey(&m_state, map.key)) {
if (shift && map.button == static_cast<u64>(Button::L)) {
buttons_cur |= static_cast<u64>(Button::L2);
} else if (shift && map.button == static_cast<u64>(Button::R)) {
buttons_cur |= static_cast<u64>(Button::R2);
} else if (ctrl && map.button == static_cast<u64>(Button::L)) {
buttons_cur |= static_cast<u64>(Button::L3);
} else if (ctrl && map.button == static_cast<u64>(Button::R)) {
buttons_cur |= static_cast<u64>(Button::R3);
} else {
buttons_cur |= map.button;
}
}
}
}
private:
Map m_map{};
HidKeyboardState m_state{};
};
struct Controller {
u64 m_kdown{};
u64 m_kheld{};
@@ -399,15 +474,29 @@ struct Controller {
void UpdateButtonHeld(u64 buttons, double delta) {
if (m_kdown & buttons) {
m_step = 50;
m_step_max = m_MAX_STEP;
m_step = m_INC_STEP;
m_counter = 0;
m_step_max_counter = 0;
} else if (m_kheld & buttons) {
m_counter += m_step * delta;
// if we are at the max, ignore the delta and go as fast as the frame rate.
if (m_step_max == m_MAX) {
m_counter = m_MAX;
}
if (m_counter >= m_MAX) {
m_kdown |= m_kheld & buttons;
m_counter = 0;
m_step = std::min(m_step + 50, m_MAX_STEP);
m_step = std::min(m_step + m_INC_STEP, m_step_max);
// slowly speed up until we reach 1 button down per frame.
m_step_max_counter++;
if (m_step_max_counter >= 5) {
m_step_max_counter = 0;
m_step_max = std::min(m_step_max + m_INC_STEP, m_MAX);
}
}
}
}
@@ -415,8 +504,12 @@ struct Controller {
private:
static constexpr double m_MAX = 1000;
static constexpr double m_MAX_STEP = 250;
double m_step = 50;
static constexpr double m_INC_STEP = 50;
double m_step_max = m_MAX_STEP;
double m_step = m_INC_STEP;
double m_counter = 0;
int m_step_max_counter = 0;
};
} // namespace sphaira

View File

@@ -24,6 +24,14 @@ struct Usb {
// Result OpenFile(u32 index, s64& file_size);
Result CloseFile();
auto GetOpenResult() const {
return m_open_result;
}
auto GetCancelEvent() {
return m_usb->GetCancelEvent();
}
private:
Result SendAndVerify(const void* data, u32 size, u64 timeout, api::ResultPacket* out = nullptr);
Result SendAndVerify(const void* data, u32 size, api::ResultPacket* out = nullptr);

View File

@@ -25,6 +25,14 @@ struct Usb {
Result OpenFile(u32 index, s64& file_size);
Result CloseFile();
auto GetOpenResult() const {
return m_open_result;
}
auto GetCancelEvent() {
return m_usb->GetCancelEvent();
}
private:
Result SendAndVerify(const void* data, u32 size, u64 timeout, api::ResultPacket* out = nullptr);
Result SendAndVerify(const void* data, u32 size, api::ResultPacket* out = nullptr);

View File

@@ -29,6 +29,14 @@ struct Usb {
Result file_transfer_loop();
auto GetOpenResult() const {
return m_open_result;
}
auto GetCancelEvent() {
return m_usb->GetCancelEvent();
}
private:
Result SendResult(u32 result, u32 arg3 = 0, u32 arg4 = 0);

View File

@@ -2,34 +2,42 @@
#include "fs.hpp"
#include "yati/source/base.hpp"
#include "location.hpp"
#include <switch.h>
#include <memory>
namespace sphaira::devoptab {
// mounts to "lower_case_hex_id:/"
Result MountSaveSystem(u64 id, fs::FsPath& out_path);
void UnmountSave(u64 id);
Result MountZip(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path);
void UmountZip(const fs::FsPath& mount);
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);
Result MountVfsAll();
Result MountWebdavAll();
Result MountHttpAll();
Result MountFtpAll();
Result MountSftpAll();
Result MountNfsAll();
Result MountSmb2All();
Result MountFatfsAll();
Result MountGameAll();
Result MountInternalMounts();
Result GetNetworkDevices(location::StdioEntries& out);
void UmountAllNeworkDevices();
void UmountNeworkDevice(const fs::FsPath& mount);
// manually set the array so that we can avoid nullptr access.
// SEE: https://github.com/devkitPro/newlib/issues/35
void FixDkpBug();
void DisplayDevoptabSideBar();
} // namespace sphaira::devoptab

View File

@@ -2,8 +2,13 @@
#include "yati/source/file.hpp"
#include "utils/lru.hpp"
#include "location.hpp"
#include <memory>
#include <optional>
#include <span>
#include <functional>
#include <unordered_map>
#include <curl/curl.h>
namespace sphaira::devoptab::common {
@@ -81,6 +86,142 @@ private:
std::vector<BufferedFileData> buffered_large{}; // 1MiB
};
bool fix_path(const char* str, char* out);
bool fix_path(const char* str, char* out, bool strip_leading_slash = false);
void update_devoptab_for_read_only(devoptab_t* devoptab, bool read_only);
struct PushPullThreadData {
static constexpr size_t MAX_BUFFER_SIZE = 1024 * 64; // 64KB max buffer
explicit PushPullThreadData(CURL* _curl);
virtual ~PushPullThreadData();
Result CreateAndStart();
void Cancel();
bool IsRunning();
// only set curl=true if called from a curl callback.
size_t PullData(char* data, size_t total_size, bool curl = false);
size_t PushData(const char* data, size_t total_size, bool curl = false);
static size_t progress_callback(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow);
private:
static void thread_func(void* arg);
public:
CURL* const curl{};
std::vector<char> buffer{};
Mutex mutex{};
CondVar can_push{};
CondVar can_pull{};
long code{};
bool error{};
bool finished{};
bool started{};
private:
Thread thread{};
};
struct MountConfig {
std::string name{};
std::string url{};
std::string user{};
std::string pass{};
std::string dump_path{};
long port{};
long timeout{};
bool read_only{};
bool no_stat_file{true};
bool no_stat_dir{true};
bool fs_hidden{};
bool dump_hidden{};
std::unordered_map<std::string, std::string> extra{};
};
using MountConfigs = std::vector<MountConfig>;
struct PullThreadData final : PushPullThreadData {
using PushPullThreadData::PushPullThreadData;
static size_t pull_thread_callback(char *ptr, size_t size, size_t nmemb, void *userdata);
};
struct PushThreadData final : PushPullThreadData {
using PushPullThreadData::PushPullThreadData;
static size_t push_thread_callback(const char *ptr, size_t size, size_t nmemb, void *userdata);
};
struct MountDevice {
MountDevice(const MountConfig& _config) : config{_config} {}
virtual ~MountDevice() = default;
virtual bool fix_path(const char* str, char* out, bool strip_leading_slash = false) {
return common::fix_path(str, out, strip_leading_slash);
}
virtual bool Mount() = 0;
virtual int devoptab_open(void *fileStruct, const char *path, int flags, int mode) { return -EIO; }
virtual int devoptab_close(void *fd) { return -EIO; }
virtual ssize_t devoptab_read(void *fd, char *ptr, size_t len) { return -EIO; }
virtual ssize_t devoptab_write(void *fd, const char *ptr, size_t len) { return -EIO; }
virtual ssize_t devoptab_seek(void *fd, off_t pos, int dir) { return 0; }
virtual int devoptab_fstat(void *fd, struct stat *st) { return -EIO; }
virtual int devoptab_unlink(const char *path) { return -EIO; }
virtual int devoptab_rename(const char *oldName, const char *newName) { return -EIO; }
virtual int devoptab_mkdir(const char *path, int mode) { return -EIO; }
virtual int devoptab_rmdir(const char *path) { return -EIO; }
virtual int devoptab_diropen(void* fd, const char *path) { return -EIO; }
virtual int devoptab_dirreset(void* fd) { return -EIO; }
virtual int devoptab_dirnext(void* fd, char *filename, struct stat *filestat) { return -EIO; }
virtual int devoptab_dirclose(void* fd) { return -EIO; }
virtual int devoptab_lstat(const char *path, struct stat *st) { return -EIO; }
virtual int devoptab_ftruncate(void *fd, off_t len) { return -EIO; }
virtual int devoptab_statvfs(const char *_path, struct statvfs *buf) { return -EIO; }
virtual int devoptab_fsync(void *fd) { return -EIO; }
virtual int devoptab_utimes(const char *_path, const struct timeval times[2]) { return -EIO; }
const MountConfig config;
};
struct MountCurlDevice : MountDevice {
using MountDevice::MountDevice;
virtual ~MountCurlDevice();
PushThreadData* CreatePushData(CURL* curl, const std::string& url, size_t offset);
PullThreadData* CreatePullData(CURL* curl, const std::string& url, bool append = false);
virtual bool Mount();
virtual void curl_set_common_options(CURL* curl, const std::string& url);
static size_t write_memory_callback(char *ptr, size_t size, size_t nmemb, void *userdata);
static size_t write_data_callback(char *ptr, size_t size, size_t nmemb, void *userdata);
static size_t read_data_callback(char *ptr, size_t size, size_t nmemb, void *userdata);
static std::string html_decode(const std::string_view& str);
static std::string url_decode(const std::string& str);
std::string build_url(const std::string& path, bool is_dir);
protected:
CURL* curl{};
CURL* transfer_curl{};
private:
// path extracted from the url.
std::string m_url_path{};
CURLU* curlu{};
CURLSH* m_curl_share{};
RwLock m_rwlocks[CURL_LOCK_DATA_LAST]{};
bool m_mounted{};
};
void LoadConfigsFromIni(const fs::FsPath& path, MountConfigs& out_configs);
using CreateDeviceCallback = std::function<std::unique_ptr<MountDevice>(const MountConfig& config)>;
Result MountNetworkDevice(const CreateDeviceCallback& create_device, size_t file_size, size_t dir_size, const char* name, bool force_read_only = false);
// same as above but takes in the device and expects the mount name to be set.
bool MountNetworkDevice2(std::unique_ptr<MountDevice>&& device, const MountConfig& config, size_t file_size, size_t dir_size, const char* name, const char* mount_name);
bool MountReadOnlyIndexDevice(const CreateDeviceCallback& create_device, size_t file_size, size_t dir_size, const char* name, fs::FsPath& out_path);
} // namespace sphaira::devoptab::common

View File

@@ -22,4 +22,11 @@ constexpr inline T AlignDown(T value, T align) {
return value &~ (align - 1);
}
// formats size to 1.23 MB in 1024 base.
// only uses 32 bytes so its SSO optimised, not need to cache.
std::string formatSizeStorage(u64 size);
// formats size to 1.23 MB in 1000 base (used for progress bars).
std::string formatSizeNetwork(u64 size);
} // namespace sphaira::utils

View File

@@ -43,6 +43,14 @@ struct Usb final : Base {
return m_usb->CloseFile();
}
auto GetOpenResult() const {
return m_usb->GetOpenResult();
}
auto GetCancelEvent() {
return m_usb->GetCancelEvent();
}
private:
std::unique_ptr<usb::install::Usb> m_usb{};
};

View File

@@ -22,11 +22,11 @@
#include "haze_helper.hpp"
#include "web.hpp"
#include "swkbd.hpp"
#include "fatfs.hpp"
#include "usbdvd.hpp"
#include "utils/profile.hpp"
#include "utils/thread.hpp"
#include "utils/devoptab.hpp"
#include <nanovg_dk.h>
#include <minIni.h>
@@ -37,7 +37,10 @@
#include <ctime>
#include <span>
#include <dirent.h>
#include <usbhsfs.h>
#ifdef ENABLE_LIBUSBHSFS
#include <usbhsfs.h>
#endif // ENABLE_LIBUSBHSFS
extern "C" {
u32 __nx_applet_exit_mode = 0;
@@ -71,6 +74,37 @@ struct NszOption {
const char* name;
};
constexpr KeyboardState::MapEntry KEYBOARD_BUTTON_MAP[] = {
{HidKeyboardKey_UpArrow, static_cast<u64>(Button::DPAD_UP)},
{HidKeyboardKey_DownArrow, static_cast<u64>(Button::DPAD_DOWN)},
{HidKeyboardKey_LeftArrow, static_cast<u64>(Button::DPAD_LEFT)},
{HidKeyboardKey_RightArrow, static_cast<u64>(Button::DPAD_RIGHT)},
{HidKeyboardKey_W, static_cast<u64>(Button::DPAD_UP)},
{HidKeyboardKey_S, static_cast<u64>(Button::DPAD_DOWN)},
{HidKeyboardKey_A, static_cast<u64>(Button::DPAD_LEFT)},
{HidKeyboardKey_D, static_cast<u64>(Button::DPAD_RIGHT)},
// options (may swap).
{HidKeyboardKey_Z, static_cast<u64>(Button::Y)},
{HidKeyboardKey_X, static_cast<u64>(Button::X)},
// menus.
{HidKeyboardKey_Q, static_cast<u64>(Button::L)},
{HidKeyboardKey_E, static_cast<u64>(Button::R)},
// select and back.
{HidKeyboardKey_Return, static_cast<u64>(Button::A)},
{HidKeyboardKey_Space, static_cast<u64>(Button::A)},
{HidKeyboardKey_Backspace, static_cast<u64>(Button::B)},
// exit.
{HidKeyboardKey_Escape, static_cast<u64>(Button::START)},
// idk what this should map to.
{HidKeyboardKey_R, static_cast<u64>(Button::SELECT)},
};
constexpr NszOption NSZ_COMPRESS_LEVEL_OPTIONS[] = {
{ .value = 0, .name = "Level 0 (no compression)" },
{ .value = 1, .name = "Level 1" },
@@ -542,7 +576,14 @@ void App::Loop() {
}
auto App::Push(std::unique_ptr<ui::Widget>&& widget) -> void {
log_write("[Mui] pushing widget\n");
log_write("[APP] pushing widget\n");
// when freeing widges, this may cancel a transfer which causes it to push
// an error box, so check if we are quitting first before adding.
if (g_app->m_quit) {
log_write("[APP] is quitting, not pushing widget\n");
return;
}
// check if the widget wants to pop before adding.
// this can happen if something failed in the constructor and the widget wants to exit.
@@ -711,10 +752,6 @@ auto App::GetTextScrollSpeed() -> long {
return g_app->m_text_scroll_speed.Get();
}
auto App::Get12HourTimeEnable() -> bool {
return g_app->m_12hour_time.Get();
}
auto App::GetNszCompressLevel() -> u8 {
return NSZ_COMPRESS_LEVEL_OPTIONS[App::GetApp()->m_nsz_compress_level.Get()].value;
}
@@ -741,6 +778,7 @@ void App::SetNxlinkEnable(bool enable) {
void App::SetHddEnable(bool enable) {
if (App::GetHddEnable() != enable) {
g_app->m_hdd_enabled.Set(enable);
#ifdef ENABLE_LIBUSBHSFS
if (enable) {
if (App::GetWriteProtect()) {
usbHsFsSetFileSystemMountFlags(UsbHsFsMountFlags_ReadOnly);
@@ -749,6 +787,7 @@ void App::SetHddEnable(bool enable) {
} else {
usbHsFsExit();
}
#endif // ENABLE_LIBUSBHSFS
}
}
@@ -756,11 +795,13 @@ void App::SetWriteProtect(bool enable) {
if (App::GetWriteProtect() != enable) {
g_app->m_hdd_write_protect.Set(enable);
#ifdef ENABLE_LIBUSBHSFS
if (enable) {
usbHsFsSetFileSystemMountFlags(UsbHsFsMountFlags_ReadOnly);
} else {
usbHsFsSetFileSystemMountFlags(0);
}
#endif // ENABLE_LIBUSBHSFS
}
}
@@ -897,29 +938,31 @@ void App::SetThemeMusicEnable(bool enable) {
}
}
void App::Set12HourTimeEnable(bool enable) {
g_app->m_12hour_time.Set(enable);
}
void App::SetMtpEnable(bool enable) {
if (App::GetMtpEnable() != enable) {
g_app->m_mtp_enabled.Set(enable);
#ifdef ENABLE_LIBHAZE
if (enable) {
haze::Init();
libhaze::Init();
} else {
haze::Exit();
libhaze::Exit();
}
#endif // ENABLE_LIBHAZE
}
}
void App::SetFtpEnable(bool enable) {
if (App::GetFtpEnable() != enable) {
g_app->m_ftp_enabled.Set(enable);
#ifdef ENABLE_FTPSRV
if (enable) {
ftpsrv::Init();
} else {
ftpsrv::Exit();
}
#endif // ENABLE_FTPSRV
}
}
@@ -1006,6 +1049,16 @@ void App::Poll() {
hidGetTouchScreenStates(&state, 1);
m_touch_info.is_clicked = false;
// todo:
#if 0
HidMouseState mouse_state{};
hidGetMouseStates(&mouse_state, 1);
if (mouse_state.buttons) {
log_write("[MOUSE] buttons: 0x%X x: %d y: %d dx: %d dy: %d wx: %d wy: %d\n", mouse_state.buttons, mouse_state.x, mouse_state.y, mouse_state.delta_x, mouse_state.delta_y, mouse_state.wheel_delta_x, mouse_state.wheel_delta_y);
}
#endif
// todo: replace old touch code with gestures from below
#if 0
static HidGestureState prev_gestures[17]{};
@@ -1051,6 +1104,7 @@ void App::Poll() {
memcpy(prev_gestures, gestures, sizeof(gestures));
#endif
// todo: support mouse scroll / touch.
if (state.count == 1 && !m_touch_info.is_touching) {
m_touch_info.initial = m_touch_info.cur = state.touches[0];
m_touch_info.is_touching = true;
@@ -1074,14 +1128,29 @@ void App::Poll() {
}
}
u64 kdown = 0;
u64 kup = 0;
u64 kheld = 0;
// todo: better implement this to match hos
if (!m_touch_info.is_touching && !m_touch_info.is_clicked) {
// controller.
padUpdate(&m_pad);
m_controller.m_kdown = padGetButtonsDown(&m_pad);
m_controller.m_kheld = padGetButtons(&m_pad);
m_controller.m_kup = padGetButtonsUp(&m_pad);
m_controller.UpdateButtonHeld(static_cast<u64>(Button::ANY_DIRECTION), m_delta_time);
kdown |= padGetButtonsDown(&m_pad);
kheld |= padGetButtons(&m_pad);
kup |= padGetButtonsUp(&m_pad);
// keyboard.
m_keyboard.Update();
kdown |= m_keyboard.GetButtonsDown();
kheld |= m_keyboard.GetButtons();
kup |= m_keyboard.GetButtonsUp();
}
m_controller.m_kdown = kdown;
m_controller.m_kheld = kheld;
m_controller.m_kup = kup;
m_controller.UpdateButtonHeld(static_cast<u64>(Button::ANY_DIRECTION), m_delta_time);
}
void App::Update() {
@@ -1385,6 +1454,20 @@ void App::SetBackgroundMusicPause(bool pause) {
}
}
Result App::GetSdSize(s64* free, s64* total) {
fs::FsNativeContentStorage fs{FsContentStorageId_SdCard};
R_TRY(fs.GetFreeSpace("/", free));
R_TRY(fs.GetTotalSpace("/", total));
R_SUCCEED();
}
Result App::GetEmmcSize(s64* free, s64* total) {
fs::FsNativeContentStorage fs{FsContentStorageId_User};
R_TRY(fs.GetFreeSpace("/", free));
R_TRY(fs.GetTotalSpace("/", total));
R_SUCCEED();
}
App::App(const char* argv0) {
// boost mode is enabled in userAppInit().
ON_SCOPE_EXIT(App::SetBoostMode(false));
@@ -1407,7 +1490,7 @@ App::App(const char* argv0) {
// init fs for app use.
m_fs = std::make_shared<fs::FsNativeSd>(true);
auto cb = [](const mTCHAR *Section, const mTCHAR *Key, const mTCHAR *Value, void *UserData) -> int {
static const auto cb = [](const mTCHAR *Section, const mTCHAR *Key, const mTCHAR *Value, void *UserData) -> int {
auto app = static_cast<App*>(UserData);
if (!std::strcmp(Section, INI_SECTION)) {
@@ -1421,8 +1504,9 @@ App::App(const char* argv0) {
else if (app->m_default_music.LoadFrom(Key, Value)) {}
else if (app->m_theme_path.LoadFrom(Key, Value)) {}
else if (app->m_theme_music.LoadFrom(Key, Value)) {}
else if (app->m_12hour_time.LoadFrom(Key, Value)) {}
else if (app->m_show_ip_addr.LoadFrom(Key, Value)) {}
else if (app->m_language.LoadFrom(Key, Value)) {}
else if (app->m_center_menu.LoadFrom(Key, Value)) {}
else if (app->m_left_menu.LoadFrom(Key, Value)) {}
else if (app->m_right_menu.LoadFrom(Key, Value)) {}
else if (app->m_install_sysmmc.LoadFrom(Key, Value)) {}
@@ -1458,6 +1542,37 @@ App::App(const char* argv0) {
else if (app->m_nsz_compress_ldm.LoadFrom(Key, Value)) {}
else if (app->m_nsz_compress_block.LoadFrom(Key, Value)) {}
else if (app->m_nsz_compress_block_exponent.LoadFrom(Key, Value)) {}
} else if (!std::strcmp(Section, "ftp")) {
if (app->m_ftp_port.LoadFrom(Key, Value)) {}
else if (app->m_ftp_anon.LoadFrom(Key, Value)) {}
else if (app->m_ftp_user.LoadFrom(Key, Value)) {}
else if (app->m_ftp_pass.LoadFrom(Key, Value)) {}
else if (app->m_ftp_show_album.LoadFrom(Key, Value)) {}
else if (app->m_ftp_show_ams_contents.LoadFrom(Key, Value)) {}
else if (app->m_ftp_show_bis_storage.LoadFrom(Key, Value)) {}
else if (app->m_ftp_show_bis_fs.LoadFrom(Key, Value)) {}
else if (app->m_ftp_show_content_system.LoadFrom(Key, Value)) {}
else if (app->m_ftp_show_content_user.LoadFrom(Key, Value)) {}
else if (app->m_ftp_show_content_sd.LoadFrom(Key, Value)) {}
// else if (app->m_ftp_show_content_sd0.LoadFrom(Key, Value)) {}
// else if (app->m_ftp_show_custom_system.LoadFrom(Key, Value)) {}
// else if (app->m_ftp_show_custom_sd.LoadFrom(Key, Value)) {}
else if (app->m_ftp_show_games.LoadFrom(Key, Value)) {}
else if (app->m_ftp_show_install.LoadFrom(Key, Value)) {}
else if (app->m_ftp_show_mounts.LoadFrom(Key, Value)) {}
else if (app->m_ftp_show_switch.LoadFrom(Key, Value)) {}
} else if (!std::strcmp(Section, "mtp")) {
if (app->m_mtp_vid.LoadFrom(Key, Value)) {}
else if (app->m_mtp_pid.LoadFrom(Key, Value)) {}
else if (app->m_mtp_allocate_file.LoadFrom(Key, Value)) {}
else if (app->m_mtp_show_album.LoadFrom(Key, Value)) {}
else if (app->m_mtp_show_content_sd.LoadFrom(Key, Value)) {}
else if (app->m_mtp_show_content_system.LoadFrom(Key, Value)) {}
else if (app->m_mtp_show_content_user.LoadFrom(Key, Value)) {}
else if (app->m_mtp_show_games.LoadFrom(Key, Value)) {}
else if (app->m_mtp_show_install.LoadFrom(Key, Value)) {}
else if (app->m_mtp_show_mounts.LoadFrom(Key, Value)) {}
else if (app->m_mtp_show_speedtest.LoadFrom(Key, Value)) {}
}
return 1;
@@ -1472,7 +1587,7 @@ App::App(const char* argv0) {
if (App::GetLogEnable()) {
log_file_init();
log_write("hello world v%s\n", APP_VERSION_HASH);
log_write("hello world v%s\n", APP_DISPLAY_VERSION);
}
// anything that can be async loaded should be placed in here in order
@@ -1492,6 +1607,7 @@ App::App(const char* argv0) {
m_fs->CreateDirectory("/config/sphaira/themes");
m_fs->CreateDirectory("/config/sphaira/github");
m_fs->CreateDirectory("/config/sphaira/i18n");
m_fs->CreateDirectory("/config/sphaira/mount");
}
{
@@ -1547,21 +1663,28 @@ App::App(const char* argv0) {
}
}
devoptab::FixDkpBug();
#ifdef ENABLE_LIBHAZE
if (App::GetMtpEnable()) {
SCOPED_TIMESTAMP("mtp init");
haze::Init();
libhaze::Init();
}
#endif // ENABLE_LIBHAZE
#ifdef ENABLE_FTPSRV
if (App::GetFtpEnable()) {
SCOPED_TIMESTAMP("ftp init");
ftpsrv::Init();
}
#endif // ENABLE_FTPSRV
if (App::GetNxlinkEnable()) {
SCOPED_TIMESTAMP("nxlink init");
nxlinkInitialize(nxlink_callback);
}
#ifdef ENABLE_LIBUSBHSFS
if (App::GetHddEnable()) {
SCOPED_TIMESTAMP("hdd init");
if (App::GetWriteProtect()) {
@@ -1570,26 +1693,85 @@ App::App(const char* argv0) {
usbHsFsInitialize(1);
}
#endif // ENABLE_LIBUSBHSFS
{
SCOPED_TIMESTAMP("fat init");
if (R_FAILED(fatfs::MountAll())) {
log_write("[FAT] failed to mount bis\n");
}
}
#ifdef ENABLE_LIBUSBDVD
{
SCOPED_TIMESTAMP("usbdvd init");
if (R_FAILED(usbdvd::MountAll())) {
log_write("[USBDVD] failed to mount\n");
}
}
#endif // ENABLE_LIBUSBDVD
{
SCOPED_TIMESTAMP("curl init");
curl::Init();
}
// this has to come after curl init as it inits curl global.
{
SCOPED_TIMESTAMP("vfs init");
devoptab::MountVfsAll();
}
#ifdef ENABLE_DEVOPTAB_HTTP
{
SCOPED_TIMESTAMP("http init");
devoptab::MountHttpAll();
}
#endif // ENABLE_DEVOPTAB_HTTP
#ifdef ENABLE_DEVOPTAB_WEBDAV
{
SCOPED_TIMESTAMP("webdav init");
devoptab::MountWebdavAll();
}
#endif // ENABLE_DEVOPTAB_WEBDAV
#ifdef ENABLE_DEVOPTAB_FTP
{
SCOPED_TIMESTAMP("ftp init");
devoptab::MountFtpAll();
}
#endif // ENABLE_DEVOPTAB_FTP
#ifdef ENABLE_DEVOPTAB_SFTP
{
SCOPED_TIMESTAMP("sftp init");
devoptab::MountSftpAll();
}
#endif // ENABLE_DEVOPTAB_SFTP
#ifdef ENABLE_DEVOPTAB_NFS
{
SCOPED_TIMESTAMP("nfs init");
devoptab::MountNfsAll();
}
#endif // ENABLE_DEVOPTAB_NFS
#ifdef ENABLE_DEVOPTAB_SMB2
{
SCOPED_TIMESTAMP("smb init");
devoptab::MountSmb2All();
}
#endif // ENABLE_DEVOPTAB_SMB2
{
SCOPED_TIMESTAMP("game init");
devoptab::MountGameAll();
}
{
SCOPED_TIMESTAMP("fatfs init");
devoptab::MountFatfsAll();
}
{
SCOPED_TIMESTAMP("mounts init");
devoptab::MountInternalMounts();
}
{
SCOPED_TIMESTAMP("timestamp init");
// ini_putl(GetExePath(), "timestamp", m_start_timestamp, App::PLAYLOG_PATH);
@@ -1599,9 +1781,14 @@ App::App(const char* argv0) {
SCOPED_TIMESTAMP("HID init");
hidInitializeTouchScreen();
hidInitializeGesture();
hidInitializeKeyboard();
hidInitializeMouse();
padConfigureInput(8, HidNpadStyleSet_NpadStandard);
// padInitializeDefault(&m_pad);
padInitializeAny(&m_pad);
m_keyboard.Init(KEYBOARD_BUTTON_MAP);
}
{
@@ -1777,9 +1964,10 @@ void App::DisplayThemeOptions(bool left_side) {
"Each theme can have it's own music file. "
"If a theme does not set a music file, the default music is loaded instead (if it exists)."_i18n);
options->Add<ui::SidebarEntryBool>("12 Hour Time"_i18n, App::Get12HourTimeEnable(), [](bool& enable){
App::Set12HourTimeEnable(enable);
}, "Changes the clock to 12 hour"_i18n);
options->Add<ui::SidebarEntryBool>("Show IP address"_i18n, App::GetApp()->m_show_ip_addr,
"Shows the IP address in all menus, including the WiFi strength.\n\n"
"NOTE: The IP address will be hidden in applet mode due to the applet warning being displayed in it's place."_i18n
);
// todo: add file picker for music here.
// todo: add array to audio which has the list of supported extensions.
@@ -1797,12 +1985,14 @@ void App::DisplayNetworkOptions(bool left_side) {
}
void App::DisplayMiscOptions(bool left_side) {
auto options = std::make_unique<ui::Sidebar>("Misc Options"_i18n, left_side ? ui::Sidebar::Side::LEFT : ui::Sidebar::Side::RIGHT);
void App::DisplayMenuOptions(bool left_side) {
auto options = std::make_unique<ui::Sidebar>("Menus"_i18n, left_side ? ui::Sidebar::Side::LEFT : ui::Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(std::move(options)));
for (auto& e : ui::menu::main::GetMiscMenuEntries()) {
if (e.name == g_app->m_left_menu.Get()) {
for (auto& e : ui::menu::main::GetMenuMenuEntries()) {
if (e.name == g_app->m_center_menu.Get()) {
continue;
} else if (e.name == g_app->m_left_menu.Get()) {
continue;
} else if (e.name == g_app->m_right_menu.Get()) {
continue;
@@ -1859,7 +2049,7 @@ void App::DisplayAdvancedOptions(bool left_side) {
std::vector<std::string> menu_names;
ui::SidebarEntryArray::Items menu_items;
for (auto& e : ui::menu::main::GetMiscMenuEntries()) {
for (auto& e : ui::menu::main::GetMenuMenuEntries()) {
if (!e.IsShortcut()) {
continue;
}
@@ -1877,6 +2067,12 @@ void App::DisplayAdvancedOptions(bool left_side) {
}, "When enabled, it replaces /hbmenu.nro with Sphaira, creating a backup of hbmenu to /switch/hbmenu.nro\n\n" \
"Disabling will give you the option to restore hbmenu."_i18n);
options->Add<ui::SidebarEntryCallback>("Add / modify mounts"_i18n, [](){
devoptab::DisplayDevoptabSideBar();
}, "Create, modify, delete network mounts (HTTP, FTP, SFTP, SMB, NFS).\n"
"Mount options only require a URL and Name be set, with other fields being optional, such as port, user, pass etc.\n\n"
"Any changes made will require restarting Sphaira to take effect."_i18n);
options->Add<ui::SidebarEntryBool>("Boost CPU during transfer"_i18n, App::GetApp()->m_progress_boost_mode,
"Enables boost mode during transfers which can improve transfer speed. "
"This sets the CPU to 1785mhz and lowers the GPU 76mhz"_i18n);
@@ -1885,11 +2081,33 @@ void App::DisplayAdvancedOptions(bool left_side) {
App::SetTextScrollSpeed(index_out);
}, App::GetTextScrollSpeed(), "Change how fast the scrolling text updates"_i18n);
options->Add<ui::SidebarEntryArray>("Set center menu"_i18n, menu_items, [menu_names](s64& index_out){
const auto e = menu_names[index_out];
if (g_app->m_center_menu.Get() != e) {
// swap menus around.
if (g_app->m_left_menu.Get() == e) {
g_app->m_left_menu.Set(g_app->m_left_menu.Get());
} else if (g_app->m_right_menu.Get() == e) {
g_app->m_right_menu.Set(g_app->m_left_menu.Get());
}
g_app->m_center_menu.Set(e);
App::Push<ui::OptionBox>(
"Press OK to restart Sphaira"_i18n, "OK"_i18n, [](auto){
App::ExitRestart();
}
);
}
}, i18n::get(g_app->m_center_menu.Get()), "Set the menu that appears on the center tab."_i18n);
options->Add<ui::SidebarEntryArray>("Set left-side menu"_i18n, menu_items, [menu_names](s64& index_out){
const auto e = menu_names[index_out];
if (g_app->m_left_menu.Get() != e) {
// swap menus around.
if (g_app->m_right_menu.Get() == e) {
if (g_app->m_center_menu.Get() == e) {
g_app->m_center_menu.Set(g_app->m_left_menu.Get());
} else if (g_app->m_right_menu.Get() == e) {
g_app->m_right_menu.Set(g_app->m_left_menu.Get());
}
g_app->m_left_menu.Set(e);
@@ -1906,7 +2124,9 @@ void App::DisplayAdvancedOptions(bool left_side) {
const auto e = menu_names[index_out];
if (g_app->m_right_menu.Get() != e) {
// swap menus around.
if (g_app->m_left_menu.Get() == e) {
if (g_app->m_center_menu.Get() == e) {
g_app->m_center_menu.Set(g_app->m_right_menu.Get());
} else if (g_app->m_left_menu.Get() == e) {
g_app->m_left_menu.Set(g_app->m_right_menu.Get());
}
g_app->m_right_menu.Set(e);
@@ -1927,24 +2147,6 @@ void App::DisplayAdvancedOptions(bool left_side) {
options->Add<ui::SidebarEntryCallback>("Export options"_i18n, [left_side](){
App::DisplayDumpOptions(left_side);
}, "Change the export options."_i18n);
static const char* erpt_path = "/atmosphere/erpt_reports";
options->Add<ui::SidebarEntryBool>("Disable erpt_reports"_i18n, g_app->m_fs->FileExists(erpt_path), [](bool& enable){
if (enable) {
Result rc;
// it's possible for erpt to generate a report in between deleting the folder and creating the file.
for (int i = 0; i < 10; i++) {
g_app->m_fs->DeleteDirectoryRecursively(erpt_path);
if (R_SUCCEEDED(rc = g_app->m_fs->CreateFile(erpt_path))) {
break;
}
}
enable = R_SUCCEEDED(rc);
} else {
g_app->m_fs->DeleteFile(erpt_path);
g_app->m_fs->CreateDirectory(erpt_path);
}
}, "Disables error reports generated in /atmosphere/erpt_reports."_i18n);
}
void App::DisplayInstallOptions(bool left_side) {
@@ -2118,6 +2320,236 @@ void App::DisplayDumpOptions(bool left_side) {
block_size_option->Depends(App::GetApp()->m_nsz_compress_block, "NSZ block compression is disabled."_i18n);
}
void App::DisplayFtpOptions(bool left_side) {
// todo: prompt on exit to restart ftp server if options were changed.
auto options = std::make_unique<ui::Sidebar>("FTP Options"_i18n, left_side ? ui::Sidebar::Side::LEFT : ui::Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(std::move(options)));
options->SetOnExitWhenChanged([](){
if (App::GetFtpEnable()) {
App::Notify("Restarting FTP server..."_i18n);
App::SetFtpEnable(false);
App::SetFtpEnable(true);
}
});
options->Add<ui::SidebarEntryBool>("Enable"_i18n, App::GetFtpEnable(), [](bool& enable){
App::SetFtpEnable(enable);
}, "Enable FTP server to run in the background."_i18n);
options->Add<ui::SidebarEntryTextInput>(
"Port", App::GetApp()->m_ftp_port.Get(), "", "", 1, 5,
"Opens the FTP server on this port."_i18n,
[](auto* input){
App::GetApp()->m_ftp_port.Set(input->GetNumValue());
}
);
options->Add<ui::SidebarEntryBool>(
"Anon"_i18n, App::GetApp()->m_ftp_anon,
"Allows you to login without setting a username and password.\n"
"If disabled, you must set a user name and password below!"_i18n
);
options->Add<ui::SidebarEntryTextInput>(
"User", App::GetApp()->m_ftp_user.Get(), "", "", -1, 64,
"Sets the username, must be set if anon is disabled."_i18n,
[](auto* input){
App::GetApp()->m_ftp_user.Set(input->GetValue());
}
);
options->Add<ui::SidebarEntryTextInput>(
"Pass", App::GetApp()->m_ftp_pass.Get(), "", "", -1, 64,
"Sets the password, must be set if anon is disabled."_i18n,
[](auto* input){
App::GetApp()->m_ftp_pass.Set(input->GetValue());
}
);
options->Add<ui::SidebarEntryBool>(
"Show album"_i18n, App::GetApp()->m_ftp_show_album,
"Shows the microSD card album folder."_i18n
);
options->Add<ui::SidebarEntryBool>(
"Show Atmosphere contents"_i18n, App::GetApp()->m_ftp_show_ams_contents,
"Shows the shortcut for the /atmosphere/contents folder."_i18n
);
options->Add<ui::SidebarEntryBool>(
"Show bis storage"_i18n, App::GetApp()->m_ftp_show_bis_storage,
"Shows the bis folder which contains the following:\n"
"- BootPartition1Root.bin\n"
"- BootPartition2Root.bin\n"
"- UserDataRoot.bin\n"
"- BootConfigAndPackage2Part1.bin\n"
"- BootConfigAndPackage2Part2.bin\n"
"- BootConfigAndPackage2Part3.bin\n"
"- BootConfigAndPackage2Part4.bin\n"
"- BootConfigAndPackage2Part5.bin\n"
"- BootConfigAndPackage2Part6.bin\n"
"- CalibrationFile.bin\n"
"- SafeMode.bin\n"
"- User.bin\n"
"- System.bin\n"
"- SystemProperEncryption.bin"_i18n
);
options->Add<ui::SidebarEntryBool>(
"Show bis file systems"_i18n, App::GetApp()->m_ftp_show_bis_fs,
"Shows the following bis file systems:\n"
"- bis_calibration_file\n"
"- bis_safe_mode\n"
"- bis_user\n"
"- bis_system"_i18n
);
options->Add<ui::SidebarEntryBool>(
"Show system contents"_i18n, App::GetApp()->m_ftp_show_content_system,
"Shows the system contents folder."_i18n
);
options->Add<ui::SidebarEntryBool>(
"Show user contents"_i18n, App::GetApp()->m_ftp_show_content_user,
"Shows the user contents folder."_i18n
);
options->Add<ui::SidebarEntryBool>(
"Show microSD contents"_i18n, App::GetApp()->m_ftp_show_content_sd,
"Shows the microSD contents folder.\n\n"
"NOTE: This is not the normal microSD card storage, it is instead "
"the location where NCA's are stored. The normal microSD card is always mounted."_i18n
);
options->Add<ui::SidebarEntryBool>(
"Show games"_i18n, App::GetApp()->m_ftp_show_games,
"Shows the games folder.\n\n"
"This folder contains all of your installed games, allowing you to create "
"backups over FTP!\n\n"
"NOTE: This folder is read-only. You cannot delete games over FTP."_i18n
);
options->Add<ui::SidebarEntryBool>(
"Show install"_i18n, App::GetApp()->m_ftp_show_install,
"Shows the install folder.\n\n"
"This folder is used for installing games via FTP.\n\n"
"NOTE: You must open the \"FTP Install\" menu when trying to install a game!"_i18n
);
options->Add<ui::SidebarEntryBool>(
"Show mounts"_i18n, App::GetApp()->m_ftp_show_mounts,
"Shows the mounts folder.\n\n"
"This folder is contains all of the mounts added to Sphaira, allowing you to acces them over FTP!\n"
"For example, you can access your SMB, WebDav or other FTP mounts over FTP."_i18n
);
options->Add<ui::SidebarEntryBool>(
"Show switch"_i18n, App::GetApp()->m_ftp_show_switch,
"Shows the shortcut for the /switch folder."
"This is the folder that contains all your homebrew (NRO's)."_i18n
);
}
void App::DisplayMtpOptions(bool left_side) {
// todo: prompt on exit to restart ftp server if options were changed.
auto options = std::make_unique<ui::Sidebar>("MTP Options"_i18n, left_side ? ui::Sidebar::Side::LEFT : ui::Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(std::move(options)));
options->SetOnExitWhenChanged([](){
if (App::GetMtpEnable()) {
App::Notify("Restarting MTP server..."_i18n);
App::SetMtpEnable(false);
App::SetMtpEnable(true);
}
});
options->Add<ui::SidebarEntryBool>("Enable"_i18n, App::GetMtpEnable(), [](bool& enable){
App::SetMtpEnable(enable);
}, "Enable MTP server to run in the background."_i18n);
// not sure if i want to expose this to users yet.
// its also stubbed currently.
#if 0
options->Add<ui::SidebarEntryBool>(
"Pre-allocate file"_i18n, App::GetApp()->m_mtp_allocate_file,
"Enables pre-allocating the file size before writing.\n"
"This speeds up file writes, however, this can cause timeouts if all these conditions are met:\n"
"- using Windows\n"
"- using emuMMC\n"
"- transferring a large file (>1GB)\n\n"
"This option should be left enabled, however if you use the above and experience timeouts, "
"then try again with this option disabled."_i18n
);
#endif
options->Add<ui::SidebarEntryBool>(
"Show album"_i18n, App::GetApp()->m_mtp_show_album,
"Shows the microSD card album folder."_i18n
);
options->Add<ui::SidebarEntryBool>(
"Show microSD contents"_i18n, App::GetApp()->m_mtp_show_content_sd,
"Shows the microSD contents folder.\n\n"
"NOTE: This is not the normal microSD card storage, it is instead "
"the location where NCA's are stored. The normal microSD card is always mounted."_i18n
);
options->Add<ui::SidebarEntryBool>(
"Show system contents"_i18n, App::GetApp()->m_mtp_show_content_system,
"Shows the system contents folder."_i18n
);
options->Add<ui::SidebarEntryBool>(
"Show user contents"_i18n, App::GetApp()->m_mtp_show_content_user,
"Shows the user contents folder."_i18n
);
options->Add<ui::SidebarEntryBool>(
"Show games"_i18n, App::GetApp()->m_mtp_show_games,
"Shows the games folder.\n\n"
"This folder contains all of your installed games, allowing you to create "
"backups over MTP!\n\n"
"NOTE: This folder is read-only. You cannot delete games over MTP."_i18n
);
options->Add<ui::SidebarEntryBool>(
"Show install"_i18n, App::GetApp()->m_mtp_show_install,
"Shows the install folder.\n\n"
"This folder is used for installing games via MTP.\n\n"
"NOTE: You must open the \"MTP Install\" menu when trying to install a game!"_i18n
);
options->Add<ui::SidebarEntryBool>(
"Show mounts"_i18n, App::GetApp()->m_mtp_show_mounts,
"Shows the mounts folder.\n\n"
"This folder is contains all of the mounts added to Sphaira, allowing you to acces them over MTP!\n"
"For example, you can access your SMB, WebDav and FTP mounts over MTP."_i18n
);
options->Add<ui::SidebarEntryBool>(
"Show DevNull"_i18n, App::GetApp()->m_mtp_show_speedtest,
"Shows the DevNull (Speed Test) folder.\n\n"
"This folder is used for benchmarking USB uploads. "
"This ia virtual folder, nothing is actally written to disk."_i18n
);
}
void App::DisplayHddOptions(bool left_side) {
auto options = std::make_unique<ui::Sidebar>("HDD Options"_i18n, left_side ? ui::Sidebar::Side::LEFT : ui::Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(std::move(options)));
options->Add<ui::SidebarEntryBool>("Enable"_i18n, App::GetHddEnable(), [](bool& enable){
App::SetHddEnable(enable);
}, "Enable mounting of connected USB/HDD devices. "
"Connected devices can be used in the FileBrowser, as well as a backup location when dumping games and saves."_i18n
);
options->Add<ui::SidebarEntryBool>("HDD write protect"_i18n, App::GetWriteProtect(), [](bool& enable){
App::SetWriteProtect(enable);
}, "Makes the connected HDD read-only."_i18n);
}
void App::ShowEnableInstallPrompt() {
// warn the user the dangers of installing.
App::Push<ui::OptionBox>(
@@ -2168,36 +2600,55 @@ App::~App() {
// async exit as these threads sleep every 100ms.
{
SCOPED_TIMESTAMP("async signal");
nxlinkSignalExit();
#ifdef ENABLE_FTPSRV
ftpsrv::ExitSignal();
#endif // ENABLE_FTPSRV
nxlinkSignalExit();
audio::ExitSignal();
curl::ExitSignal();
}
utils::Async async_exit([this](){
{
SCOPED_TIMESTAMP("usbdvd_exit");
usbdvd::UnmountAll();
// this has to be called before any cleanup to ensure the lifetime of
// nvg is still active as some widgets may need to free images.
// clear in reverse order as the widgets are a stack.
{
SCOPED_TIMESTAMP("widget exit");
while (!m_widgets.empty()) {
m_widgets.pop_back();
}
}
utils::Async async_exit([this](){
{
SCOPED_TIMESTAMP("i18n_exit");
i18n::exit();
}
#ifdef ENABLE_LIBUSBDVD
{
SCOPED_TIMESTAMP("usbdvd_exit");
usbdvd::UnmountAll();
}
#endif // ENABLE_LIBUSBDVD
#ifdef ENABLE_LIBHAZE
{
SCOPED_TIMESTAMP("mtp exit");
haze::Exit();
libhaze::Exit();
}
#endif // ENABLE_LIBHAZE
#ifdef ENABLE_LIBUSBHSFS
{
SCOPED_TIMESTAMP("hdd exit");
usbHsFsExit();
}
#endif // ENABLE_LIBUSBHSFS
// this has to come before curl exit as it uses curl global.
{
SCOPED_TIMESTAMP("fatfs exit");
fatfs::UnmountAll();
SCOPED_TIMESTAMP("devoptab exit");
devoptab::UmountAllNeworkDevices();
}
// do these last as they were signalled to exit.
@@ -2207,10 +2658,12 @@ App::~App() {
audio::Exit();
}
#ifdef ENABLE_FTPSRV
{
SCOPED_TIMESTAMP("ftp exit");
ftpsrv::Exit();
}
#endif // ENABLE_FTPSRV
{
SCOPED_TIMESTAMP("nxlink exit");
@@ -2223,25 +2676,6 @@ App::~App() {
}
});
// destroy this first as it seems to prevent a crash when exiting the appstore
// when an image that was being drawn is displayed
// replicate: saves -> homebrew -> misc -> appstore -> sphaira -> changelog -> exit
// it will crash when deleting image 43.
{
SCOPED_TIMESTAMP("destroy frame buffer resources");
this->destroyFramebufferResources();
}
// this has to be called before any cleanup to ensure the lifetime of
// nvg is still active as some widgets may need to free images.
// clear in reverse order as the widgets are a stack (todo: just use a stack?)
{
SCOPED_TIMESTAMP("widget exit");
while (!m_widgets.empty()) {
m_widgets.pop_back();
}
}
// do not async close theme as it frees textures.
{
SCOPED_TIMESTAMP("theme exit");
@@ -2249,6 +2683,11 @@ App::~App() {
CloseTheme();
}
{
SCOPED_TIMESTAMP("destroy frame buffer resources");
this->destroyFramebufferResources();
}
{
SCOPED_TIMESTAMP("nvg exit");
nvgDeleteImage(vg, m_default_image);

View File

@@ -33,8 +33,6 @@ namespace {
constexpr auto API_AGENT = "TotalJustice";
constexpr u64 CHUNK_SIZE = 1024*1024;
constexpr auto MAX_THREADS = 4;
constexpr int THREAD_PRIO = 0x2F;
constexpr int THREAD_CORE = 2;
std::atomic_bool g_running{};
CURLSH* g_curl_share{};
@@ -62,11 +60,6 @@ struct SeekCustomData {
s64 size{};
};
// helper for creating webdav folders as libcurl does not have built-in
// support for it.
// only creates the folders if they don't exist.
auto WebdavCreateFolder(CURL* curl, const Api& e) -> bool;
auto generate_key_from_path(const fs::FsPath& path) -> std::string {
const auto key = crc32Calculate(path.s, path.size());
return std::to_string(key);
@@ -595,22 +588,16 @@ auto EscapeString(CURL* curl, const std::string& str) -> std::string {
return result;
}
auto EncodeUrl(std::string url) -> std::string {
auto EncodeUrl(const std::string& url) -> std::string {
log_write("[CURL] encoding url\n");
if (url.starts_with("webdav://")) {
log_write("[CURL] updating host\n");
url.replace(0, std::strlen("webdav"), "https");
log_write("[CURL] updated host: %s\n", url.c_str());
}
auto clu = curl_url();
R_UNLESS(clu, url);
ON_SCOPE_EXIT(curl_url_cleanup(clu));
log_write("[CURL] setting url\n");
CURLUcode clu_code;
clu_code = curl_url_set(clu, CURLUPART_URL, url.c_str(), CURLU_URLENCODE);
clu_code = curl_url_set(clu, CURLUPART_URL, url.c_str(), CURLU_DEFAULT_SCHEME | CURLU_URLENCODE);
R_UNLESS(clu_code == CURLUE_OK, url);
log_write("[CURL] set url success\n");
@@ -834,13 +821,6 @@ auto UploadInternal(CURL* curl, const Api& e) -> ApiResult {
return {};
}
if (e.GetUrl().starts_with("webdav://")) {
if (!WebdavCreateFolder(curl, e)) {
log_write("[CURL] failed to create webdav folder, aborting\n");
return {};
}
}
const auto& info = e.GetUploadInfo();
const auto url = e.GetUrl() + "/" + info.m_name;
const auto encoded_url = EncodeUrl(url);
@@ -960,76 +940,6 @@ auto UploadInternal(CURL* curl, const Api& e) -> ApiResult {
return {success, http_code, header_out, chunk_out.data};
}
auto WebdavCreateFolder(CURL* curl, const Api& e) -> bool {
// if using webdav, extract the file path and create the directories.
// https://github.com/WebDAVDevs/webdav-request-samples/blob/master/webdav_curl.md
if (e.GetUrl().starts_with("webdav://")) {
log_write("[CURL] found webdav url\n");
const auto info = e.GetUploadInfo();
if (info.m_name.empty()) {
return true;
}
const auto& file_path = info.m_name;
log_write("got file path: %s\n", file_path.c_str());
const auto file_loc = file_path.find_last_of('/');
if (file_loc == file_path.npos) {
log_write("failed to find last slash\n");
return true;
}
const auto path_view = file_path.substr(0, file_loc);
log_write("got folder path: %s\n", path_view.c_str());
auto e2 = e;
e2.SetOption(Path{});
e2.SetOption(Url{e.GetUrl() + "/" + path_view});
e2.SetOption(Flags{e.GetFlags() | Flag_NoBody});
e2.SetOption(CustomRequest{"PROPFIND"});
e2.SetOption(Header{
{ "Depth", "0" },
});
// test to see if the directory exists first.
const auto exist_result = DownloadInternal(curl, e2);
if (exist_result.success) {
log_write("[CURL] folder already exist: %s\n", path_view.c_str());
return true;
} else {
log_write("[CURL] folder does NOT exist, manually creating: %s\n", path_view.c_str());
}
// make the request to create the folder.
std::string folder;
for (const auto dir : std::views::split(path_view, '/')) {
if (dir.empty()) {
continue;
}
folder += "/" + std::string{dir.data(), dir.size()};
e2.SetOption(Url{e.GetUrl() + folder});
e2.SetOption(Header{});
e2.SetOption(CustomRequest{"MKCOL"});
const auto result = DownloadInternal(curl, e2);
if (result.code == 201) {
log_write("[CURL] created webdav directory\n");
} else if (result.code == 405) {
log_write("[CURL] webdav directory already exists: %ld\n", result.code);
} else {
log_write("[CURL] failed to create webdav directory: %ld\n", result.code);
return false;
}
}
} else {
log_write("[CURL] not a webdav url: %s\n", e.GetUrl().c_str());
}
return true;
}
void my_lock(CURL *handle, curl_lock_data data, curl_lock_access laccess, void *useptr) {
mutexLock(&g_mutex_share[data]);
}

View File

@@ -7,7 +7,6 @@
#include "i18n.hpp"
#include "location.hpp"
#include "threaded_file_transfer.hpp"
#include "haze_helper.hpp"
#include "ui/sidebar.hpp"
#include "ui/error_box.hpp"
@@ -133,9 +132,9 @@ struct WriteNullSource final : WriteSource {
struct WriteUsbSource final : WriteSource {
WriteUsbSource(u64 transfer_timeout) {
// disable mtp if enabled.
m_was_mtp_enabled = haze::IsInit();
m_was_mtp_enabled = App::GetMtpEnable();
if (m_was_mtp_enabled) {
haze::Exit();
App::SetMtpEnable(false);
}
m_usb = std::make_unique<usb::dump::Usb>(transfer_timeout);
@@ -145,7 +144,7 @@ struct WriteUsbSource final : WriteSource {
m_usb.reset();
if (m_was_mtp_enabled) {
haze::Init();
App::SetMtpEnable(true);
}
}
@@ -165,6 +164,14 @@ struct WriteUsbSource final : WriteSource {
R_SUCCEED();
}
auto GetOpenResult() const {
return m_usb->GetOpenResult();
}
auto GetCancelEvent() {
return m_usb->GetCancelEvent();
}
private:
std::unique_ptr<usb::dump::Usb> m_usb{};
bool m_was_mtp_enabled{};
@@ -178,8 +185,8 @@ constexpr DumpLocationEntry DUMP_LOCATIONS[]{
};
struct UsbTest final : usb::upload::Usb, yati::source::Stream {
UsbTest(ui::ProgressBox* pbox, BaseSource* source, std::span<const fs::FsPath> paths)
: Usb{UINT64_MAX}
UsbTest(ui::ProgressBox* pbox, BaseSource* source, std::span<const fs::FsPath> paths, u64 timeout)
: Usb{timeout}
, m_pbox{pbox}
, m_source{source}
, m_paths{paths} {
@@ -248,6 +255,10 @@ struct UsbTest final : usb::upload::Usb, yati::source::Stream {
return m_pull_offset;
}
auto GetOpenResult() const {
return Usb::GetOpenResult();
}
private:
ui::ProgressBox* m_pbox{};
BaseSource* m_source{};
@@ -261,7 +272,14 @@ private:
};
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);
// create write source and verify that it opened.
constexpr u64 timeout = UINT64_MAX;
auto write_source = std::make_unique<WriteUsbSource>(timeout);
R_TRY(write_source->GetOpenResult());
// add cancel event.
pbox->AddCancelEvent(write_source->GetCancelEvent());
ON_SCOPE_EXIT(pbox->RemoveCancelEvent(write_source->GetCancelEvent()));
for (const auto& path : paths) {
const auto file_size = source->GetSize(path);
@@ -273,7 +291,7 @@ Result DumpToUsb(ui::ProgressBox* pbox, BaseSource* source, std::span<const fs::
while (true) {
R_TRY(pbox->ShouldExitResult());
const auto rc = write_source->WaitForConnection(path, 3e+9);
const auto rc = write_source->WaitForConnection(path, timeout);
if (R_SUCCEEDED(rc)) {
break;
}
@@ -318,7 +336,7 @@ 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));
R_TRY(fs->OpenFile(temp_path, FsOpenMode_Write|FsOpenMode_Append, &file));
auto write_source = std::make_unique<WriteFileSource>(&file);
if (custom_transfer) {
@@ -353,7 +371,8 @@ Result DumpToFileNative(ui::ProgressBox* pbox, BaseSource* source, std::span<con
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, custom_transfer);
const auto mount_path = fs::AppendPath(loc.mount, loc.dump_path);
return DumpToFile(pbox, &fs, mount_path, source, paths, custom_transfer);
}
Result DumpToUsbS2SInternal(ui::ProgressBox* pbox, UsbTest* usb) {
@@ -398,8 +417,14 @@ Result DumpToUsbS2S(ui::ProgressBox* pbox, BaseSource* source, std::span<const f
file_list.emplace_back(path);
}
auto usb = std::make_unique<UsbTest>(pbox, source, paths);
constexpr u64 timeout = 3e+9;
// create usb test instance and verify that it opened.
constexpr u64 timeout = UINT64_MAX;
auto usb = std::make_unique<UsbTest>(pbox, source, paths, timeout);
R_TRY(usb->GetOpenResult());
// add cancel event.
pbox->AddCancelEvent(usb->GetCancelEvent());
ON_SCOPE_EXIT(pbox->RemoveCancelEvent(usb->GetCancelEvent()));
while (!pbox->ShouldExit()) {
if (R_SUCCEEDED(usb->IsUsbConnected(timeout))) {
@@ -461,54 +486,6 @@ Result DumpToDevNull(ui::ProgressBox* pbox, BaseSource* source, std::span<const
R_SUCCEED();
}
Result DumpToNetwork(ui::ProgressBox* pbox, const location::Entry& loc, BaseSource* source, std::span<const fs::FsPath> paths) {
for (auto path : paths) {
R_TRY(pbox->ShouldExitResult());
const auto file_size = source->GetSize(path);
pbox->SetImage(source->GetIcon(path));
pbox->SetTitle(source->GetName(path));
pbox->NewTransfer(path);
R_TRY(thread::TransferPull(pbox, file_size,
[&](void* data, s64 off, s64 size, u64* bytes_read) -> Result {
return source->Read(path, data, off, size, bytes_read);
},
[&](thread::PullCallback pull) -> Result {
s64 offset{};
const auto result = curl::Api().FromMemory(
CURL_LOCATION_TO_API(loc),
curl::OnProgress{pbox->OnDownloadProgressCallback()},
curl::UploadInfo{
path, file_size,
[&](void *ptr, size_t size) -> size_t {
// curl will request past the size of the file, causing an error.
if (offset >= file_size) {
log_write("finished file upload\n");
return 0;
}
u64 bytes_read{};
if (R_FAILED(pull(ptr, size, &bytes_read))) {
log_write("failed to read in custom callback: %zd size: %zd\n", offset, size);
return 0;
}
offset += bytes_read;
return bytes_read;
}
}
);
R_UNLESS(result.success, Result_DumpFailedNetworkUpload);
R_SUCCEED();
}
));
}
R_SUCCEED();
}
} // namespace
void DumpGetLocation(const std::string& title, u32 location_flags, const OnLocation& on_loc, const CustomTransfer& custom_transfer) {
@@ -516,19 +493,17 @@ void DumpGetLocation(const std::string& title, u32 location_flags, const OnLocat
ui::PopupList::Items items;
std::vector<DumpEntry> dump_entries;
out.network = location::Load();
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);
}
}
out.stdio = location::GetStdio(true);
const auto stdio_entries = location::GetStdio(true);
if (location_flags & (1 << DumpLocationType_Stdio)) {
for (s32 i = 0; i < std::size(out.stdio); i++) {
dump_entries.emplace_back(DumpLocationType_Stdio, i);
items.emplace_back(out.stdio[i].name);
for (auto& e : stdio_entries) {
if (e.dump_hidden) {
continue;
}
const auto index = out.stdio.size();
dump_entries.emplace_back(DumpLocationType_Stdio, index);
items.emplace_back(e.name);
out.stdio.emplace_back(e);
}
}
@@ -553,9 +528,7 @@ void DumpGetLocation(const std::string& title, u32 location_flags, const OnLocat
}
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) {
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));

File diff suppressed because it is too large Load Diff

View File

@@ -134,7 +134,7 @@ Result read_entire_file(Fs* fs, const FsPath& path, std::vector<u8>& out) {
R_SUCCEED();
}
Result write_entire_file(Fs* fs, const FsPath& path, const std::vector<u8>& in, bool ignore_read_only) {
Result write_entire_file(Fs* fs, const FsPath& path, std::span<const u8> in, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only(path), Result_FsReadOnly);
if (auto rc = fs->CreateFile(path, in.size(), 0); R_FAILED(rc) && rc != FsError_PathAlreadyExists) {
@@ -215,14 +215,18 @@ Result CreateDirectoryRecursively(FsFileSystem* fs, const FsPath& _path, bool ig
rc = CreateDirectory(path, ignore_read_only);
}
if (R_FAILED(rc) && rc != FsError_PathAlreadyExists) {
log_write("failed to create folder: %s\n", path.s);
return rc;
}
// log_write("created_directory: %s\n", path);
std::strcat(path, "/");
}
// only check if the last folder creation failed.
// reason being is that it may try to create "/" root folder, which some network
// fs will return a EPERM/EACCES error.
// however if the last directory failed, then it is a real error.
if (R_FAILED(rc) && rc != FsError_PathAlreadyExists) {
log_write("failed to create folder: %s\n", path.s);
return rc;
}
R_SUCCEED();
}
@@ -231,7 +235,7 @@ Result CreateDirectoryRecursivelyWithPath(FsFileSystem* fs, const FsPath& _path,
// strip file name form path.
const auto last_slash = std::strrchr(_path, '/');
if (!last_slash) {
if (!last_slash || last_slash == _path.s) {
R_SUCCEED();
}
@@ -317,12 +321,12 @@ Result CreateFile(const FsPathReal& path, u64 size, u32 option, bool ignore_read
}
R_TRY(fsdevGetLastResult());
return Result_FsUnknownStdioError;
return Result_FsStdioFailedToCreate;
}
ON_SCOPE_EXIT(close(fd));
if (size) {
R_UNLESS(!ftruncate(fd, size), Result_FsUnknownStdioError);
R_UNLESS(!ftruncate(fd, size), Result_FsStdioFailedToTruncate);
}
R_SUCCEED();
@@ -337,7 +341,7 @@ Result CreateDirectory(const FsPathReal& path, bool ignore_read_only) {
}
R_TRY(fsdevGetLastResult());
return Result_FsUnknownStdioError;
return Result_FsStdioFailedToCreateDirectory;
}
R_SUCCEED();
}
@@ -359,7 +363,7 @@ Result DeleteFile(const FsPathReal& path, bool ignore_read_only) {
if (unlink(path)) {
R_TRY(fsdevGetLastResult());
return Result_FsUnknownStdioError;
return Result_FsStdioFailedToDeleteFile;
}
R_SUCCEED();
}
@@ -369,7 +373,7 @@ Result DeleteDirectory(const FsPathReal& path, bool ignore_read_only) {
if (rmdir(path)) {
R_TRY(fsdevGetLastResult());
return Result_FsUnknownStdioError;
return Result_FsStdioFailedToDeleteDirectory;
}
R_SUCCEED();
}
@@ -401,7 +405,7 @@ Result RenameFile(const FsPathReal& src, const FsPathReal& dst, bool ignore_read
if (rename(src, dst)) {
R_TRY(fsdevGetLastResult());
return Result_FsUnknownStdioError;
return Result_FsStdioFailedToRename;
}
R_SUCCEED();
}
@@ -417,7 +421,7 @@ Result GetEntryType(const FsPathReal& path, FsDirEntryType* out) {
struct stat st;
if (stat(path, &st)) {
R_TRY(fsdevGetLastResult());
return Result_FsUnknownStdioError;
return Result_FsStdioFailedToStat;
}
*out = S_ISREG(st.st_mode) ? FsDirEntryType_File : FsDirEntryType_Dir;
R_SUCCEED();
@@ -427,7 +431,7 @@ Result GetFileTimeStampRaw(const FsPathReal& path, FsTimeStampRaw *out) {
struct stat st;
if (stat(path, &st)) {
R_TRY(fsdevGetLastResult());
return Result_FsUnknownStdioError;
return Result_FsStdioFailedToStat;
}
out->is_valid = true;
@@ -464,6 +468,10 @@ bool DirExists(const FsPath& path) {
}
Result OpenFile(fs::Fs* fs, const FsPathReal& path, u32 mode, File* f) {
const auto should_buffer = (mode & OpenMode_EnableBuffer);
// remove the invalid flag so that native fs doesn't error.
mode &= ~OpenMode_EnableBuffer;
f->m_fs = fs;
f->m_mode = mode;
@@ -481,7 +489,14 @@ Result OpenFile(fs::Fs* fs, const FsPathReal& path, u32 mode, File* f) {
f->m_stdio = std::fopen(path, "rb+");
}
R_UNLESS(f->m_stdio, Result_FsUnknownStdioError);
R_UNLESS(f->m_stdio, Result_FsStdioFailedToOpenFile);
// disable buffering to match native fs behavior.
// this also causes problems with network io as it will do double reads.
// which kills performance (see sftp).
if (!should_buffer) {
std::setvbuf(f->m_stdio, nullptr, _IONBF, 0);
}
}
R_SUCCEED();
@@ -500,9 +515,11 @@ Result File::Read( s64 off, void* buf, u64 read_size, u32 option, u64* bytes_rea
} else {
R_UNLESS(m_stdio, Result_FsUnknownStdioError);
if (m_stdio_off != off) {
m_stdio_off = off;
std::fseek(m_stdio, off, SEEK_SET);
if (off != std::ftell(m_stdio)) {
const auto ret = std::fseek(m_stdio, off, SEEK_SET);
log_write("[FS] fseek to %ld ret: %d new_off: %zd\n", off, ret, std::ftell(m_stdio));
R_UNLESS(ret == 0, Result_FsStdioFailedToSeek);
R_UNLESS(off == std::ftell(m_stdio), Result_FsStdioFailedToSeek);
}
*bytes_read = std::fread(buf, 1, read_size, m_stdio);
@@ -510,11 +527,10 @@ Result File::Read( s64 off, void* buf, u64 read_size, u32 option, u64* bytes_rea
// if we read less bytes than expected, check if there was an error (ignoring eof).
if (*bytes_read < read_size) {
if (!std::feof(m_stdio) && std::ferror(m_stdio)) {
R_THROW(Result_FsUnknownStdioError);
log_write("[FS] fread error: %d\n", std::ferror(m_stdio));
R_THROW(Result_FsStdioFailedToRead);
}
}
m_stdio_off += *bytes_read;
}
R_SUCCEED();
@@ -528,17 +544,15 @@ Result File::Write(s64 off, const void* buf, u64 write_size, u32 option) {
} else {
R_UNLESS(m_stdio, Result_FsUnknownStdioError);
if (m_stdio_off != off) {
log_write("[FS] diff seek\n");
m_stdio_off = off;
std::fseek(m_stdio, off, SEEK_SET);
if (off != std::ftell(m_stdio)) {
const auto ret = std::fseek(m_stdio, off, SEEK_SET);
R_UNLESS(ret == 0, Result_FsStdioFailedToSeek);
R_UNLESS(off == std::ftell(m_stdio), Result_FsStdioFailedToSeek);
}
const auto result = std::fwrite(buf, 1, write_size, m_stdio);
// log_write("[FS] fwrite res: %zu vs %zu\n", result, write_size);
R_UNLESS(result == write_size, Result_FsUnknownStdioError);
m_stdio_off += write_size;
R_UNLESS(result == write_size, Result_FsStdioFailedToWrite);
}
R_SUCCEED();
@@ -552,8 +566,8 @@ Result File::SetSize(s64 sz) {
} else {
R_UNLESS(m_stdio, Result_FsUnknownStdioError);
const auto fd = fileno(m_stdio);
R_UNLESS(fd > 0, Result_FsUnknownStdioError);
R_UNLESS(!ftruncate(fd, sz), Result_FsUnknownStdioError);
R_UNLESS(fd > 0, Result_FsStdioFailedToTruncate);
R_UNLESS(!ftruncate(fd, sz), Result_FsStdioFailedToTruncate);
}
R_SUCCEED();
@@ -568,7 +582,7 @@ Result File::GetSize(s64* out) {
R_UNLESS(m_stdio, Result_FsUnknownStdioError);
struct stat st;
R_UNLESS(!fstat(fileno(m_stdio), &st), Result_FsUnknownStdioError);
R_UNLESS(!fstat(fileno(m_stdio), &st), Result_FsStdioFailedToStat);
*out = st.st_size;
}
@@ -590,6 +604,7 @@ void File::Close() {
}
} else {
if (m_stdio) {
log_write("[FS] closing stdio file\n");
std::fclose(m_stdio);
m_stdio = {};
}
@@ -605,7 +620,7 @@ Result OpenDirectory(fs::Fs* fs, const FsPathReal& path, u32 mode, Dir* d) {
R_TRY(fsFsOpenDirectory(&fs->m_fs, path, mode, &d->m_native));
} else {
d->m_stdio = opendir(path);
R_UNLESS(d->m_stdio, Result_FsUnknownStdioError);
R_UNLESS(d->m_stdio, Result_FsStdioFailedToOpenDirectory);
}
R_SUCCEED();
@@ -673,6 +688,20 @@ Result Dir::GetEntryCount(s64* out) {
if (!std::strcmp(d->d_name, ".") || !std::strcmp(d->d_name, "..")) {
continue;
}
if (d->d_type == DT_DIR) {
if (!(m_mode & FsDirOpenMode_ReadDirs)) {
continue;
}
} else if (d->d_type == DT_REG) {
if (!(m_mode & FsDirOpenMode_ReadFiles)) {
continue;
}
} else {
log_write("[FS] WARNING: unknown type when counting dir: %u\n", d->d_type);
continue;
}
(*out)++;
}

View File

@@ -6,11 +6,12 @@
#include "utils/thread.hpp"
#include <algorithm>
#include <minIni.h>
#include <ftpsrv.h>
#include <ftpsrv_vfs.h>
#include <nx/vfs_nx.h>
#include <nx/utils.h>
#include <unistd.h>
#include <fcntl.h>
namespace sphaira::ftpsrv {
namespace {
@@ -28,19 +29,21 @@ struct InstallSharedData {
bool enabled;
};
const char* INI_PATH = "/config/ftpsrv/config.ini";
FtpSrvConfig g_ftpsrv_config = {0};
std::atomic_bool g_should_exit = false;
bool g_is_running{false};
Thread g_thread;
FtpSrvConfig g_ftpsrv_config{};
int g_ftpsrv_mount_flags{};
std::vector<VfsNxCustomPath> g_custom_vfs{};
std::atomic_bool g_should_exit{};
bool g_is_running{};
Thread g_thread{};
Mutex g_mutex{};
void ftp_log_callback(enum FTP_API_LOG_TYPE type, const char* msg) {
sphaira::App::NotifyFlashLed();
log_write("[FTPSRV] %s\n", msg);
App::NotifyFlashLed();
}
void ftp_progress_callback(void) {
sphaira::App::NotifyFlashLed();
App::NotifyFlashLed();
}
InstallSharedData g_shared_data{};
@@ -274,65 +277,203 @@ FtpVfs g_vfs_install = {
.rename = vfs_install_rename,
};
struct FtpVfsFile {
int fd;
int valid;
};
struct FtpVfsDir {
DIR* fd;
};
struct FtpVfsDirEntry {
struct dirent* buf;
};
auto vfs_stdio_fix_path(const char* str) -> fs::FsPath {
while (*str == '/') {
str++;
}
fs::FsPath out = str;
if (out.ends_with(":")) {
out += '/';
}
return out;
}
int vfs_stdio_open(void* user, const char* _path, enum FtpVfsOpenMode mode) {
auto f = static_cast<FtpVfsFile*>(user);
const auto path = vfs_stdio_fix_path(_path);
int flags = 0, args = 0;
switch (mode) {
case FtpVfsOpenMode_READ:
flags = O_RDONLY;
args = 0;
break;
case FtpVfsOpenMode_WRITE:
flags = O_WRONLY | O_CREAT | O_TRUNC;
args = 0666;
break;
case FtpVfsOpenMode_APPEND:
flags = O_WRONLY | O_CREAT | O_APPEND;
args = 0666;
break;
}
f->fd = open(path, flags, args);
if (f->fd >= 0) {
f->valid = 1;
}
return f->fd;
}
int vfs_stdio_read(void* user, void* buf, size_t size) {
auto f = static_cast<FtpVfsFile*>(user);
return read(f->fd, buf, size);
}
int vfs_stdio_write(void* user, const void* buf, size_t size) {
auto f = static_cast<FtpVfsFile*>(user);
return write(f->fd, buf, size);
}
int vfs_stdio_seek(void* user, const void* buf, size_t size, size_t off) {
auto f = static_cast<FtpVfsFile*>(user);
const auto pos = lseek(f->fd, off, SEEK_SET);
if (pos < 0) {
return -1;
}
return 0;
}
int vfs_stdio_isfile_open(void* user) {
auto f = static_cast<FtpVfsFile*>(user);
return f->valid && f->fd >= 0;
}
int vfs_stdio_close(void* user) {
auto f = static_cast<FtpVfsFile*>(user);
int rc = 0;
if (vfs_stdio_isfile_open(f)) {
rc = close(f->fd);
f->fd = -1;
f->valid = 0;
}
return rc;
}
int vfs_stdio_opendir(void* user, const char* _path) {
auto f = static_cast<FtpVfsDir*>(user);
const auto path = vfs_stdio_fix_path(_path);
f->fd = opendir(path);
if (!f->fd) {
return -1;
}
return 0;
}
const char* vfs_stdio_readdir(void* user, void* user_entry) {
auto f = static_cast<FtpVfsDir*>(user);
auto entry = static_cast<FtpVfsDirEntry*>(user_entry);
entry->buf = readdir(f->fd);
if (!entry->buf) {
return NULL;
}
return entry->buf->d_name;
}
int vfs_stdio_dirlstat(void* user, const void* user_entry, const char* _path, struct stat* st) {
// could probably be optimised to th below, but we won't know its r/w perms.
#if 0
auto entry = static_cast<FtpVfsDirEntry*>(user_entry);
if (entry->buf->d_type == DT_DIR) {
st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
st->st_nlink = 1;
return 0;
}
#else
#endif
const auto path = vfs_stdio_fix_path(_path);
return lstat(path, st);
}
int vfs_stdio_isdir_open(void* user) {
auto f = static_cast<FtpVfsDir*>(user);
return f->fd != NULL;
}
int vfs_stdio_closedir(void* user) {
auto f = static_cast<FtpVfsDir*>(user);
int rc = 0;
if (vfs_stdio_isdir_open(f)) {
rc = closedir(f->fd);
f->fd = NULL;
}
return rc;
}
int vfs_stdio_stat(const char* _path, struct stat* st) {
const auto path = vfs_stdio_fix_path(_path);
return stat(path, st);
}
int vfs_stdio_mkdir(const char* _path) {
const auto path = vfs_stdio_fix_path(_path);
return mkdir(path, 0777);
}
int vfs_stdio_unlink(const char* _path) {
const auto path = vfs_stdio_fix_path(_path);
return unlink(path);
}
int vfs_stdio_rmdir(const char* _path) {
const auto path = vfs_stdio_fix_path(_path);
return rmdir(path);
}
int vfs_stdio_rename(const char* _src, const char* _dst) {
const auto src = vfs_stdio_fix_path(_src);
const auto dst = vfs_stdio_fix_path(_dst);
return rename(src, dst);
}
FtpVfs g_vfs_stdio = {
.open = vfs_stdio_open,
.read = vfs_stdio_read,
.write = vfs_stdio_write,
.seek = vfs_stdio_seek,
.close = vfs_stdio_close,
.isfile_open = vfs_stdio_isfile_open,
.opendir = vfs_stdio_opendir,
.readdir = vfs_stdio_readdir,
.dirlstat = vfs_stdio_dirlstat,
.closedir = vfs_stdio_closedir,
.isdir_open = vfs_stdio_isdir_open,
.stat = vfs_stdio_stat,
.lstat = vfs_stdio_stat,
.mkdir = vfs_stdio_mkdir,
.unlink = vfs_stdio_unlink,
.rmdir = vfs_stdio_rmdir,
.rename = vfs_stdio_rename,
};
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);
vfs_nx_init(g_custom_vfs.data(), std::size(g_custom_vfs), g_ftpsrv_mount_flags, false);
}
ON_SCOPE_EXIT(
@@ -363,16 +504,93 @@ bool Init() {
return false;
}
// if (R_FAILED(fsdev_wrapMountSdmc())) {
// log_write("[FTP] cannot mount sdmc\n");
// return false;
// }
g_ftpsrv_mount_flags = 0;
g_ftpsrv_config = {};
g_custom_vfs.clear();
// todo: replace everything with ini_browse for faster loading.
// or load everything in the init thread.
auto app = App::GetApp();
g_ftpsrv_config.log_callback = ftp_log_callback;
g_ftpsrv_config.progress_callback = ftp_progress_callback;
g_ftpsrv_config.anon = app->m_ftp_anon.Get();
std::strncpy(g_ftpsrv_config.user, app->m_ftp_user.Get().c_str(), sizeof(g_ftpsrv_config.user) - 1);
std::strncpy(g_ftpsrv_config.pass, app->m_ftp_pass.Get().c_str(), sizeof(g_ftpsrv_config.pass) - 1);
g_ftpsrv_config.port = app->m_ftp_port.Get();
if (app->m_ftp_show_album.Get()) {
g_ftpsrv_mount_flags |= VfsNxMountFlag_ALBUM;
}
if (app->m_ftp_show_ams_contents.Get()) {
g_ftpsrv_mount_flags |= VfsNxMountFlag_AMS_CONTENTS;
}
if (app->m_ftp_show_bis_storage.Get()) {
g_ftpsrv_mount_flags |= VfsNxMountFlag_BIS_STORAGE;
}
if (app->m_ftp_show_bis_fs.Get()) {
g_ftpsrv_mount_flags |= VfsNxMountFlag_BIS_FS;
}
if (app->m_ftp_show_content_system.Get()) {
g_ftpsrv_mount_flags |= VfsNxMountFlag_CONTENT_SYSTEM;
}
if (app->m_ftp_show_content_user.Get()) {
g_ftpsrv_mount_flags |= VfsNxMountFlag_CONTENT_USER;
}
if (app->m_ftp_show_content_sd.Get()) {
g_ftpsrv_mount_flags |= VfsNxMountFlag_CONTENT_SDCARD;
}
#if 0
if (app->m_ftp_show_content_sd0.Get()) {
g_ftpsrv_mount_flags |= VfsNxMountFlag_CONTENT_SDCARD0;
}
if (app->m_ftp_show_custom_system.Get()) {
g_ftpsrv_mount_flags |= VfsNxMountFlag_CUSTOM_SYSTEM;
}
if (app->m_ftp_show_custom_sd.Get()) {
g_ftpsrv_mount_flags |= VfsNxMountFlag_CUSTOM_SDCARD;
}
#endif
if (app->m_ftp_show_switch.Get()) {
g_ftpsrv_mount_flags |= VfsNxMountFlag_SWITCH;
}
if (app->m_ftp_show_install.Get()) {
g_custom_vfs.push_back({
.name = "install",
.user = &g_shared_data,
.func = &g_vfs_install,
});
}
if (app->m_ftp_show_games.Get()) {
g_custom_vfs.push_back({
.name = "games",
.user = NULL,
.func = &g_vfs_stdio,
});
}
if (app->m_ftp_show_mounts.Get()) {
g_custom_vfs.push_back({
.name = "mounts",
.user = NULL,
.func = &g_vfs_stdio,
});
}
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 (!std::strlen(g_ftpsrv_config.user) && !std::strlen(g_ftpsrv_config.pass)) {
g_ftpsrv_config.anon = true;
log_write("[FTP] no user pass, defaulting to anon\n");
}
Result rc;
if (R_FAILED(rc = utils::CreateThread(&g_thread, loop, nullptr, 1024*16))) {
if (R_FAILED(rc = utils::CreateThread(&g_thread, loop, nullptr))) {
log_write("[FTP] failed to create nxlink thread: 0x%X\n", rc);
return false;
}
@@ -399,7 +617,9 @@ void Exit() {
threadWaitForExit(&g_thread);
threadClose(&g_thread);
memset(&g_ftpsrv_config, 0, sizeof(g_ftpsrv_config));
std::memset(&g_ftpsrv_config, 0, sizeof(g_ftpsrv_config));
g_custom_vfs.clear();
g_ftpsrv_mount_flags = 0;
log_write("[FTP] exitied\n");
}

View File

@@ -4,9 +4,6 @@
#include <mbedtls/md5.h>
#include <utility>
#include <zlib.h>
#include <zstd.h>
namespace sphaira::hash {
namespace {
@@ -81,131 +78,6 @@ 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, s64 file_size) override {
m_seed = crc32CalculateWithSeed(m_seed, buf, size);
@@ -328,8 +200,6 @@ auto GetTypeStr(Type type) -> const char* {
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 "";
}
@@ -341,8 +211,6 @@ Result Hash(ui::ProgressBox* pbox, Type type, BaseSource* source, std::string& o
case Type::Sha1: return Hash(pbox, std::make_unique<HashSha1>(), source, out);
case Type::Sha256: return Hash(pbox, std::make_unique<HashSha256>(), source, out);
case Type::Null: return Hash(pbox, std::make_unique<HashNull>(), source, out);
case Type::Deflate: return Hash(pbox, std::make_unique<HashDeflate>(), source, out);
case Type::Zstd: return Hash(pbox, std::make_unique<HashZstd>(), source, out);
}
std::unreachable();
}

View File

@@ -9,7 +9,7 @@
#include <algorithm>
#include <haze.h>
namespace sphaira::haze {
namespace sphaira::libhaze {
namespace {
struct InstallSharedData {
@@ -56,22 +56,23 @@ void on_thing() {
}
}
struct FsProxyBase : ::haze::FileSystemProxyImpl {
struct FsProxyBase : haze::FileSystemProxyImpl {
FsProxyBase(const char* name, const char* display_name) : m_name{name}, m_display_name{display_name} {
}
auto FixPath(const char* path) const {
auto FixPath(const char* base, const char* path) const {
fs::FsPath buf;
const auto len = std::strlen(GetName());
if (len && !strncasecmp(path + 1, GetName(), len)) {
std::snprintf(buf, sizeof(buf), "/%s", path + 1 + len);
if (len && !strncasecmp(path, GetName(), len)) {
std::snprintf(buf, sizeof(buf), "%s/%s", base, path + len);
} else {
std::strcpy(buf, path);
std::snprintf(buf, sizeof(buf), "%s/%s", base, path);
// std::strcpy(buf, path);
}
// log_write("[FixPath] %s -> %s\n", path, buf.s);
log_write("[FixPath] %s -> %s\n", path, buf.s);
return buf;
}
@@ -88,6 +89,10 @@ protected:
};
struct FsProxy final : FsProxyBase {
using File = fs::File;
using Dir = fs::Dir;
using DirEntry = FsDirectoryEntry;
FsProxy(std::unique_ptr<fs::Fs>&& fs, const char* name, const char* display_name)
: FsProxyBase{name, display_name}
, m_fs{std::forward<decltype(fs)>(fs)} {
@@ -100,137 +105,183 @@ struct FsProxy final : FsProxyBase {
}
}
auto FixPath(const char* path) const {
return FsProxyBase::FixPath(m_fs->Root(), path);
}
// TODO: impl this for stdio
Result GetTotalSpace(const char *path, s64 *out) override {
if (m_fs->IsNative()) {
auto fs = (fs::FsNative*)m_fs.get();
return fsFsGetTotalSpace(&fs->m_fs, FixPath(path), out);
}
// todo: use statvfs.
// then fallback to 256gb if not available.
*out = 1024ULL * 1024ULL * 1024ULL * 256ULL;
R_SUCCEED();
}
Result GetFreeSpace(const char *path, s64 *out) override {
if (m_fs->IsNative()) {
auto fs = (fs::FsNative*)m_fs.get();
return fsFsGetFreeSpace(&fs->m_fs, FixPath(path), out);
}
// todo: use statvfs.
// then fallback to 256gb if not available.
*out = 1024ULL * 1024ULL * 1024ULL * 256ULL;
R_SUCCEED();
}
Result GetEntryType(const char *path, FsDirEntryType *out_entry_type) override {
const auto rc = m_fs->GetEntryType(FixPath(path), out_entry_type);
log_write("[HAZE] GetEntryType(%s) 0x%X\n", path, rc);
return rc;
Result GetEntryType(const char *path, haze::FileAttrType *out_entry_type) override {
FsDirEntryType type;
R_TRY(m_fs->GetEntryType(FixPath(path), &type));
*out_entry_type = (type == FsDirEntryType_Dir) ? haze::FileAttrType_DIR : haze::FileAttrType_FILE;
R_SUCCEED();
}
Result CreateFile(const char* path, s64 size, u32 option) override {
Result GetEntryAttributes(const char *path, haze::FileAttr *out) override {
FsDirEntryType type;
R_TRY(m_fs->GetEntryType(FixPath(path), &type));
if (type == FsDirEntryType_File) {
out->type = haze::FileAttrType_FILE;
// it doesn't matter if this fails.
s64 size{};
FsTimeStampRaw timestamp{};
R_TRY(m_fs->FileGetSizeAndTimestamp(FixPath(path), &timestamp, &size));
out->size = size;
if (timestamp.is_valid) {
out->ctime = timestamp.created;
out->mtime = timestamp.modified;
}
} else {
out->type = haze::FileAttrType_DIR;
}
if (IsReadOnly()) {
out->flag |= haze::FileAttrFlag_READ_ONLY;
}
R_SUCCEED();
}
Result CreateFile(const char* path, s64 size) override {
log_write("[HAZE] CreateFile(%s)\n", path);
return m_fs->CreateFile(FixPath(path), size, option);
return m_fs->CreateFile(FixPath(path), 0, 0);
}
Result DeleteFile(const char* path) override {
log_write("[HAZE] DeleteFile(%s)\n", path);
return m_fs->DeleteFile(FixPath(path));
}
Result RenameFile(const char *old_path, const char *new_path) override {
log_write("[HAZE] RenameFile(%s -> %s)\n", old_path, new_path);
return m_fs->RenameFile(FixPath(old_path), FixPath(new_path));
}
Result OpenFile(const char *path, u32 mode, FsFile *out_file) override {
log_write("[HAZE] OpenFile(%s)\n", path);
auto fptr = new fs::File();
const auto rc = m_fs->OpenFile(FixPath(path), mode, fptr);
if (R_SUCCEEDED(rc)) {
std::memcpy(&out_file->s, &fptr, sizeof(fptr));
} else {
delete fptr;
Result OpenFile(const char *path, haze::FileOpenMode mode, haze::File *out_file) override {
log_write("[HAZE] OpenFile(%s)\n", path);
u32 flags = FsOpenMode_Read;
if (mode == haze::FileOpenMode_WRITE) {
flags = FsOpenMode_Write | FsOpenMode_Append;
}
return rc;
auto f = new File();
const auto rc = m_fs->OpenFile(FixPath(path), flags, f);
if (R_FAILED(rc)) {
log_write("[HAZE] OpenFile(%s) failed: 0x%X\n", path, rc);
delete f;
return rc;
}
out_file->impl = f;
R_SUCCEED();
}
Result GetFileSize(FsFile *file, s64 *out_size) override {
log_write("[HAZE] GetFileSize()\n");
fs::File* f;
std::memcpy(&f, &file->s, sizeof(f));
Result GetFileSize(haze::File *file, s64 *out_size) override {
auto f = static_cast<File*>(file->impl);
return f->GetSize(out_size);
}
Result SetFileSize(FsFile *file, s64 size) override {
log_write("[HAZE] SetFileSize(%zd)\n", size);
fs::File* f;
std::memcpy(&f, &file->s, sizeof(f));
Result SetFileSize(haze::File *file, s64 size) override {
auto f = static_cast<File*>(file->impl);
return f->SetSize(size);
}
Result ReadFile(FsFile *file, s64 off, void *buf, u64 read_size, u32 option, u64 *out_bytes_read) override {
log_write("[HAZE] ReadFile(%zd, %zu)\n", off, read_size);
fs::File* f;
std::memcpy(&f, &file->s, sizeof(f));
return f->Read(off, buf, read_size, option, out_bytes_read);
Result ReadFile(haze::File *file, s64 off, void *buf, u64 read_size, u64 *out_bytes_read) override {
auto f = static_cast<File*>(file->impl);
return f->Read(off, buf, read_size, FsReadOption_None, out_bytes_read);
}
Result WriteFile(FsFile *file, s64 off, const void *buf, u64 write_size, u32 option) override {
log_write("[HAZE] WriteFile(%zd, %zu)\n", off, write_size);
fs::File* f;
std::memcpy(&f, &file->s, sizeof(f));
return f->Write(off, buf, write_size, option);
Result WriteFile(haze::File *file, s64 off, const void *buf, u64 write_size) override {
auto f = static_cast<File*>(file->impl);
return f->Write(off, buf, write_size, FsWriteOption_None);
}
void CloseFile(FsFile *file) override {
log_write("[HAZE] CloseFile()\n");
fs::File* f;
std::memcpy(&f, &file->s, sizeof(f));
void CloseFile(haze::File *file) override {
auto f = static_cast<File*>(file->impl);
if (f) {
delete f;
file->impl = nullptr;
}
std::memset(file, 0, sizeof(*file));
}
Result CreateDirectory(const char* path) override {
log_write("[HAZE] DeleteFile(%s)\n", path);
return m_fs->CreateDirectory(FixPath(path));
}
Result DeleteDirectoryRecursively(const char* path) override {
log_write("[HAZE] DeleteDirectoryRecursively(%s)\n", path);
return m_fs->DeleteDirectoryRecursively(FixPath(path));
}
Result RenameDirectory(const char *old_path, const char *new_path) override {
log_write("[HAZE] RenameDirectory(%s -> %s)\n", old_path, new_path);
return m_fs->RenameDirectory(FixPath(old_path), FixPath(new_path));
}
Result OpenDirectory(const char *path, u32 mode, FsDir *out_dir) override {
auto fptr = new fs::Dir();
const auto rc = m_fs->OpenDirectory(FixPath(path), mode, fptr);
if (R_SUCCEEDED(rc)) {
std::memcpy(&out_dir->s, &fptr, sizeof(fptr));
} else {
delete fptr;
Result OpenDirectory(const char *path, haze::Dir *out_dir) override {
auto dir = new Dir();
const auto rc = m_fs->OpenDirectory(FixPath(path), FsDirOpenMode_ReadDirs | FsDirOpenMode_ReadFiles | FsDirOpenMode_NoFileSize, dir);
if (R_FAILED(rc)) {
log_write("[HAZE] OpenDirectory(%s) failed: 0x%X\n", path, rc);
delete dir;
return rc;
}
log_write("[HAZE] OpenDirectory(%s) 0x%X\n", path, rc);
return rc;
out_dir->impl = dir;
R_SUCCEED();
}
Result ReadDirectory(FsDir *d, s64 *out_total_entries, size_t max_entries, FsDirectoryEntry *buf) override {
fs::Dir* f;
std::memcpy(&f, &d->s, sizeof(f));
const auto rc = f->Read(out_total_entries, max_entries, buf);
log_write("[HAZE] ReadDirectory(%zd) 0x%X\n", *out_total_entries, rc);
return rc;
}
Result GetDirectoryEntryCount(FsDir *d, s64 *out_count) override {
fs::Dir* f;
std::memcpy(&f, &d->s, sizeof(f));
const auto rc = f->GetEntryCount(out_count);
log_write("[HAZE] GetDirectoryEntryCount(%zd) 0x%X\n", *out_count, rc);
return rc;
}
void CloseDirectory(FsDir *d) override {
log_write("[HAZE] CloseDirectory()\n");
fs::Dir* f;
std::memcpy(&f, &d->s, sizeof(f));
if (f) {
delete f;
Result ReadDirectory(haze::Dir *d, s64 *out_total_entries, size_t max_entries, haze::DirEntry *buf) override {
auto dir = static_cast<Dir*>(d->impl);
std::vector<FsDirectoryEntry> entries(max_entries);
R_TRY(dir->Read(out_total_entries, entries.size(), entries.data()));
for (s64 i = 0; i < *out_total_entries; i++) {
std::strcpy(buf[i].name, entries[i].name);
}
std::memset(d, 0, sizeof(*d));
R_SUCCEED();
}
virtual bool MultiThreadTransfer(s64 size, bool read) override {
return !App::IsFileBaseEmummc();
Result GetDirectoryEntryCount(haze::Dir *d, s64 *out_count) override {
auto dir = static_cast<Dir*>(d->impl);
return dir->GetEntryCount(out_count);
}
void CloseDirectory(haze::Dir *d) override {
auto dir = static_cast<Dir*>(d->impl);
if (dir) {
delete dir;
d->impl = nullptr;
}
}
private:
@@ -240,9 +291,22 @@ private:
// fake fs that allows for files to create r/w on the root.
// folders are not yet supported.
struct FsProxyVfs : FsProxyBase {
struct File {
u64 index{};
haze::FileOpenMode mode{};
};
struct Dir {
u64 pos{};
};
using FsProxyBase::FsProxyBase;
virtual ~FsProxyVfs() = default;
auto FixPath(const char* path) const {
return FsProxyBase::FixPath("", path);
}
auto GetFileName(const char* s) -> const char* {
const auto file_name = std::strrchr(s, '/');
if (!file_name || file_name[1] == '\0') {
@@ -251,9 +315,9 @@ struct FsProxyVfs : FsProxyBase {
return file_name + 1;
}
virtual Result GetEntryType(const char *path, FsDirEntryType *out_entry_type) {
virtual Result GetEntryType(const char *path, haze::FileAttrType *out_entry_type) {
if (FixPath(path) == "/") {
*out_entry_type = FsDirEntryType_Dir;
*out_entry_type = haze::FileAttrType_DIR;
R_SUCCEED();
} else {
const auto file_name = GetFileName(path);
@@ -264,11 +328,12 @@ struct FsProxyVfs : FsProxyBase {
});
R_UNLESS(it != m_entries.end(), FsError_PathNotFound);
*out_entry_type = FsDirEntryType_File;
*out_entry_type = haze::FileAttrType_FILE;
R_SUCCEED();
}
}
virtual Result CreateFile(const char* path, s64 size, u32 option) {
virtual Result CreateFile(const char* path, s64 size) {
const auto file_name = GetFileName(path);
R_UNLESS(file_name, FsError_PathNotFound);
@@ -285,6 +350,7 @@ struct FsProxyVfs : FsProxyBase {
m_entries.emplace_back(entry);
R_SUCCEED();
}
virtual Result DeleteFile(const char* path) {
const auto file_name = GetFileName(path);
R_UNLESS(file_name, FsError_PathNotFound);
@@ -297,6 +363,7 @@ struct FsProxyVfs : FsProxyBase {
m_entries.erase(it);
R_SUCCEED();
}
virtual Result RenameFile(const char *old_path, const char *new_path) {
const auto file_name = GetFileName(old_path);
R_UNLESS(file_name, FsError_PathNotFound);
@@ -317,7 +384,8 @@ struct FsProxyVfs : FsProxyBase {
std::strcpy(it->name, file_name_new);
R_SUCCEED();
}
virtual Result OpenFile(const char *path, u32 mode, FsFile *out_file) {
virtual Result OpenFile(const char *path, haze::FileOpenMode mode, haze::File *out_file) {
const auto file_name = GetFileName(path);
R_UNLESS(file_name, FsError_PathNotFound);
@@ -326,65 +394,89 @@ struct FsProxyVfs : FsProxyBase {
});
R_UNLESS(it != m_entries.end(), FsError_PathNotFound);
out_file->s.object_id = std::distance(m_entries.begin(), it);
out_file->s.own_handle = mode;
auto f = new File();
f->index = std::distance(m_entries.begin(), it);
f->mode = mode;
out_file->impl = f;
R_SUCCEED();
}
virtual Result GetFileSize(FsFile *file, s64 *out_size) {
auto& e = m_entries[file->s.object_id];
*out_size = e.file_size;
virtual Result GetFileSize(haze::File *file, s64 *out_size) {
auto f = static_cast<File*>(file->impl);
*out_size = m_entries[f->index].file_size;
R_SUCCEED();
}
virtual Result SetFileSize(FsFile *file, s64 size) {
auto& e = m_entries[file->s.object_id];
e.file_size = size;
virtual Result SetFileSize(haze::File *file, s64 size) {
auto f = static_cast<File*>(file->impl);
m_entries[f->index].file_size = size;
R_SUCCEED();
}
virtual Result ReadFile(FsFile *file, s64 off, void *buf, u64 read_size, u32 option, u64 *out_bytes_read) {
virtual Result ReadFile(haze::File *file, s64 off, void *buf, u64 read_size, u64 *out_bytes_read) {
// stub for now as it may confuse users who think that the returned file is valid.
// the code below can be used to benchmark mtp reads.
R_THROW(FsError_NotImplemented);
// auto& e = m_entries[file->s.object_id];
// read_size = std::min<s64>(e.file_size - off, read_size);
// std::memset(buf, 0, read_size);
// *out_bytes_read = read_size;
// R_SUCCEED();
}
virtual Result WriteFile(FsFile *file, s64 off, const void *buf, u64 write_size, u32 option) {
auto& e = m_entries[file->s.object_id];
virtual Result WriteFile(haze::File *file, s64 off, const void *buf, u64 write_size) {
auto f = static_cast<File*>(file->impl);
auto& e = m_entries[f->index];
e.file_size = std::max<s64>(e.file_size, off + write_size);
R_SUCCEED();
}
virtual void CloseFile(FsFile *file) {
std::memset(file, 0, sizeof(*file));
virtual void CloseFile(haze::File *file) {
auto f = static_cast<File*>(file->impl);
if (f) {
delete f;
file->impl = nullptr;
}
}
Result CreateDirectory(const char* path) override {
R_THROW(FsError_NotImplemented);
}
Result DeleteDirectoryRecursively(const char* path) override {
R_THROW(FsError_NotImplemented);
}
Result RenameDirectory(const char *old_path, const char *new_path) override {
R_THROW(FsError_NotImplemented);
}
Result OpenDirectory(const char *path, u32 mode, FsDir *out_dir) override {
std::memset(out_dir, 0, sizeof(*out_dir));
Result OpenDirectory(const char *path, haze::Dir *out_dir) override {
auto dir = new Dir();
out_dir->impl = dir;
R_SUCCEED();
}
Result ReadDirectory(FsDir *d, s64 *out_total_entries, size_t max_entries, FsDirectoryEntry *buf) override {
max_entries = std::min<s64>(m_entries.size()- d->s.object_id, max_entries);
std::memcpy(buf, m_entries.data() + d->s.object_id, max_entries * sizeof(*buf));
d->s.object_id += max_entries;
Result ReadDirectory(haze::Dir *d, s64 *out_total_entries, size_t max_entries, haze::DirEntry *buf) override {
auto dir = static_cast<Dir*>(d->impl);
max_entries = std::min<s64>(m_entries.size() - dir->pos, max_entries);
for (size_t i = 0; i < max_entries; i++) {
std::strcpy(buf[i].name, m_entries[dir->pos + i].name);
}
dir->pos += max_entries;
*out_total_entries = max_entries;
R_SUCCEED();
}
Result GetDirectoryEntryCount(FsDir *d, s64 *out_count) override {
Result GetDirectoryEntryCount(haze::Dir *d, s64 *out_count) override {
*out_count = m_entries.size();
R_SUCCEED();
}
void CloseDirectory(FsDir *d) override {
std::memset(d, 0, sizeof(*d));
void CloseDirectory(haze::Dir *d) override {
auto dir = static_cast<Dir*>(d->impl);
if (dir) {
delete dir;
d->impl = nullptr;
}
}
protected:
@@ -398,13 +490,11 @@ struct FsDevNullProxy final : FsProxyVfs {
*out = 1024ULL * 1024ULL * 1024ULL * 256ULL;
R_SUCCEED();
}
Result GetFreeSpace(const char *path, s64 *out) override {
*out = 1024ULL * 1024ULL * 1024ULL * 256ULL;
R_SUCCEED();
}
bool MultiThreadTransfer(s64 size, bool read) override {
return true;
}
};
struct FsInstallProxy final : FsProxyVfs {
@@ -447,6 +537,7 @@ struct FsInstallProxy final : FsProxyVfs {
return fs::FsNativeContentStorage(FsContentStorageId_User).GetTotalSpace("/", out);
}
}
Result GetFreeSpace(const char *path, s64 *out) override {
if (App::GetApp()->m_install_sd.Get()) {
return fs::FsNativeContentStorage(FsContentStorageId_SdCard).GetFreeSpace("/", out);
@@ -455,27 +546,30 @@ struct FsInstallProxy final : FsProxyVfs {
}
}
Result GetEntryType(const char *path, FsDirEntryType *out_entry_type) override {
Result GetEntryType(const char *path, haze::FileAttrType *out_entry_type) override {
R_TRY(FsProxyVfs::GetEntryType(path, out_entry_type));
if (*out_entry_type == FsDirEntryType_File) {
if (*out_entry_type == haze::FileAttrType_FILE) {
R_TRY(FailedIfNotEnabled());
}
R_SUCCEED();
}
Result CreateFile(const char* path, s64 size, u32 option) override {
Result CreateFile(const char* path, s64 size) override {
R_TRY(FailedIfNotEnabled());
R_TRY(IsValidFileType(path));
R_TRY(FsProxyVfs::CreateFile(path, size, option));
R_TRY(FsProxyVfs::CreateFile(path, size));
R_SUCCEED();
}
Result OpenFile(const char *path, u32 mode, FsFile *out_file) override {
Result OpenFile(const char *path, haze::FileOpenMode mode, haze::File *out_file) override {
R_TRY(FailedIfNotEnabled());
R_TRY(IsValidFileType(path));
R_TRY(FsProxyVfs::OpenFile(path, mode, out_file));
log_write("[MTP] done file open: %s mode: 0x%X\n", path, mode);
if (mode & FsOpenMode_Write) {
const auto& e = m_entries[out_file->s.object_id];
if (mode == haze::FileOpenMode_WRITE) {
auto f = static_cast<File*>(out_file->impl);
const auto& e = m_entries[f->index];
// check if we already have this file queued.
log_write("[MTP] checking if empty\n");
@@ -488,7 +582,8 @@ struct FsInstallProxy final : FsProxyVfs {
log_write("[MTP] got file: %s\n", path);
R_SUCCEED();
}
Result WriteFile(FsFile *file, s64 off, const void *buf, u64 write_size, u32 option) override {
Result WriteFile(haze::File *file, s64 off, const void *buf, u64 write_size) override {
SCOPED_MUTEX(&g_shared_data.mutex);
if (!g_shared_data.enabled) {
log_write("[MTP] failing as not enabled\n");
@@ -500,14 +595,20 @@ struct FsInstallProxy final : FsProxyVfs {
R_THROW(FsError_NotImplemented);
}
R_TRY(FsProxyVfs::WriteFile(file, off, buf, write_size, option));
R_TRY(FsProxyVfs::WriteFile(file, off, buf, write_size));
R_SUCCEED();
}
void CloseFile(FsFile *file) override {
void CloseFile(haze::File *file) override {
auto f = static_cast<File*>(file->impl);
if (!f) {
return;
}
bool update{};
{
SCOPED_MUTEX(&g_shared_data.mutex);
if (file->s.own_handle & FsOpenMode_Write) {
if (f->mode == haze::FileOpenMode_WRITE) {
log_write("[MTP] closing current file\n");
if (g_shared_data.on_close) {
g_shared_data.on_close();
@@ -525,40 +626,36 @@ struct FsInstallProxy final : FsProxyVfs {
FsProxyVfs::CloseFile(file);
}
// installs are already multi-threaded via yati.
bool MultiThreadTransfer(s64 size, bool read) override {
App::IsFileBaseEmummc();
return false;
}
};
::haze::FsEntries g_fs_entries{};
haze::FsEntries g_fs_entries{};
void haze_callback(const ::haze::CallbackData *data) {
void haze_callback(const haze::CallbackData *data) {
#if 0
auto& e = *data;
switch (e.type) {
case ::haze::CallbackType_OpenSession: log_write("[LIBHAZE] Opening Session\n"); break;
case ::haze::CallbackType_CloseSession: log_write("[LIBHAZE] Closing Session\n"); break;
case haze::CallbackType_OpenSession: log_write("[LIBHAZE] Opening Session\n"); break;
case haze::CallbackType_CloseSession: log_write("[LIBHAZE] Closing Session\n"); break;
case ::haze::CallbackType_CreateFile: log_write("[LIBHAZE] Creating File: %s\n", e.file.filename); break;
case ::haze::CallbackType_DeleteFile: log_write("[LIBHAZE] Deleting File: %s\n", e.file.filename); break;
case haze::CallbackType_CreateFile: log_write("[LIBHAZE] Creating File: %s\n", e.file.filename); break;
case haze::CallbackType_DeleteFile: log_write("[LIBHAZE] Deleting File: %s\n", e.file.filename); break;
case ::haze::CallbackType_RenameFile: log_write("[LIBHAZE] Rename File: %s -> %s\n", e.rename.filename, e.rename.newname); break;
case ::haze::CallbackType_RenameFolder: log_write("[LIBHAZE] Rename Folder: %s -> %s\n", e.rename.filename, e.rename.newname); break;
case haze::CallbackType_RenameFile: log_write("[LIBHAZE] Rename File: %s -> %s\n", e.rename.filename, e.rename.newname); break;
case haze::CallbackType_RenameFolder: log_write("[LIBHAZE] Rename Folder: %s -> %s\n", e.rename.filename, e.rename.newname); break;
case ::haze::CallbackType_CreateFolder: log_write("[LIBHAZE] Creating Folder: %s\n", e.file.filename); break;
case ::haze::CallbackType_DeleteFolder: log_write("[LIBHAZE] Deleting Folder: %s\n", e.file.filename); break;
case haze::CallbackType_CreateFolder: log_write("[LIBHAZE] Creating Folder: %s\n", e.file.filename); break;
case haze::CallbackType_DeleteFolder: log_write("[LIBHAZE] Deleting Folder: %s\n", e.file.filename); break;
case ::haze::CallbackType_ReadBegin: log_write("[LIBHAZE] Reading File Begin: %s \n", e.file.filename); break;
case ::haze::CallbackType_ReadProgress: log_write("\t[LIBHAZE] Reading File: offset: %lld size: %lld\n", e.progress.offset, e.progress.size); break;
case ::haze::CallbackType_ReadEnd: log_write("[LIBHAZE] Reading File Finished: %s\n", e.file.filename); break;
case haze::CallbackType_ReadBegin: log_write("[LIBHAZE] Reading File Begin: %s \n", e.file.filename); break;
case haze::CallbackType_ReadProgress: log_write("\t[LIBHAZE] Reading File: offset: %lld size: %lld\n", e.progress.offset, e.progress.size); break;
case haze::CallbackType_ReadEnd: log_write("[LIBHAZE] Reading File Finished: %s\n", e.file.filename); break;
case ::haze::CallbackType_WriteBegin: log_write("[LIBHAZE] Writing File Begin: %s \n", e.file.filename); break;
case ::haze::CallbackType_WriteProgress: log_write("\t[LIBHAZE] Writing File: offset: %lld size: %lld\n", e.progress.offset, e.progress.size); break;
case ::haze::CallbackType_WriteEnd: log_write("[LIBHAZE] Writing File Finished: %s\n", e.file.filename); break;
case haze::CallbackType_WriteBegin: log_write("[LIBHAZE] Writing File Begin: %s \n", e.file.filename); break;
case haze::CallbackType_WriteProgress: log_write("\t[LIBHAZE] Writing File: offset: %lld size: %lld\n", e.progress.offset, e.progress.size); break;
case haze::CallbackType_WriteEnd: log_write("[LIBHAZE] Writing File Finished: %s\n", e.file.filename); break;
}
#endif
App::NotifyFlashLed();
}
@@ -572,14 +669,43 @@ bool Init() {
return false;
}
// add default mount of the sd card.
g_fs_entries.emplace_back(std::make_shared<FsProxy>(std::make_unique<fs::FsNativeSd>(), "", "microSD card"));
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)"));
g_fs_entries.emplace_back(std::make_shared<FsInstallProxy>("install", "Install (NSP, XCI, NSZ, XCZ)"));
if (App::GetApp()->m_mtp_show_album.Get()) {
g_fs_entries.emplace_back(std::make_shared<FsProxy>(std::make_unique<fs::FsNativeImage>(FsImageDirectoryId_Sd), "Album", "Album (Image SD)"));
}
if (App::GetApp()->m_mtp_show_content_sd.Get()) {
g_fs_entries.emplace_back(std::make_shared<FsProxy>(std::make_unique<fs::FsNativeContentStorage>(FsContentStorageId_SdCard), "ContentsM", "Contents (microSD card)"));
}
if (App::GetApp()->m_mtp_show_content_system.Get()) {
g_fs_entries.emplace_back(std::make_shared<FsProxy>(std::make_unique<fs::FsNativeContentStorage>(FsContentStorageId_System), "ContentsS", "Contents (System)"));
}
if (App::GetApp()->m_mtp_show_content_user.Get()) {
g_fs_entries.emplace_back(std::make_shared<FsProxy>(std::make_unique<fs::FsNativeContentStorage>(FsContentStorageId_User), "ContentsU", "Contents (User)"));
}
if (App::GetApp()->m_mtp_show_games.Get()) {
g_fs_entries.emplace_back(std::make_shared<FsProxy>(std::make_unique<fs::FsStdio>(true, "games:/"), "Games", "Games"));
}
if (App::GetApp()->m_mtp_show_install.Get()) {
g_fs_entries.emplace_back(std::make_shared<FsInstallProxy>("install", "Install (NSP, XCI, NSZ, XCZ)"));
}
if (App::GetApp()->m_mtp_show_mounts.Get()) {
g_fs_entries.emplace_back(std::make_shared<FsProxy>(std::make_unique<fs::FsStdio>(true, "mounts:/"), "Mounts", "Mounts"));
}
if (App::GetApp()->m_mtp_show_speedtest.Get()) {
g_fs_entries.emplace_back(std::make_shared<FsDevNullProxy>("DevNull", "DevNull (Speed Test)"));
}
g_should_exit = false;
if (!::haze::Initialize(haze_callback, THREAD_PRIO, THREAD_CORE, g_fs_entries)) {
if (!haze::Initialize(haze_callback, g_fs_entries, App::GetApp()->m_mtp_vid.Get(), App::GetApp()->m_mtp_pid.Get())) {
return false;
}
@@ -598,7 +724,7 @@ void Exit() {
return;
}
::haze::Exit();
haze::Exit();
g_is_running = false;
g_should_exit = true;
g_fs_entries.clear();
@@ -619,4 +745,4 @@ void DisableInstallMode() {
g_shared_data.enabled = false;
}
} // namespace sphaira::haze
} // namespace sphaira::libhaze

View File

@@ -8,12 +8,15 @@
namespace sphaira::i18n {
namespace {
std::vector<u8> g_i18n_data;
yyjson_doc* json;
yyjson_val* root;
std::unordered_map<std::string, std::string> g_tr_cache;
std::vector<u8> g_i18n_data{};
yyjson_doc* json{};
yyjson_val* root{};
std::unordered_map<std::string, std::string> g_tr_cache{};
Mutex g_mutex{};
std::string get_internal(std::string_view str) {
SCOPED_MUTEX(&g_mutex);
const std::string kkey = {str.data(), str.length()};
if (auto it = g_tr_cache.find(kkey); it != g_tr_cache.end()) {
@@ -50,6 +53,8 @@ std::string get_internal(std::string_view str) {
} // namespace
bool init(long index) {
SCOPED_MUTEX(&g_mutex);
g_tr_cache.clear();
R_TRY_RESULT(romfsInit(), false);
ON_SCOPE_EXIT( romfsExit() );
@@ -87,7 +92,7 @@ bool init(long index) {
case SetLanguage_DE: lang_name = "de"; break;
case SetLanguage_IT: lang_name = "it"; break;
case SetLanguage_ES: lang_name = "es"; break;
case SetLanguage_ZHCN: lang_name = "zh"; break;
case SetLanguage_ZHCN: lang_name = "zh"; break;
case SetLanguage_KO: lang_name = "ko"; break;
case SetLanguage_NL: lang_name = "nl"; break;
case SetLanguage_PT: lang_name = "pt"; break;
@@ -128,6 +133,8 @@ bool init(long index) {
}
void exit() {
SCOPED_MUTEX(&g_mutex);
if (json) {
yyjson_doc_free(json);
json = nullptr;

View File

@@ -1,84 +1,50 @@
#include "location.hpp"
#include "fs.hpp"
#include "app.hpp"
#include "usbdvd.hpp"
#include "utils/devoptab.hpp"
#include <ff.h>
#include <cstring>
#include <minIni.h>
#include <usbhsfs.h>
#ifdef ENABLE_LIBUSBDVD
#include "usbdvd.hpp"
#endif // ENABLE_LIBUSBDVD
#ifdef ENABLE_LIBUSBHSFS
#include <usbhsfs.h>
#endif // ENABLE_LIBUSBHSFS
namespace sphaira::location {
namespace {
constexpr fs::FsPath location_path{"/config/sphaira/locations.ini"};
} // namespace
void Add(const Entry& e) {
if (e.name.empty() || e.url.empty()) {
return;
}
ini_puts(e.name.c_str(), "url", e.url.c_str(), location_path);
if (!e.user.empty()) {
ini_puts(e.name.c_str(), "user", e.user.c_str(), location_path);
}
if (!e.pass.empty()) {
ini_puts(e.name.c_str(), "pass", e.pass.c_str(), location_path);
}
if (!e.bearer.empty()) {
ini_puts(e.name.c_str(), "bearer", e.bearer.c_str(), location_path);
}
if (!e.pub_key.empty()) {
ini_puts(e.name.c_str(), "pub_key", e.pub_key.c_str(), location_path);
}
if (!e.priv_key.empty()) {
ini_puts(e.name.c_str(), "priv_key", e.priv_key.c_str(), location_path);
}
if (e.port) {
ini_putl(e.name.c_str(), "port", e.port, location_path);
}
}
auto Load() -> Entries {
Entries out{};
auto cb = [](const mTCHAR *Section, const mTCHAR *Key, const mTCHAR *Value, void *UserData) -> int {
auto e = static_cast<Entries*>(UserData);
// add new entry if use section changed.
if (e->empty() || std::strcmp(Section, e->back().name.c_str())) {
e->emplace_back(Section);
}
if (!std::strcmp(Key, "url")) {
e->back().url = Value;
} else if (!std::strcmp(Key, "user")) {
e->back().user = Value;
} else if (!std::strcmp(Key, "pass")) {
e->back().pass = Value;
} else if (!std::strcmp(Key, "bearer")) {
e->back().bearer = Value;
} else if (!std::strcmp(Key, "pub_key")) {
e->back().pub_key = Value;
} else if (!std::strcmp(Key, "priv_key")) {
e->back().priv_key = Value;
} else if (!std::strcmp(Key, "port")) {
e->back().port = std::atoi(Value);
}
return 1;
};
ini_browse(cb, &out, location_path);
return out;
}
auto GetStdio(bool write) -> StdioEntries {
StdioEntries out{};
const auto add_from_entries = [](StdioEntries& entries, StdioEntries& out, bool write) {
for (auto& e : entries) {
if (write && (e.flags & FsEntryFlag::FsEntryFlag_ReadOnly)) {
log_write("[STDIO] skipping read only mount: %s\n", e.name.c_str());
continue;
}
if (e.flags & FsEntryFlag::FsEntryFlag_ReadOnly) {
e.name += " (Read Only)";
}
out.emplace_back(e);
}
};
{
StdioEntries entries;
if (R_SUCCEEDED(devoptab::GetNetworkDevices(entries))) {
log_write("[LOCATION] got devoptab mounts: %zu\n", entries.size());
add_from_entries(entries, out, write);
}
}
#ifdef ENABLE_LIBUSBDVD
// try and load usbdvd entry.
// todo: check if more than 1 entry is supported.
// todo: only call if usbdvd is init.
@@ -88,7 +54,9 @@ auto GetStdio(bool write) -> StdioEntries {
out.emplace_back(entry);
}
}
#endif // ENABLE_LIBUSBDVD
#ifdef ENABLE_LIBUSBHSFS
// bail out early if usbhdd is disabled.
if (!App::GetHddEnable()) {
log_write("[USBHSFS] not enabled\n");
@@ -111,25 +79,15 @@ auto GetStdio(bool write) -> StdioEntries {
char display_name[0x100];
std::snprintf(display_name, sizeof(display_name), "%s (%s - %s - %zu GB)", e.name, LIBUSBHSFS_FS_TYPE_STR(e.fs_type), e.product_name, e.capacity / 1024 / 1024 / 1024);
out.emplace_back(e.name, display_name, e.write_protect);
u32 flags = 0;
if (e.write_protect || (e.flags & UsbHsFsMountFlags_ReadOnly)) {
flags |= FsEntryFlag::FsEntryFlag_ReadOnly;
}
out.emplace_back(e.name, display_name, flags);
log_write("\t[USBHSFS] %s name: %s serial: %s man: %s\n", e.name, e.product_name, e.serial_number, e.manufacturer);
}
return out;
}
auto GetFat() -> StdioEntries {
StdioEntries out{};
for (auto& e : VolumeStr) {
char path[64];
std::snprintf(path, sizeof(path), "%s:/", e);
char display_name[0x100];
std::snprintf(display_name, sizeof(display_name), "%s (Read Only)", path);
out.emplace_back(path, display_name, true);
}
#endif // ENABLE_LIBUSBHSFS
return out;
}

View File

@@ -56,8 +56,6 @@ void userAppInit(void) {
diagAbortWithResult(rc);
if (R_FAILED(rc = plInitialize(PlServiceType_User)))
diagAbortWithResult(rc);
if (R_FAILED(rc = psmInitialize()))
diagAbortWithResult(rc);
if (R_FAILED(rc = nifmInitialize(NifmServiceType_User)))
diagAbortWithResult(rc);
if (R_FAILED(rc = accountInitialize(is_application ? AccountServiceType_Application : AccountServiceType_System)))
@@ -83,7 +81,6 @@ void userAppExit(void) {
setExit();
accountExit();
nifmExit();
psmExit();
plExit();
socketExit();
// NOTE (DMC): prevents exfat corruption.

View File

@@ -223,7 +223,6 @@ 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);
}
@@ -373,7 +372,7 @@ void FileFuncNative(zlib_filefunc64_def* funcs) {
Result PeekFirstFileName(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& name) {
fs::File file;
R_TRY(fs->OpenFile(path, FsOpenMode_Read, &file));
R_TRY(fs->OpenFile(path, fs::OpenMode_ReadBuffered, &file));
mmz_LocalHeader local_hdr;
u64 bytes_read;

View File

@@ -20,7 +20,7 @@ auto OptionBase<T>::GetInternal(const char* name) -> T {
} 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];
char buf[PATH_MAX]{};
ini_gets(m_section.c_str(), name, m_default_value.c_str(), buf, sizeof(buf), App::CONFIG_PATH);
m_value = buf;
}

View File

@@ -6,11 +6,11 @@ namespace sphaira::swkbd {
namespace {
struct Config {
char out_text[FS_MAX_PATH]{};
char out_text[PATH_MAX]{};
bool numpad{};
};
Result ShowInternal(Config& cfg, const char* guide, const char* initial, s64 len_min, s64 len_max) {
Result ShowInternal(Config& cfg, const char* header, const char* guide, const char* initial, s64 len_min, s64 len_max) {
SwkbdConfig c;
R_TRY(swkbdCreate(&c, 0));
swkbdConfigMakePresetDefault(&c);
@@ -20,7 +20,17 @@ Result ShowInternal(Config& cfg, const char* guide, const char* initial, s64 len
swkbdConfigSetType(&c, SwkbdType_NumPad);
}
// only works if len_max <= 32.
if (header) {
swkbdConfigSetHeaderText(&c, header);
}
if (guide) {
// only works if len_max <= 32.
if (header) {
swkbdConfigSetSubText(&c, guide);
}
swkbdConfigSetGuideText(&c, guide);
}
@@ -41,17 +51,17 @@ Result ShowInternal(Config& cfg, const char* guide, const char* initial, s64 len
} // namespace
Result ShowText(std::string& out, const char* guide, const char* initial, s64 len_min, s64 len_max) {
Result ShowText(std::string& out, const char* header, const char* guide, const char* initial, s64 len_min, s64 len_max) {
Config cfg{};
R_TRY(ShowInternal(cfg, guide, initial, len_min, len_max));
R_TRY(ShowInternal(cfg, header, guide, initial, len_min, len_max));
out = cfg.out_text;
R_SUCCEED();
}
Result ShowNumPad(s64& out, const char* guide, const char* initial, s64 len_min, s64 len_max) {
Result ShowNumPad(s64& out, const char* header, const char* guide, const char* initial, s64 len_min, s64 len_max) {
Config cfg{};
cfg.numpad = true;
R_TRY(ShowInternal(cfg, guide, initial, len_min, len_max));
R_TRY(ShowInternal(cfg, header, guide, initial, len_min, len_max));
out = std::atoll(cfg.out_text);
R_SUCCEED();
}

View File

@@ -82,6 +82,14 @@ struct ThreadData {
return read_running || decompress_running || write_running;
}
auto GetReadOffset() volatile const -> s64 {
return read_offset;
}
auto GetDecompressOffset() volatile const -> s64 {
return decompress_offset;
}
auto GetWriteOffset() volatile const -> s64 {
return write_offset;
}
@@ -94,8 +102,16 @@ struct ThreadData {
return &m_uevent_done;
}
auto GetProgressEvent() {
return &m_uevent_progres;
auto GetReadProgressEvent() {
return &m_uevent_read_progress;
}
auto GetDecompressProgressEvent() {
return &m_uevent_decompress_progress;
}
auto GetWriteProgressEvent() {
return &m_uevent_write_progress;
}
void SetReadResult(Result result) {
@@ -174,7 +190,9 @@ private:
CondVar can_pull_write{};
UEvent m_uevent_done{};
UEvent m_uevent_progres{};
UEvent m_uevent_read_progress{};
UEvent m_uevent_decompress_progress{};
UEvent m_uevent_write_progress{};
RingBuf<2> read_buffers{};
RingBuf<2> write_buffers{};
@@ -219,8 +237,10 @@ ThreadData::ThreadData(ui::ProgressBox* _pbox, s64 size, const ReadCallback& _rf
condvarInit(std::addressof(can_pull));
condvarInit(std::addressof(can_pull_write));
ueventCreate(&m_uevent_done, false);
ueventCreate(&m_uevent_progres, true);
ueventCreate(GetDoneEvent(), false);
ueventCreate(GetReadProgressEvent(), true);
ueventCreate(GetDecompressProgressEvent(), true);
ueventCreate(GetWriteProgressEvent(), true);
}
auto ThreadData::GetResults() volatile -> Result {
@@ -379,6 +399,7 @@ Result ThreadData::readFuncInternal() {
break;
}
ueventSignal(GetReadProgressEvent());
auto buf_size = bytes_read;
R_TRY(this->SetDecompressBuf(buf, buffer_offset, buf_size));
}
@@ -423,25 +444,17 @@ Result ThreadData::decompressFuncInternal() {
}
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);
// }
this->decompress_offset += rsize;
ueventSignal(GetDecompressProgressEvent());
}
R_SUCCEED();
}));
} else {
this->decompress_offset += buf.size();
ueventSignal(GetDecompressProgressEvent());
R_TRY(this->SetWriteBuf(buf, buf.size()));
}
}
@@ -479,7 +492,7 @@ Result ThreadData::writeFuncInternal() {
}
this->write_offset += size;
ueventSignal(GetProgressEvent());
ueventSignal(GetWriteProgressEvent());
}
log_write("finished write thread success!\n");
@@ -586,7 +599,11 @@ Result TransferInternal(ui::ProgressBox* pbox, s64 size, const ReadCallback& rfu
R_TRY(start_threads());
log_write("[THREAD] started threads\n");
const auto waiter_progress = waiterForUEvent(t_data.GetProgressEvent());
// use the read progress as the write output may be smaller due to compressing
// so read will show a more accurate progress.
// TODO: show progress bar for all 3 threads.
// NOTE: went back to using write progress for now.
const auto waiter_progress = waiterForUEvent(t_data.GetWriteProgressEvent());
const auto waiter_cancel = waiterForUEvent(pbox->GetCancelEvent());
const auto waiter_done = waiterForUEvent(t_data.GetDoneEvent());
@@ -777,9 +794,14 @@ Result TransferUnzipAll(ui::ProgressBox* pbox, void* zfile, fs::Fs* fs, const fs
continue;
}
const auto path_len = std::strlen(path);
if (!path_len) {
continue;
}
pbox->NewTransfer(name);
if (path[std::strlen(path) -1] == '/') {
if (path[path_len -1] == '/') {
Result rc;
if (R_FAILED(rc = fs->CreateDirectoryRecursively(path)) && rc != FsError_PathAlreadyExists) {
log_write("failed to create folder: %s 0x%04X\n", path.s, rc);

View File

@@ -28,6 +28,7 @@ struct ThreadData {
void Clear();
void PushAsync(u64 id);
void PushAsync(const std::span<const NsApplicationRecord> app_ids);
auto GetAsync(u64 app_id) -> ThreadResultData*;
auto Get(u64 app_id, bool* cached = nullptr) -> ThreadResultData*;
@@ -208,6 +209,30 @@ void ThreadData::PushAsync(u64 id) {
}
}
void ThreadData::PushAsync(const std::span<const NsApplicationRecord> app_ids) {
SCOPED_MUTEX(&m_mutex_id);
SCOPED_MUTEX(&m_mutex_result);
bool added_at_least_one = false;
for (auto& record : app_ids) {
const auto id = record.application_id;
const auto it_id = std::ranges::find(m_ids, id);
const auto it_result = std::ranges::find_if(m_result, [id](auto& e){
return id == e->id;
});
if (it_id == m_ids.end() && it_result == m_result.end()) {
m_ids.emplace_back(id);
added_at_least_one = true;
}
}
if (added_at_least_one) {
ueventSignal(&m_uevent);
}
}
auto ThreadData::GetAsync(u64 app_id) -> ThreadResultData* {
SCOPED_MUTEX(&m_mutex_result);
@@ -428,6 +453,13 @@ void PushAsync(u64 app_id) {
}
}
void PushAsync(const std::span<const NsApplicationRecord> app_ids) {
SCOPED_MUTEX(&g_mutex);
if (g_thread_data) {
g_thread_data->PushAsync(app_ids);
}
}
auto GetAsync(u64 app_id) -> ThreadResultData* {
SCOPED_MUTEX(&g_mutex);
if (g_thread_data) {

View File

@@ -51,6 +51,19 @@ auto GetCodeMessage(Result rc) -> const char* {
case Result_FsLoadingCancelled: return "SphairaError_FsLoadingCancelled";
case Result_FsBrokenRoot: return "SphairaError_FsBrokenRoot";
case Result_FsUnknownStdioError: return "SphairaError_FsUnknownStdioError";
case Result_FsStdioFailedToSeek: return "SphairaError_FsStdioFailedToSeek";
case Result_FsStdioFailedToRead: return "SphairaError_FsStdioFailedToRead";
case Result_FsStdioFailedToWrite: return "SphairaError_FsStdioFailedToWrite";
case Result_FsStdioFailedToOpenFile: return "SphairaError_FsStdioFailedToOpenFile";
case Result_FsStdioFailedToCreate: return "SphairaError_FsStdioFailedToCreate";
case Result_FsStdioFailedToTruncate: return "SphairaError_FsStdioFailedToTruncate";
case Result_FsStdioFailedToFlush: return "SphairaError_FsStdioFailedToFlush";
case Result_FsStdioFailedToCreateDirectory: return "SphairaError_FsStdioFailedToCreateDirectory";
case Result_FsStdioFailedToDeleteFile: return "SphairaError_FsStdioFailedToDeleteFile";
case Result_FsStdioFailedToDeleteDirectory: return "SphairaError_FsStdioFailedToDeleteDirectory";
case Result_FsStdioFailedToOpenDirectory: return "SphairaError_FsStdioFailedToOpenDirectory";
case Result_FsStdioFailedToRename: return "SphairaError_FsStdioFailedToRename";
case Result_FsStdioFailedToStat: return "SphairaError_FsStdioFailedToStat";
case Result_FsReadOnly: return "SphairaError_FsReadOnly";
case Result_FsNotActive: return "SphairaError_FsNotActive";
case Result_FsFailedStdioStat: return "SphairaError_FsFailedStdioStat";

View File

@@ -65,11 +65,7 @@ auto List::ScrollDown(s64& index, s64 step, s64 count) -> bool {
return false;
}
if (index + step < count) {
index += step;
} else {
index = count - 1;
}
index = std::min(index + step, count - 1);
if (index != old_index) {
App::PlaySoundEffect(SoundEffect::Scroll);
@@ -103,11 +99,7 @@ auto List::ScrollUp(s64& index, s64 step, s64 count) -> bool {
return false;
}
if (index >= step) {
index -= step;
} else {
index = 0;
}
index = std::max<s64>(0, index - step);
if (index != old_index) {
App::PlaySoundEffect(SoundEffect::Scroll);
@@ -169,20 +161,24 @@ void List::OnUpdateGrid(Controller* controller, TouchInfo* touch, s64 index, s64
const auto page_up_button = GetPageJump() ? (m_row == 1 ? Button::DPAD_LEFT : Button::L2) : (Button::NONE);
const auto page_down_button = GetPageJump() ? (m_row == 1 ? Button::DPAD_RIGHT : Button::R2) : (Button::NONE);
const auto hotkey = m_row == 1 ? controller->GotHeld(Button::R2) : false;
const auto end_page = INT32_MAX;
const auto hot_page = m_page * 4;
if (controller->GotDown(Button::DOWN)) {
if (ScrollDown(index, m_row, count)) {
if (ScrollDown(index, hotkey ? end_page : m_row, count)) {
callback(false, index);
}
} else if (controller->GotDown(Button::UP)) {
if (ScrollUp(index, m_row, count)) {
if (ScrollUp(index, hotkey ? end_page : m_row, count)) {
callback(false, index);
}
} else if (controller->GotDown(page_down_button)) {
if (ScrollDown(index, m_page, count)) {
if (ScrollDown(index, hotkey ? hot_page : m_page, count)) {
callback(false, index);
}
} else if (controller->GotDown(page_up_button)) {
if (ScrollUp(index, m_page, count)) {
if (ScrollUp(index, hotkey ? hot_page : m_page, count)) {
callback(false, index);
}
} else if (m_row > 1 && controller->GotDown(Button::RIGHT)) {

View File

@@ -20,6 +20,8 @@
#include "web.hpp"
#include "minizip_helper.hpp"
#include "utils/utils.hpp"
#include <minIni.h>
#include <string>
#include <cstring>
@@ -597,7 +599,8 @@ EntryMenu::EntryMenu(Entry& entry, const LazyImage& default_icon, Menu& menu)
options->Add<SidebarEntryCallback>("Leave Feedback"_i18n, [this](){
std::string out;
if (R_SUCCEEDED(swkbd::ShowText(out)) && !out.empty()) {
std::string header = "Leave feedback for " + m_entry.title;
if (R_SUCCEEDED(swkbd::ShowText(out, header.c_str())) && !out.empty()) {
const auto post = "name=" "switch_user" "&package=" + m_entry.name + "&message=" + out;
const auto file = BuildFeedbackCachePath(m_entry);
@@ -738,7 +741,7 @@ void EntryMenu::Draw(NVGcontext* vg, Theme* theme) {
text_start_y += text_inc_y;
gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "category: %s"_i18n.c_str(), m_entry.category.c_str());
text_start_y += text_inc_y;
gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "extracted: %.2f MiB"_i18n.c_str(), (double)m_entry.extracted / 1024.0);
gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "extracted: %s"_i18n.c_str(), utils::formatSizeStorage(m_entry.extracted).c_str());
text_start_y += text_inc_y;
gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "app_dls: %s"_i18n.c_str(), AppDlToStr(m_entry.app_dls).c_str());
text_start_y += text_inc_y;
@@ -968,7 +971,7 @@ Menu::Menu(u32 flags) : grid::Menu{"AppStore"_i18n, flags} {
options->Add<SidebarEntryCallback>("Search"_i18n, [this](){
std::string out;
if (R_SUCCEEDED(swkbd::ShowText(out)) && !out.empty()) {
if (R_SUCCEEDED(swkbd::ShowText(out, "Search for app")) && !out.empty()) {
SetSearch(out);
log_write("got %s\n", out.c_str());
}

View File

@@ -10,6 +10,7 @@
#include "ui/error_box.hpp"
#include "ui/music_player.hpp"
#include "utils/utils.hpp"
#include "utils/devoptab.hpp"
#include "log.hpp"
@@ -31,7 +32,6 @@
#include "yati/yati.hpp"
#include "yati/source/file.hpp"
#include <usbdvd.h>
#include <minIni.h>
#include <minizip/zip.h>
#include <minizip/unzip.h>
@@ -45,12 +45,16 @@
#include <utility>
#include <ranges>
#ifdef ENABLE_LIBUSBDVD
#include <usbdvd.h>
#endif // ENABLE_LIBUSBDVD
namespace sphaira::ui::menu::filebrowser {
namespace {
using RomDatabaseIndexs = std::vector<size_t>;
struct ForwarderForm final : public Sidebar {
struct ForwarderForm final : public FormSidebar {
explicit ForwarderForm(const FileAssocEntry& assoc, const RomDatabaseIndexs& db_indexs, const FileEntry& entry, const fs::FsPath& arg_path);
private:
@@ -70,7 +74,7 @@ private:
SidebarEntryFilePicker* m_icon{};
};
constinit UEvent g_change_uevent;
std::atomic_bool g_change_signalled{};
constexpr FsEntry FS_ENTRY_DEFAULT{
"microSD card", "/", FsType::Sd, FsEntryFlag_Assoc | FsEntryFlag_IsSd,
@@ -78,8 +82,7 @@ constexpr FsEntry FS_ENTRY_DEFAULT{
constexpr FsEntry FS_ENTRIES[]{
FS_ENTRY_DEFAULT,
{ "Image System memory", "/", FsType::ImageNand },
{ "Image microSD card", "/", FsType::ImageSd},
{ "Album", "/", FsType::ImageSd},
};
constexpr std::string_view AUDIO_EXTENSIONS[] = {
@@ -115,7 +118,7 @@ constexpr std::string_view ZIP_EXTENSIONS[] = {
};
// supported music playback extensions.
constexpr std::string_view MUSIC_EXTENSIONS[] = {
"bfstm", "bfwav", "wav", "mp3", "ogg", "adf",
"bfstm", "bfwav", "wav", "mp3", "ogg", "flac", "adf",
};
// supported theme music playback extensions.
constexpr std::span THEME_MUSIC_EXTENSIONS = MUSIC_EXTENSIONS;
@@ -286,7 +289,7 @@ auto GetRomIcon(std::string filename, const RomDatabaseIndexs& db_indexs, const
}
ForwarderForm::ForwarderForm(const FileAssocEntry& assoc, const RomDatabaseIndexs& db_indexs, const FileEntry& entry, const fs::FsPath& arg_path)
: Sidebar{"Forwarder Creation", Side::RIGHT}
: FormSidebar{"Forwarder Creation"}
, m_assoc{assoc}
, m_db_indexs{db_indexs}
, m_arg_path{arg_path} {
@@ -312,17 +315,17 @@ ForwarderForm::ForwarderForm(const FileAssocEntry& assoc, const RomDatabaseIndex
const auto icon = m_assoc.path;
m_name = this->Add<SidebarEntryTextInput>(
"Name", name, "", -1, sizeof(NacpLanguageEntry::name) - 1,
"Name", name, "", "", -1, sizeof(NacpLanguageEntry::name) - 1,
"Set the name of the application"_i18n
);
m_author = this->Add<SidebarEntryTextInput>(
"Author", author, "", -1, sizeof(NacpLanguageEntry::author) - 1,
"Author", author, "", "", -1, sizeof(NacpLanguageEntry::author) - 1,
"Set the author of the application"_i18n
);
m_version = this->Add<SidebarEntryTextInput>(
"Version", version, "", -1, sizeof(NacpStruct::display_version) - 1,
"Version", version, "", "", -1, sizeof(NacpStruct::display_version) - 1,
"Set the display version of the application"_i18n
);
@@ -416,7 +419,7 @@ auto IsExtension(std::string_view ext, std::span<const std::string_view> list) -
}
void SignalChange() {
ueventSignal(&g_change_uevent);
g_change_signalled = true;
}
FsView::FsView(Base* menu, const std::shared_ptr<fs::Fs>& fs, const fs::FsPath& path, const FsEntry& entry, ViewSide side) : m_menu{menu}, m_side{side} {
@@ -526,12 +529,31 @@ FsView::~FsView() {
}
void FsView::Update(Controller* controller, TouchInfo* touch) {
m_list->OnUpdate(controller, touch, m_index, m_entries_current.size(), [this](bool touch, auto i) {
m_list->OnUpdate(controller, touch, m_index, m_entries_current.size(), [this, controller](bool touch, auto i) {
if (touch && m_index == i) {
FireAction(Button::A);
} else {
App::PlaySoundEffect(SoundEffect::Focus);
auto old_index = m_index;
SetIndex(i);
const auto new_index = m_index;
// if L2 is helt, select all between old and new index.
if (old_index != new_index && controller->GotHeld(Button::L2)) {
const auto inc = old_index < new_index ? +1 : -1;
while (old_index != new_index) {
old_index += inc;
auto& e = GetEntry(old_index);
e.selected ^= 1;
if (e.selected) {
m_selected_count++;
} else {
m_selected_count--;
}
}
}
}
});
}
@@ -591,8 +613,7 @@ void FsView::Draw(NVGcontext* vg, Theme* theme) {
m_scroll_name.Draw(vg, selected, x + text_xoffset+65, y + (h / 2.f), w-(75+text_xoffset+65+50), 20, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(text_id), e.name);
// NOTE: make this native only if i disable dir scan from above.
if (e.IsDir()) {
if (e.IsDir() && !m_fs_entry.IsNoStatDir() && (e.dir_count != -1 || !e.done_stat)) {
// 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.done_stat && e.file_count == -1 && e.dir_count == -1) {
@@ -602,12 +623,12 @@ void FsView::Draw(NVGcontext* vg, Theme* theme) {
}
if (e.file_count != -1) {
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) - 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_BOTTOM, theme->GetColour(text_id), "%zd files"_i18n.c_str(), e.file_count);
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) - 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_BOTTOM, theme->GetColour(ThemeEntryID_TEXT_INFO), "%zd files"_i18n.c_str(), e.file_count);
}
if (e.dir_count != -1) {
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) + 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_TOP, theme->GetColour(text_id), "%zd dirs"_i18n.c_str(), e.dir_count);
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) + 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT_INFO), "%zd dirs"_i18n.c_str(), e.dir_count);
}
} else if (e.IsFile()) {
} else if (e.IsFile() && !m_fs_entry.IsNoStatFile() && (e.file_size != -1 || !e.time_stamp.is_valid)) {
if (!e.time_stamp.is_valid && !e.done_stat) {
e.done_stat = true;
const auto path = GetNewPath(e);
@@ -621,12 +642,9 @@ void FsView::Draw(NVGcontext* vg, Theme* theme) {
const auto t = (time_t)(e.time_stamp.modified);
struct tm tm{};
localtime_r(&t, &tm);
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) + 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_TOP, theme->GetColour(text_id), "%02u/%02u/%u", tm.tm_mday, tm.tm_mon + 1, tm.tm_year + 1900);
if ((double)e.file_size / 1024.0 / 1024.0 <= 0.009) {
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) - 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_BOTTOM, theme->GetColour(text_id), "%.2f KiB", (double)e.file_size / 1024.0);
} else {
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) - 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_BOTTOM, theme->GetColour(text_id), "%.2f MiB", (double)e.file_size / 1024.0 / 1024.0);
}
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) + 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT_INFO), "%02u/%02u/%u", tm.tm_mday, tm.tm_mon + 1, tm.tm_year + 1900);
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) - 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_BOTTOM, theme->GetColour(ThemeEntryID_TEXT_INFO), "%s", utils::formatSizeStorage(e.file_size).c_str());
}
});
}
@@ -710,20 +728,22 @@ void FsView::OnClick() {
}
});
} else if (IsExtension(entry.GetExtension(), NCA_EXTENSIONS)) {
MountFileFs(devoptab::MountNca, devoptab::UmountNca);
MountFileFs(devoptab::MountNca, devoptab::UmountNeworkDevice);
} else if (IsExtension(entry.GetExtension(), NSP_EXTENSIONS)) {
MountFileFs(devoptab::MountNsp, devoptab::UmountNsp);
MountFileFs(devoptab::MountNsp, devoptab::UmountNeworkDevice);
} else if (IsExtension(entry.GetExtension(), XCI_EXTENSIONS)) {
MountFileFs(devoptab::MountXci, devoptab::UmountXci);
MountFileFs(devoptab::MountXci, devoptab::UmountNeworkDevice);
} else if (IsExtension(entry.GetExtension(), "zip")) {
MountFileFs(devoptab::MountZip, devoptab::UmountZip);
MountFileFs(devoptab::MountZip, devoptab::UmountNeworkDevice);
} else if (IsExtension(entry.GetExtension(), "bfsar")) {
MountFileFs(devoptab::MountBfsar, devoptab::UmountBfsar);
MountFileFs(devoptab::MountBfsar, devoptab::UmountNeworkDevice);
} 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)) {
}
#ifdef ENABLE_LIBUSBDVD
else if (IsExtension(entry.GetExtension(), CDDVD_EXTENSIONS)) {
std::shared_ptr<CUSBDVD> usbdvd;
if (entry.GetExtension() == "cue") {
@@ -747,7 +767,9 @@ void FsView::OnClick() {
} else {
log_write("[USBDVD] failed to mount\n");
}
} else if (IsExtension(entry.GetExtension(), INSTALL_EXTENSIONS)) {
}
#endif // ENABLE_LIBUSBDVD
else if (IsExtension(entry.GetExtension(), INSTALL_EXTENSIONS)) {
InstallFiles();
} else if (IsSd()) {
const auto assoc_list = m_menu->FindFileAssocFor();
@@ -946,7 +968,7 @@ void FsView::ZipFiles(fs::FsPath zip_out) {
auto zfile = zipOpen2_64(zip_out, APPEND_STATUS_CREATE, nullptr, &file_func);
R_UNLESS(zfile, Result_ZipOpen2_64);
ON_SCOPE_EXIT(zipClose(zfile, "sphaira v" APP_VERSION_HASH));
ON_SCOPE_EXIT(zipClose(zfile, "sphaira v" APP_DISPLAY_VERSION));
const auto zip_add = [&](const fs::FsPath& file_path) -> Result {
// the file name needs to be relative to the current directory.
@@ -1004,114 +1026,6 @@ void FsView::ZipFiles(fs::FsPath zip_out) {
});
}
void FsView::UploadFiles() {
const auto targets = GetSelectedEntries();
const auto network_locations = location::Load();
if (network_locations.empty()) {
App::Notify("No upload locations set!"_i18n);
return;
}
PopupList::Items items;
for (const auto&p : network_locations) {
items.emplace_back(p.name);
}
App::Push<PopupList>(
"Select upload location"_i18n, items, [this, network_locations](auto op_index){
if (!op_index) {
return;
}
const auto loc = network_locations[*op_index];
App::Push<ProgressBox>(0, "Uploading"_i18n, "", [this, loc](auto pbox) -> Result {
auto targets = GetSelectedEntries();
const auto is_file_based_emummc = App::IsFileBaseEmummc();
const auto file_add = [&](s64 file_size, const fs::FsPath& file_path, const char* name) -> Result {
// the file name needs to be relative to the current directory.
const auto relative_file_name = file_path.s + std::strlen(m_path);
pbox->SetTitle(name);
pbox->NewTransfer(relative_file_name);
fs::File f;
R_TRY(m_fs->OpenFile(file_path, FsOpenMode_Read, &f));
return thread::TransferPull(pbox, file_size,
[&](void* data, s64 off, s64 size, u64* bytes_read) -> Result {
const auto rc = f.Read(off, data, size, FsReadOption_None, bytes_read);
if (m_fs->IsNative() && is_file_based_emummc) {
svcSleepThread(2e+6); // 2ms
}
return rc;
},
[&](thread::PullCallback pull) -> Result {
s64 offset{};
const auto result = curl::Api().FromMemory(
CURL_LOCATION_TO_API(loc),
curl::OnProgress{pbox->OnDownloadProgressCallback()},
curl::UploadInfo{
relative_file_name, file_size,
[&](void *ptr, size_t size) -> size_t {
// curl will request past the size of the file, causing an error.
if (offset >= file_size) {
log_write("finished file upload\n");
return 0;
}
u64 bytes_read{};
if (R_FAILED(pull(ptr, size, &bytes_read))) {
log_write("failed to read in custom callback: %zd size: %zd\n", offset, size);
return 0;
}
offset += bytes_read;
return bytes_read;
}
}
);
R_UNLESS(result.success, Result_FileBrowserFailedUpload);
R_SUCCEED();
}
);
};
for (auto& e : targets) {
if (e.IsFile()) {
const auto file_path = GetNewPath(e);
R_TRY(file_add(e.file_size, file_path, e.GetName().c_str()));
} else {
FsDirCollections collections;
get_collections(GetNewPath(e), e.name, collections, true);
for (const auto& collection : collections) {
for (const auto& file : collection.files) {
const auto file_path = fs::AppendPath(collection.path, file.name);
R_TRY(file_add(file.file_size, file_path, file.name));
}
}
}
}
R_SUCCEED();
}, [this](Result rc){
App::PushErrorBox(rc, "Failed to, TODO: add message here"_i18n);
m_menu->ResetSelection();
if (R_SUCCEEDED(rc)) {
App::Notify("Upload successfull!"_i18n);
log_write("Upload successfull!!!\n");
} else {
App::Notify("Upload failed!"_i18n);
log_write("Upload failed!!!\n");
}
});
}
);
}
auto FsView::Scan(fs::FsPath new_path, bool is_walk_up) -> Result {
App::SetBoostMode(true);
ON_SCOPE_EXIT(App::SetBoostMode(false));
@@ -1127,6 +1041,7 @@ auto FsView::Scan(fs::FsPath new_path, bool is_walk_up) -> Result {
m_previous_highlighted_file.emplace_back(f);
}
g_change_signalled = false;
m_path = new_path;
m_entries.clear();
m_entries_index.clear();
@@ -1582,7 +1497,7 @@ auto FsView::get_collections(fs::Fs* fs, const fs::FsPath& path, const fs::FsPat
}
auto FsView::get_collection(const fs::FsPath& path, const fs::FsPath& parent_name, FsDirCollection& out, bool inc_file, bool inc_dir, bool inc_size) -> Result {
return get_collection(m_fs.get(), path, parent_name, out, true, true, inc_size);
return get_collection(m_fs.get(), path, parent_name, out, inc_file, inc_dir, inc_size);
}
auto FsView::get_collections(const fs::FsPath& path, const fs::FsPath& parent_name, FsDirCollections& out, bool inc_size) -> Result {
@@ -1702,17 +1617,6 @@ void FsView::DisplayOptions() {
SidebarEntryArray::Items mount_items;
std::vector<FsEntry> fs_entries;
const auto stdio_locations = location::GetStdio(false);
for (const auto& e: stdio_locations) {
u32 flags{};
if (e.write_protect) {
flags |= FsEntryFlag_ReadOnly;
}
fs_entries.emplace_back(e.name, e.mount, FsType::Stdio, flags);
mount_items.push_back(e.name);
}
for (const auto& e: FS_ENTRIES) {
fs_entries.emplace_back(e);
mount_items.push_back(i18n::get(e.name));
@@ -1723,14 +1627,13 @@ void FsView::DisplayOptions() {
mount_items.push_back(m_menu->m_custom_fs_entry.name);
}
const auto fat_entries = location::GetFat();
for (const auto& e: fat_entries) {
u32 flags{};
if (e.write_protect) {
flags |= FsEntryFlag_ReadOnly;
const auto stdio_locations = location::GetStdio(false);
for (const auto& e: stdio_locations) {
if (e.fs_hidden) {
continue;
}
fs_entries.emplace_back(e.name, e.mount, FsType::Stdio, flags);
fs_entries.emplace_back(e.name, e.mount, FsType::Stdio, e.flags);
mount_items.push_back(e.name);
}
@@ -1808,7 +1711,8 @@ void FsView::DisplayOptions() {
std::string out;
const auto& entry = GetEntry();
const auto name = entry.GetName();
if (R_SUCCEEDED(swkbd::ShowText(out, "Set New File Name"_i18n.c_str(), name.c_str())) && !out.empty() && out != name) {
const auto header = "Set new name"_i18n;
if (R_SUCCEEDED(swkbd::ShowText(out, header.c_str(), header.c_str(), name.c_str())) && !out.empty() && out != name) {
App::PopToMenu();
const auto src_path = GetNewPath(entry);
@@ -1901,7 +1805,7 @@ void FsView::DisplayOptions() {
options->Add<SidebarEntryCallback>("Extract to..."_i18n, [this](){
std::string out;
if (R_SUCCEEDED(swkbd::ShowText(out, "Enter the path to the folder to extract into", fs::AppendPath(m_path, ""))) && !out.empty()) {
if (R_SUCCEEDED(swkbd::ShowText(out, "Extract path", "Enter the path to the folder to extract into", fs::AppendPath(m_path, ""))) && !out.empty()) {
UnzipFiles(out);
}
});
@@ -1919,7 +1823,7 @@ void FsView::DisplayOptions() {
options->Add<SidebarEntryCallback>("Compress to..."_i18n, [this](){
std::string out;
if (R_SUCCEEDED(swkbd::ShowText(out, "Enter the path to the folder to extract into", m_path)) && !out.empty()) {
if (R_SUCCEEDED(swkbd::ShowText(out, "Compress path", "Enter the path to the folder to compress into", m_path)) && !out.empty()) {
ZipFiles(out);
}
});
@@ -1939,7 +1843,8 @@ void FsView::DisplayAdvancedOptions() {
if (!m_fs_entry.IsReadOnly()) {
options->Add<SidebarEntryCallback>("Create File"_i18n, [this](){
std::string out;
if (R_SUCCEEDED(swkbd::ShowText(out, "Set File Name"_i18n.c_str(), fs::AppendPath(m_path, ""))) && !out.empty()) {
const auto header = "Set File Name"_i18n;
if (R_SUCCEEDED(swkbd::ShowText(out, header.c_str(), header.c_str(), fs::AppendPath(m_path, ""))) && !out.empty()) {
App::PopToMenu();
fs::FsPath full_path;
@@ -1961,7 +1866,8 @@ void FsView::DisplayAdvancedOptions() {
options->Add<SidebarEntryCallback>("Create Folder"_i18n, [this](){
std::string out;
if (R_SUCCEEDED(swkbd::ShowText(out, "Set Folder Name"_i18n.c_str(), fs::AppendPath(m_path, ""))) && !out.empty()) {
const auto header = "Set Folder Name"_i18n;
if (R_SUCCEEDED(swkbd::ShowText(out, header.c_str(), header.c_str(), fs::AppendPath(m_path, ""))) && !out.empty()) {
App::PopToMenu();
fs::FsPath full_path;
@@ -1987,12 +1893,6 @@ void FsView::DisplayAdvancedOptions() {
});
}
if (m_entries_current.size() && (m_menu->m_options & FsOption_CanUpload)) {
options->Add<SidebarEntryCallback>("Upload"_i18n, [this](){
UploadFiles();
});
}
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());
@@ -2020,12 +1920,6 @@ void FsView::DisplayAdvancedOptions() {
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);
});
});
}
@@ -2062,7 +1956,8 @@ Base::Base(const std::shared_ptr<fs::Fs>& fs, const FsEntry& fs_entry, const fs:
}
void Base::Update(Controller* controller, TouchInfo* touch) {
if (R_SUCCEEDED(waitSingle(waiterForUEvent(&g_change_uevent), 0))) {
if (g_change_signalled.exchange(false)) {
if (IsSplitScreen()) {
view_left->SortAndFindLastFile(true);
view_right->SortAndFindLastFile(true);
@@ -2237,8 +2132,16 @@ void Base::LoadAssocEntriesPath(const fs::FsPath& path) {
if (!assoc.path.empty()) {
file_exists = view->m_fs->FileExists(assoc.path);
} else {
auto nros = homebrew::GetNroEntries();
if (nros.empty()) {
if (m_nro_entries.empty()) {
nro_scan("/switch", m_nro_entries);
nros = m_nro_entries;
}
}
const auto nro_name = assoc.name + ".nro";
for (const auto& nro : homebrew::GetNroEntries()) {
for (const auto& nro : nros) {
const auto len = std::strlen(nro.path);
if (len < nro_name.length()) {
continue;
@@ -2403,7 +2306,6 @@ void Base::Init(const std::shared_ptr<fs::Fs>& fs, const FsEntry& fs_entry, cons
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) {

View File

@@ -36,6 +36,8 @@
namespace sphaira::ui::menu::game {
namespace {
std::atomic_bool g_change_signalled{};
struct NspSource final : dump::BaseSource {
NspSource(const std::vector<NspEntry>& entries) : m_entries{entries} {
m_is_file_based_emummc = App::IsFileBaseEmummc();
@@ -110,6 +112,7 @@ private:
bool m_is_file_based_emummc{};
};
#ifdef ENABLE_NSZ
Result NszExport(ProgressBox* pbox, const keys::Keys& keys, dump::BaseSource* _source, dump::WriteSource* writer, const fs::FsPath& path) {
auto source = (NspSource*)_source;
@@ -145,6 +148,7 @@ Result NszExport(ProgressBox* pbox, const keys::Keys& keys, dump::BaseSource* _s
R_SUCCEED();
}
#endif // ENABLE_NSZ
Result Notify(Result rc, const std::string& error_message) {
if (R_FAILED(rc)) {
@@ -230,6 +234,12 @@ Result CreateSave(u64 app_id, AccountUid uid) {
} // namespace
Result NspEntry::Read(void* buf, s64 off, s64 size, u64* bytes_read) {
if (off == nsp_size) {
log_write("[NspEntry::Read] read at eof...\n");
*bytes_read = 0;
R_SUCCEED();
}
if (off < nsp_data.size()) {
*bytes_read = size = ClipSize(off, size, nsp_data.size());
std::memcpy(buf, nsp_data.data() + off, size);
@@ -268,6 +278,10 @@ Result NspEntry::Read(void* buf, s64 off, s64 size, u64* bytes_read) {
return 0x1;
}
void SignalChange() {
g_change_signalled = true;
}
Menu::Menu(u32 flags) : grid::Menu{"Games"_i18n, flags} {
this->SetActions(
std::make_pair(Button::L3, Action{[this](){
@@ -467,8 +481,13 @@ Menu::~Menu() {
}
void Menu::Update(Controller* controller, TouchInfo* touch) {
// force update if gamecard state changed.
m_dirty |= R_SUCCEEDED(eventWait(&m_gc_event, 0));
if (g_change_signalled.exchange(false)) {
m_dirty = true;
}
if (R_SUCCEEDED(eventWait(&m_gc_event, 0))) {
m_dirty = true;
}
if (m_dirty) {
App::Notify("Updating application record list"_i18n);
@@ -558,6 +577,7 @@ void Menu::ScanHomebrew() {
FreeEntries();
m_entries.reserve(ENTRY_CHUNK_COUNT);
g_change_signalled = false;
std::vector<NsApplicationRecord> record_list(ENTRY_CHUNK_COUNT);
s32 offset{};
@@ -704,7 +724,6 @@ void Menu::ExportOptions(bool to_nsz) {
void Menu::DumpGames(u32 flags, bool to_nsz) {
auto targets = GetSelectedEntries();
ClearSelection();
std::vector<NspEntry> nsp_entries;
for (auto& e : targets) {
@@ -979,6 +998,7 @@ void DumpNsp(const std::vector<NspEntry>& entries, bool to_nsz) {
auto source = std::make_shared<NspSource>(entries);
if (to_nsz) {
#ifdef ENABLE_NSZ
// todo: log keys error.
keys::Keys keys;
keys::parse_keys(keys, true);
@@ -986,6 +1006,7 @@ void DumpNsp(const std::vector<NspEntry>& entries, bool to_nsz) {
dump::Dump(source, paths, [keys](ProgressBox* pbox, dump::BaseSource* source, dump::WriteSource* writer, const fs::FsPath& path) {
return NszExport(pbox, keys, source, writer, path);
});
#endif // ENABLE_NSZ
} else {
dump::Dump(source, paths);
}

View File

@@ -10,6 +10,8 @@
#include "yati/nx/ncm.hpp"
#include "yati/nx/es.hpp"
#include "utils/utils.hpp"
#include "title_info.hpp"
#include "app.hpp"
#include "defines.hpp"
@@ -231,14 +233,8 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
GetNcmSizeOfMetaStatus(e);
}
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) + 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_TOP, theme->GetColour(text_id), "%s", ncm::GetReadableStorageIdStr(e.status.storageID));
if ((double)e.size / 1024.0 / 1024.0 <= 0.009) {
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) - 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_BOTTOM, theme->GetColour(text_id), "%.2f KiB", (double)e.size / 1024.0);
} else if ((double)e.size / 1024.0 / 1024.0 / 1024.0 <= 0.009) {
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) - 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_BOTTOM, theme->GetColour(text_id), "%.2f MiB", (double)e.size / 1024.0 / 1024.0);
} else {
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) - 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_BOTTOM, theme->GetColour(text_id), "%.2f GiB", (double)e.size / 1024.0 / 1024.0 / 1024.0);
}
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) + 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT_INFO), "%s", ncm::GetReadableStorageIdStr(e.status.storageID));
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) - 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_BOTTOM, theme->GetColour(ThemeEntryID_TEXT_INFO), "%s", utils::formatSizeStorage(e.size).c_str());
if (e.selected) {
gfx::drawText(vg, x + text_xoffset - 80 / 2, y + (h / 2.f) - (24.f / 2), 24.f, "\uE14B", nullptr, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT_SELECTED));

View File

@@ -344,15 +344,8 @@ 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", 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);
} else if ((double)e.size / 1024.0 / 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 MiB", (double)e.size / 1024.0 / 1024.0);
} else {
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f), 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_MIDDLE, theme->GetColour(text_id), "%.2f GiB", (double)e.size / 1024.0 / 1024.0 / 1024.0);
}
gfx::drawTextArgs(vg, x + text_xoffset + 150, y + (h / 2.f), 20.f, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(text_id), "%s", utils::hexIdToStr(e.content_id).str);
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f), 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "%s", utils::formatSizeStorage(e.size).c_str());
if (e.missing) {
gfx::drawText(vg, x + text_xoffset - 80 / 2, y + (h / 2.f) - (24.f / 2), 24.f, "\uE140", nullptr, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_ERROR));
@@ -413,7 +406,7 @@ Result Menu::MountNcaFs() {
R_TRY(devoptab::MountNcaNcm(m_meta.cs, &e.content_id, root));
auto fs = std::make_shared<filebrowser::FsStdioWrapper>(root, [root](){
devoptab::UmountNca(root);
devoptab::UmountNeworkDevice(root);
});
filebrowser::MountFsHelper(fs, utils::hexIdToStr(e.content_id).str);

View File

@@ -235,6 +235,12 @@ struct XciSource final : dump::BaseSource {
int icon{};
Result Read(const std::string& path, void* buf, s64 off, s64 size, u64* bytes_read) override {
if (off == xci_size) {
log_write("[XciSource::Read] read at eof...\n");
*bytes_read = 0;
R_SUCCEED();
}
if (path.ends_with(GetDumpTypeStr(DumpFileType_XCI)) || path.ends_with(GetDumpTypeStr(DumpFileType_XCZ))) {
size = ClipSize(off, size, xci_size);
*bytes_read = size;
@@ -323,6 +329,7 @@ private:
const s64 m_offset;
};
#ifdef ENABLE_NSZ
Result NszExport(ProgressBox* pbox, const keys::Keys& keys, dump::BaseSource* _source, dump::WriteSource* writer, const fs::FsPath& path) {
auto source = (XciSource*)_source;
@@ -464,6 +471,7 @@ Result NszExport(ProgressBox* pbox, const keys::Keys& keys, dump::BaseSource* _s
R_SUCCEED();
}
#endif // ENABLE_NSZ
struct GcSource final : yati::source::Base {
GcSource(const ApplicationEntry& entry, fs::FsNativeGameCard* fs);
@@ -617,7 +625,9 @@ Menu::Menu(u32 flags) : MenuBase{"GameCard"_i18n, flags} {
add("Export Certificate"_i18n, DumpFileFlag_Cert);
add("Export Initial Data"_i18n, DumpFileFlag_Initial);
} else if (m_option_index == 2) {
#ifdef ENABLE_NSZ
DumpXcz(0);
#endif // ENABLE_NSZ
} else if (m_option_index == 3) {
const auto rc = MountGcFs();
App::PushErrorBox(rc, "Failed to mount GameCard filesystem"_i18n);
@@ -729,6 +739,11 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
if (!m_mounted) {
colour = ThemeEntryID_TEXT_INFO;
}
if (i == 2) {
#ifndef ENABLE_NSZ
colour = ThemeEntryID_TEXT_INFO;
#endif // ENABLE_NSZ
}
gfx::drawTextArgs(vg, x + 15, y + (h / 2.f), 23.f, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(colour), "%s", i18n::get(g_option_list[i]).c_str());
});
@@ -1099,6 +1114,7 @@ void Menu::OnChangeIndex(s64 new_index) {
}
}
#ifdef ENABLE_NSZ
Result Menu::DumpXcz(u32 flags) {
R_TRY(GcMountStorage());
@@ -1122,6 +1138,7 @@ Result Menu::DumpXcz(u32 flags) {
R_SUCCEED();
}
#endif // ENABLE_NSZ
Result Menu::DumpGames(u32 flags) {
// first, try and mount the storage.
@@ -1183,18 +1200,8 @@ Result Menu::DumpGames(u32 flags) {
paths.emplace_back(BuildFullDumpPath(DumpFileType_Initial, m_entries));
}
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);
}
dump::Dump(source, paths, nullptr, location_flags);
R_SUCCEED();
};
@@ -1322,7 +1329,7 @@ Result Menu::MountGcFs() {
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);
devoptab::UmountNeworkDevice(root);
});
filebrowser::MountFsHelper(fs, e.lang_entry.name);

View File

@@ -233,7 +233,7 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
nvgRestore(vg);
if (!e.tag.empty()) {
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f), 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_MIDDLE, theme->GetColour(text_id), "version: %s", e.tag.c_str());
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f), 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "version: %s", e.tag.c_str());
}
});
}

View File

@@ -2,6 +2,8 @@
#include "ui/menus/grid_menu_base.hpp"
#include "ui/nvg_util.hpp"
#include <cmath>
namespace sphaira::ui::menu::grid {
void Menu::DrawEntry(NVGcontext* vg, Theme* theme, int layout, const Vec4& v, bool selected, int image, const char* name, const char* author, const char* version) {
@@ -16,8 +18,9 @@ Vec4 Menu::DrawEntry(NVGcontext* vg, Theme* theme, bool draw_image, int layout,
const auto& [x, y, w, h] = v;
auto text_id = ThemeEntryID_TEXT;
auto info_id = ThemeEntryID_TEXT_INFO;
if (selected) {
text_id = ThemeEntryID_TEXT_SELECTED;
text_id = info_id = ThemeEntryID_TEXT_SELECTED;
gfx::drawRectOutline(vg, theme, 4.f, v);
} else {
DrawElement(v, ThemeEntryID_GRID);
@@ -36,8 +39,8 @@ Vec4 Menu::DrawEntry(NVGcontext* vg, Theme* theme, bool draw_image, int layout,
const auto text_clip_w = w - 30.f - text_off;
const float font_size = 18;
m_scroll_name.Draw(vg, selected, text_x, y + 45, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), name);
m_scroll_author.Draw(vg, selected, text_x, y + 80, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), author);
m_scroll_version.Draw(vg, selected, text_x, y + 115, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), version);
m_scroll_author.Draw(vg, selected, text_x, y + 80, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(info_id), author);
m_scroll_version.Draw(vg, selected, text_x, y + 115, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(info_id), version);
} else {
if (selected) {
gfx::drawAppLable(vg, theme, m_scroll_name, x, y, w, name);
@@ -45,7 +48,19 @@ Vec4 Menu::DrawEntry(NVGcontext* vg, Theme* theme, bool draw_image, int layout,
}
if (draw_image) {
gfx::drawImage(vg, image_v, image ?: App::GetDefaultImage(), 5);
if (image > 0) {
gfx::drawImage(vg, image_v, image, 5);
} else {
// https://www.mathopenref.com/arcradius.html
auto spinner = image_v;
spinner.w /= 2;
spinner.h /= 2;
spinner.x += (image_v.w / 2);
spinner.y += (image_v.h / 2);
const auto rad = (spinner.h / 2) + (std::powf(spinner.w, 2) / (spinner.h * 8));
gfx::drawSpinner(vg, theme, spinner.x, spinner.y, rad, armTicksToNs(armGetSystemTick()) / 1e+9);
}
}
return image_v;

View File

@@ -27,7 +27,7 @@ namespace sphaira::ui::menu::homebrew {
namespace {
Menu* g_menu{};
constinit UEvent g_change_uevent;
std::atomic_bool g_change_signalled{};
auto GenerateStarPath(const fs::FsPath& nro_path) -> fs::FsPath {
fs::FsPath out{};
@@ -44,7 +44,7 @@ void FreeEntry(NVGcontext* vg, NroEntry& e) {
} // namespace
void SignalChange() {
ueventSignal(&g_change_uevent);
g_change_signalled = true;
}
auto GetNroEntries() -> std::span<const NroEntry> {
@@ -55,7 +55,7 @@ auto GetNroEntries() -> std::span<const NroEntry> {
return g_menu->GetHomebrewList();
}
Menu::Menu() : grid::Menu{"Homebrew"_i18n, MenuFlag_Tab} {
Menu::Menu(u32 flags) : grid::Menu{"Homebrew"_i18n, flags} {
g_menu = this;
this->SetActions(
@@ -68,7 +68,6 @@ Menu::Menu() : grid::Menu{"Homebrew"_i18n, MenuFlag_Tab} {
);
OnLayoutChange();
ueventCreate(&g_change_uevent, true);
}
Menu::~Menu() {
@@ -77,7 +76,7 @@ Menu::~Menu() {
}
void Menu::Update(Controller* controller, TouchInfo* touch) {
if (R_SUCCEEDED(waitSingle(waiterForUEvent(&g_change_uevent), 0))) {
if (g_change_signalled.exchange(false)) {
m_dirty = true;
}
@@ -86,7 +85,7 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
}
MenuBase::Update(controller, touch);
m_list->OnUpdate(controller, touch, m_index, m_entries.size(), [this](bool touch, auto i) {
m_list->OnUpdate(controller, touch, m_index, m_entries_current.size(), [this](bool touch, auto i) {
if (touch && m_index == i) {
FireAction(Button::A);
} else {
@@ -198,7 +197,9 @@ void Menu::InstallHomebrew() {
}
void Menu::ScanHomebrew() {
g_change_signalled = false;
FreeEntries();
{
SCOPED_TIMESTAMP("nro scan");
nro_scan("/switch", m_entries);
@@ -355,7 +356,7 @@ void Menu::Sort() {
m_entries_current = m_entries_index[Filter_HideHidden];
}
std::sort(m_entries_current.begin(), m_entries_current.end(), sorter);
std::ranges::sort(m_entries_current, sorter);
}
void Menu::SortAndFindLastFile(bool scan) {
@@ -397,6 +398,7 @@ void Menu::FreeEntries() {
}
m_entries.clear();
m_entries_current = {};
for (auto& e : m_entries_index) {
e.clear();
}
@@ -527,7 +529,7 @@ Result Menu::MountNroFs() {
R_TRY(devoptab::MountNro(App::GetApp()->m_fs.get(), e.path, root));
auto fs = std::make_shared<filebrowser::FsStdioWrapper>(root, [root](){
devoptab::UmountNro(root);
devoptab::UmountNeworkDevice(root);
});
filebrowser::MountFsHelper(fs, root);

View File

@@ -16,34 +16,26 @@ enum class InstallState {
Finished,
};
constexpr u64 MAX_BUFFER_SIZE = 1024ULL*1024ULL*8ULL;
constexpr u64 MAX_BUFFER_RESERVE_SIZE = 1024ULL*1024ULL*32ULL;
constexpr u64 MAX_BUFFER_SIZE = 1024ULL*1024ULL*1ULL;
std::atomic<InstallState> INSTALL_STATE{InstallState::None};
// don't use condivar here as windows mtp is very broken.
// stalling for too longer (3s+) and having too varied transfer speeds
// results in windows stalling the transfer for 1m until it kills it via timeout.
// the workaround is to always accept new data, but stall for 1s.
// UPDATE: it seems possible to trigger this bug during normal file transfer
// including using stock haze.
// it seems random, and ive been unable to trigger it personally.
// for this reason, use condivar rather than trying to work around the issue.
#define USE_CONDI_VAR 1
} // namespace
Stream::Stream(const fs::FsPath& path, std::stop_token token) {
m_path = path;
m_token = token;
m_active = true;
m_buffer.reserve(MAX_BUFFER_RESERVE_SIZE);
m_buffer.reserve(MAX_BUFFER_SIZE);
mutexInit(&m_mutex);
condvarInit(&m_can_read);
condvarInit(&m_can_write);
}
Result Stream::ReadChunk(void* buf, s64 size, u64* bytes_read) {
Result Stream::ReadChunk(void* _buf, s64 size, u64* bytes_read) {
auto buf = static_cast<u8*>(_buf);
*bytes_read = 0;
log_write("[Stream::ReadChunk] inside\n");
ON_SCOPE_EXIT(
log_write("[Stream::ReadChunk] exiting\n");
@@ -59,18 +51,30 @@ Result Stream::ReadChunk(void* buf, s64 size, u64* bytes_read) {
break;
}
size = std::min<s64>(size, m_buffer.size());
std::memcpy(buf, m_buffer.data(), size);
m_buffer.erase(m_buffer.begin(), m_buffer.begin() + size);
*bytes_read = size;
return condvarWakeOne(&m_can_write);
const auto rsize = std::min<s64>(size, m_buffer.size());
std::memcpy(buf, m_buffer.data(), rsize);
m_buffer.erase(m_buffer.begin(), m_buffer.begin() + rsize);
condvarWakeOne(&m_can_write);
size -= rsize;
buf += rsize;
*bytes_read += rsize;
if (!size) {
R_SUCCEED();
}
}
log_write("[Stream::ReadChunk] failed to read\n");
R_THROW(Result_TransferCancelled);
}
bool Stream::Push(const void* buf, s64 size) {
bool Stream::Push(const void* _buf, s64 size) {
auto buf = static_cast<const u8*>(_buf);
if (!size) {
return true;
}
log_write("[Stream::Push] inside\n");
ON_SCOPE_EXIT(
log_write("[Stream::Push] exiting\n");
@@ -83,31 +87,27 @@ bool Stream::Push(const void* buf, s64 size) {
}
SCOPED_MUTEX(&m_mutex);
#if USE_CONDI_VAR
if (m_active && m_buffer.size() >= MAX_BUFFER_SIZE) {
R_TRY(condvarWait(std::addressof(m_can_write), std::addressof(m_mutex)));
}
#else
if (m_active && m_buffer.size() >= MAX_BUFFER_SIZE) {
// unlock the mutex and wait for 1s to bring transfer speed down to 1MiB/s.
log_write("[Stream::Push] buffer is full, delaying\n");
mutexUnlock(&m_mutex);
ON_SCOPE_EXIT(mutexLock(&m_mutex));
svcSleepThread(1e+9);
}
#endif
if (!m_active) {
log_write("[Stream::Push] file not active\n");
break;
}
const auto wsize = std::min<s64>(size, MAX_BUFFER_SIZE - m_buffer.size());
const auto offset = m_buffer.size();
m_buffer.resize(offset + size);
std::memcpy(m_buffer.data() + offset, buf, size);
m_buffer.resize(offset + wsize);
std::memcpy(m_buffer.data() + offset, buf, wsize);
condvarWakeOne(&m_can_read);
return true;
size -= wsize;
buf += wsize;
if (!size) {
return true;
}
}
log_write("[Stream::Push] failed to push\n");

View File

@@ -49,55 +49,57 @@ auto MiscMenuFuncGenerator(u32 flags) {
}
const MiscMenuEntry MISC_MENU_ENTRIES[] = {
{ .name = "Homebrew", .title = "Homebrew", .func = MiscMenuFuncGenerator<ui::menu::homebrew::Menu>, .flag = MiscMenuFlag_Shortcut, .info =
"The homebrew menu.\n\n"
"Allows you to launch, delete and mount homebrew!"},
{ .name = "Appstore", .title = "Appstore", .func = MiscMenuFuncGenerator<ui::menu::appstore::Menu>, .flag = MiscMenuFlag_Shortcut, .info =
"Download and update apps.\n\n"\
"Download and update apps.\n\n"
"Internet connection required." },
{ .name = "Games", .title = "Games", .func = MiscMenuFuncGenerator<ui::menu::game::Menu>, .flag = MiscMenuFlag_Shortcut, .info =
"View all installed games. "\
"View all installed games. "
"In this menu you can launch, backup, create savedata and much more." },
{ .name = "FileBrowser", .title = "FileBrowser", .func = MiscMenuFuncGenerator<ui::menu::filebrowser::Menu>, .flag = MiscMenuFlag_Shortcut, .info =
"Browse files on you SD Card. "\
"You can move, copy, delete, extract zip, create zip, upload and much more.\n\n"\
"Browse files on you SD Card. "
"You can move, copy, delete, extract zip, create zip, upload and much more.\n\n"
"A connected USB/HDD can be opened by mounting it in the advanced options." },
{ .name = "Saves", .title = "Saves", .func = MiscMenuFuncGenerator<ui::menu::save::Menu>, .flag = MiscMenuFlag_Shortcut, .info =
"View save data for each user. "\
"You can backup and restore saves.\n\n"\
"View save data for each user. "
"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"\
"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. "\
"Download releases directly from GitHub. "
"Custom entries can be added to /config/sphaira/github" },
#ifdef ENABLE_FTPSRV
{ .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. "\
"If you encounter an issue, do not open an issue, it will not be fixed." },
"Install apps via FTP." },
#endif // ENABLE_FTPSRV
#ifdef ENABLE_LIBHAZE
{ .name = "MTP", .title = "MTP Install", .func = MiscMenuFuncGenerator<ui::menu::mtp::Menu>, .flag = MiscMenuFlag_Install, .info =
"Install apps via MTP.\n\n"\
"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." },
"Install apps via MTP." },
#endif // ENABLE_LIBHAZE
{ .name = "USB", .title = "USB Install", .func = MiscMenuFuncGenerator<ui::menu::usb::Menu>, .flag = MiscMenuFlag_Install, .info =
"Install apps via USB.\n\n"\
"A USB client is required on PC, such as ns-usbloader and fluffy.\n\n"\
"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." },
"Install apps via USB.\n\n"
"A USB client is required on PC." },
{ .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. "\
"To swap GC's, simply remove the old GC and insert the new one. "\
"View info on the inserted Game Card (GC). "
"You can backup and install the inserted GC. "
"To swap GC's, simply remove the old GC and insert the new one. "
"You do not need to exit the menu." },
{ .name = "IRS", .title = "IRS (Infrared Joycon Camera)", .func = MiscMenuFuncGenerator<ui::menu::irs::Menu>, .flag = MiscMenuFlag_Shortcut, .info =
@@ -165,10 +167,35 @@ auto InstallUpdate(ProgressBox* pbox, const std::string url, const std::string v
R_SUCCEED();
}
auto CreateLeftSideMenu(std::string& name_out) -> std::unique_ptr<MenuBase> {
auto CreateCenterMenu(std::string& name_out) -> std::unique_ptr<MenuBase> {
const auto name = App::GetApp()->m_center_menu.Get();
for (auto& e : GetMenuMenuEntries()) {
if (e.name == name) {
name_out = name;
return e.func(MenuFlag_Tab);
}
}
name_out = "Homebrew";
return std::make_unique<ui::menu::homebrew::Menu>(MenuFlag_Tab);
}
auto CreateLeftSideMenu(std::string_view center_name, std::string& name_out) -> std::unique_ptr<MenuBase> {
const auto name = App::GetApp()->m_left_menu.Get();
for (auto& e : GetMiscMenuEntries()) {
// handle if the user tries to mount the same menu twice.
if (name == center_name) {
// check if we can mount the default.
if (center_name != "FileBrowser") {
return std::make_unique<ui::menu::filebrowser::Menu>(MenuFlag_Tab);
} else {
// otherwise, fallback to center default.
return std::make_unique<ui::menu::homebrew::Menu>(MenuFlag_Tab);
}
}
for (auto& e : GetMenuMenuEntries()) {
if (e.name == name) {
name_out = name;
return e.func(MenuFlag_Tab);
@@ -179,6 +206,7 @@ auto CreateLeftSideMenu(std::string& name_out) -> std::unique_ptr<MenuBase> {
return std::make_unique<ui::menu::filebrowser::Menu>(MenuFlag_Tab);
}
// todo: handle center / left menu being the same.
auto CreateRightSideMenu(std::string_view left_name) -> std::unique_ptr<MenuBase> {
const auto name = App::GetApp()->m_right_menu.Get();
@@ -193,7 +221,7 @@ auto CreateRightSideMenu(std::string_view left_name) -> std::unique_ptr<MenuBase
}
}
for (auto& e : GetMiscMenuEntries()) {
for (auto& e : GetMenuMenuEntries()) {
if (e.name == name) {
return e.func(MenuFlag_Tab);
}
@@ -204,7 +232,7 @@ auto CreateRightSideMenu(std::string_view left_name) -> std::unique_ptr<MenuBase
} // namespace
auto GetMiscMenuEntries() -> std::span<const MiscMenuEntry> {
auto GetMenuMenuEntries() -> std::span<const MiscMenuEntry> {
return MISC_MENU_ENTRIES;
}
@@ -276,9 +304,9 @@ MainMenu::MainMenu() {
this->SetActions(
std::make_pair(Button::START, Action{App::Exit}),
std::make_pair(Button::SELECT, Action{App::DisplayMiscOptions}),
std::make_pair(Button::SELECT, Action{App::DisplayMenuOptions}),
std::make_pair(Button::Y, Action{"Menu"_i18n, [this](){
auto options = std::make_unique<Sidebar>("Menu Options"_i18n, "v" APP_VERSION_HASH, Sidebar::Side::LEFT);
auto options = std::make_unique<Sidebar>("Menu Options"_i18n, "v" APP_DISPLAY_VERSION, Sidebar::Side::LEFT);
ON_SCOPE_EXIT(App::Push(std::move(options)));
SidebarEntryArray::Items language_items;
@@ -298,9 +326,22 @@ MainMenu::MainMenu() {
language_items.push_back("Vietnamese"_i18n);
language_items.push_back("Ukrainian"_i18n);
options->Add<SidebarEntryCallback>("Theme"_i18n, [](){
App::DisplayThemeOptions();
}, "Customise the look of Sphaira by changing the theme"_i18n);
// build menus info.
std::string menus_info = "Launch one of Sphaira's menus:\n"_i18n;
for (auto& e : GetMenuMenuEntries()) {
if (e.name == App::GetApp()->m_left_menu.Get()) {
continue;
} else if (e.name == App::GetApp()->m_right_menu.Get()) {
continue;
}
menus_info += "- " + i18n::get(e.title) + "\n";
}
menus_info += "\nYou can change the left/right menu in the Advanced Options."_i18n;
options->Add<SidebarEntryCallback>("Menus"_i18n, [](){
App::DisplayMenuOptions();
}, menus_info);
options->Add<SidebarEntryCallback>("Network"_i18n, [this](){
auto options = std::make_unique<Sidebar>("Network Options"_i18n, Sidebar::Side::LEFT);
@@ -326,56 +367,54 @@ MainMenu::MainMenu() {
});
}
options->Add<SidebarEntryBool>("Ftp"_i18n, App::GetFtpEnable(), [](bool& enable){
App::SetFtpEnable(enable);
}, "Enable FTP server to run in the background.\n\n"\
"The default port is 5000 with no user/pass set. "\
"You can change this behaviour in /config/ftpsrv/config.ini"_i18n);
options->Add<SidebarEntryCallback>("FTP"_i18n, [](){ App::DisplayFtpOptions(); },
"Enable / modify the FTP server settings such as port, user/pass and the folders that are shown.\n\n"
"NOTE: Changing any of the options will automatically restart the FTP server when exiting the options menu."_i18n
);
options->Add<SidebarEntryBool>("Mtp"_i18n, App::GetMtpEnable(), [](bool& enable){
App::SetMtpEnable(enable);
}, "Enable MTP server to run in the background."_i18n);
options->Add<SidebarEntryCallback>("MTP"_i18n, [](){ App::DisplayMtpOptions(); },
"Enable / modify the MTP responder settings such as the folders that are shown.\n\n"
"NOTE: Changing any of the options will automatically restart the MTP server when exiting the options menu."_i18n
);
options->Add<SidebarEntryBool>("Nxlink"_i18n, App::GetNxlinkEnable(), [](bool& enable){
options->Add<SidebarEntryCallback>("HDD"_i18n, [](){
App::DisplayHddOptions();
}, "Enable / modify the HDD mount options."_i18n);
options->Add<SidebarEntryBool>("NXlink"_i18n, App::GetNxlinkEnable(), [](bool& enable){
App::SetNxlinkEnable(enable);
}, "Enable NXlink server to run in the background. "\
"NXlink is used to send .nro's from PC to the switch\n\n"\
}, "Enable NXlink server to run in the background. "
"NXlink is used to send .nro's from PC to the switch\n\n"
"If you are not a developer, you can disable this option."_i18n);
options->Add<SidebarEntryBool>("Hdd"_i18n, App::GetHddEnable(), [](bool& enable){
App::SetHddEnable(enable);
}, "Enable mounting of connected USB/HDD devices. "\
"Connected devices can be used in the FileBrowser, as well as a backup location when dumping games and saves."_i18n);
options->Add<SidebarEntryBool>("Hdd write protect"_i18n, App::GetWriteProtect(), [](bool& enable){
App::SetWriteProtect(enable);
}, "Makes the connected HDD read-only."_i18n);
}, "Toggle FTP, MTP, HDD and NXlink\n\n" \
}, "Toggle FTP, MTP, HDD and NXlink\n\n"
"If Sphaira has a update available, you can download it from this menu"_i18n);
options->Add<SidebarEntryCallback>("Theme"_i18n, [](){
App::DisplayThemeOptions();
}, "Customise the look of Sphaira by changing the theme"_i18n);
options->Add<SidebarEntryArray>("Language"_i18n, language_items, [](s64& index_out){
App::SetLanguage(index_out);
}, (s64)App::GetLanguage(),
"Change the language.\n\n"
"If your language isn't found, or translations are missing, please consider opening a PR at "\
"If your language isn't found, or translations are missing, please consider opening a PR at "
"github.com/ITotalJustice/sphaira"_i18n);
options->Add<SidebarEntryCallback>("Misc"_i18n, [](){
App::DisplayMiscOptions();
}, "View and launch one of Sphaira's menus"_i18n);
options->Add<SidebarEntryCallback>("Advanced"_i18n, [](){
options->Add<SidebarEntryCallback>("Advanced Options"_i18n, [](){
App::DisplayAdvancedOptions();
}, "Change the advanced options. "\
}, "Change the advanced options. "
"Please view the info boxes to better understand each option."_i18n);
}}
));
m_centre_menu = std::make_unique<homebrew::Menu>();
std::string center_name;
m_centre_menu = CreateCenterMenu(center_name);
m_current_menu = m_centre_menu.get();
std::string left_side_name;
m_left_menu = CreateLeftSideMenu(left_side_name);
m_left_menu = CreateLeftSideMenu(center_name, left_side_name);
m_right_menu = CreateRightSideMenu(left_side_name);
AddOnLRPress();

View File

@@ -20,20 +20,24 @@ auto MenuBase::GetPolledData(bool force_refresh) -> PolledData {
// doesn't have focus.
if (force_refresh || timestamp.GetSeconds() >= 1) {
data.tm = {};
data.battery_percetange = {};
data.charger_type = {};
data.type = {};
data.status = {};
data.strength = {};
data.ip = {};
// avoid divide by zero if getting the size fails, for whatever reason.
data.sd_free = 1;
data.sd_total = 1;
data.emmc_free = 1;
data.emmc_total = 1;
const auto t = std::time(NULL);
localtime_r(&t, &data.tm);
psmGetBatteryChargePercentage(&data.battery_percetange);
psmGetChargerType(&data.charger_type);
nifmGetInternetConnectionStatus(&data.type, &data.strength, &data.status);
nifmGetCurrentIpAddress(&data.ip);
App::GetSdSize(&data.sd_free, &data.sd_total);
App::GetEmmcSize(&data.emmc_free, &data.emmc_total);
timestamp.Update();
}
@@ -60,7 +64,7 @@ void MenuBase::Draw(NVGcontext* vg, Theme* theme) {
const auto pdata = GetPolledData();
const float start_y = 70;
const float font_size = 22;
const float font_size = 20;
const float spacing = 30;
float start_x = 1220;
@@ -77,21 +81,59 @@ void MenuBase::Draw(NVGcontext* vg, Theme* theme) {
start_x -= spacing + (bounds[2] - bounds[0]); \
}
draw(ThemeEntryID_TEXT, 90, "%u\uFE6A", pdata.battery_percetange);
#define STORAGE_BAR_W 180
#define STORAGE_BAR_H 8
if (App::Get12HourTimeEnable()) {
draw(ThemeEntryID_TEXT, 132, "%02u:%02u %s", (pdata.tm.tm_hour == 0 || pdata.tm.tm_hour == 12) ? 12 : pdata.tm.tm_hour % 12, pdata.tm.tm_min, (pdata.tm.tm_hour < 12) ? "AM" : "PM");
} else {
draw(ThemeEntryID_TEXT, 90, "%02u:%02u", pdata.tm.tm_hour, pdata.tm.tm_min);
}
const auto rounding = 2;
const auto storage_font = 19;
const auto storage_y = start_y - 30;
auto storage_x = start_x - STORAGE_BAR_W;
if (pdata.ip) {
draw(ThemeEntryID_TEXT, 0, "%u.%u.%u.%u", pdata.ip&0xFF, (pdata.ip>>8)&0xFF, (pdata.ip>>16)&0xFF, (pdata.ip>>24)&0xFF);
} else {
draw(ThemeEntryID_TEXT, 0, ("No Internet"_i18n).c_str());
}
gfx::drawTextArgs(vg, storage_x, storage_y, storage_font, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "System %.1f GB"_i18n.c_str(), pdata.emmc_free / 1024.0 / 1024.0 / 1024.0);
// gfx::drawTextArgs(vg, storage_x, storage_y, storage_font, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "eMMC %.1f GB"_i18n.c_str(), pdata.emmc_free / 1024.0 / 1024.0 / 1024.0);
#if 0
Vec4 prog_bar{storage_x, storage_y + 24, STORAGE_BAR_W, STORAGE_BAR_H};
gfx::drawRect(vg, prog_bar, theme->GetColour(ThemeEntryID_PROGRESSBAR_BACKGROUND), rounding);
gfx::drawRect(vg, prog_bar.x, prog_bar.y, ((double)pdata.emmc_free / (double)pdata.emmc_total) * prog_bar.w, prog_bar.h, theme->GetColour(ThemeEntryID_PROGRESSBAR), rounding);
#else
gfx::drawRect(vg, storage_x, storage_y + 24, STORAGE_BAR_W, STORAGE_BAR_H, theme->GetColour(ThemeEntryID_TEXT_INFO), rounding);
gfx::drawRect(vg, storage_x + 1, storage_y + 24 + 1, STORAGE_BAR_W - 2, STORAGE_BAR_H - 2, theme->GetColour(ThemeEntryID_BACKGROUND), rounding);
gfx::drawRect(vg, storage_x + 2, storage_y + 24 + 2, STORAGE_BAR_W - (((double)pdata.emmc_free / (double)pdata.emmc_total) * STORAGE_BAR_W) - 4, STORAGE_BAR_H - 4, theme->GetColour(ThemeEntryID_TEXT_INFO), rounding);
#endif
storage_x -= (STORAGE_BAR_W + spacing);
gfx::drawTextArgs(vg, storage_x, storage_y, storage_font, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "microSD %.1f GB"_i18n.c_str(), pdata.sd_free / 1024.0 / 1024.0 / 1024.0);
gfx::drawRect(vg, storage_x, storage_y + 24, STORAGE_BAR_W, STORAGE_BAR_H, theme->GetColour(ThemeEntryID_TEXT_INFO), rounding);
gfx::drawRect(vg, storage_x + 1, storage_y + 24 + 1, STORAGE_BAR_W - 2, STORAGE_BAR_H - 2, theme->GetColour(ThemeEntryID_BACKGROUND), rounding);
gfx::drawRect(vg, storage_x + 2, storage_y + 24 + 2, STORAGE_BAR_W - (((double)pdata.sd_free / (double)pdata.sd_total) * STORAGE_BAR_W) - 4, STORAGE_BAR_H - 4, theme->GetColour(ThemeEntryID_TEXT_INFO), rounding);
start_x -= (STORAGE_BAR_W + spacing) * 2;
// ran out of space, its one or the other.
if (!App::IsApplication()) {
draw(ThemeEntryID_ERROR, 0, ("[Applet Mode]"_i18n).c_str());
} else if (App::GetApp()->m_show_ip_addr.Get()) {
if (pdata.ip) {
char ip_buf[32];
std::snprintf(ip_buf, sizeof(ip_buf), "%u.%u.%u.%u", pdata.ip & 0xFF, (pdata.ip >> 8) & 0xFF, (pdata.ip >> 16) & 0xFF, (pdata.ip >> 24) & 0xFF);
gfx::textBounds(vg, 0, 0, bounds, ip_buf);
char type_buf[32];
if (pdata.type == NifmInternetConnectionType_WiFi) {
std::snprintf(type_buf, sizeof(type_buf), "Wi-Fi %.0f%%"_i18n.c_str(), ((float)pdata.strength / 3.F) * 100);
} else if (pdata.type == NifmInternetConnectionType_Ethernet) {
std::snprintf(type_buf, sizeof(type_buf), "Ethernet"_i18n.c_str());
} else {
std::snprintf(type_buf, sizeof(type_buf), "Unknown"_i18n.c_str());
}
const auto ip_x = start_x;
const auto ip_w = bounds[2] - bounds[0];
const auto type_x = ip_x - ip_w / 2;
gfx::drawTextArgs(vg, type_x, start_y - 25, storage_font - 1, NVG_ALIGN_CENTER | NVG_ALIGN_BOTTOM, theme->GetColour(ThemeEntryID_TEXT_INFO), "%s", type_buf);
gfx::drawTextArgs(vg, ip_x, start_y, storage_font, NVG_ALIGN_RIGHT | NVG_ALIGN_BOTTOM, theme->GetColour(ThemeEntryID_TEXT), "%s", ip_buf);
} else {
draw(ThemeEntryID_TEXT, 0, ("No Internet"_i18n).c_str());
}
}
#undef draw
@@ -107,7 +149,7 @@ void MenuBase::Draw(NVGcontext* vg, Theme* theme) {
gfx::drawTextArgs(vg, 80, start_y, 28.f, NVG_ALIGN_LEFT | NVG_ALIGN_BOTTOM, theme->GetColour(ThemeEntryID_TEXT), m_title.c_str());
m_scroll_title_sub_heading.Draw(vg, true, title_sub_x, start_y, text_w - title_sub_x, 16, NVG_ALIGN_LEFT | NVG_ALIGN_BOTTOM, theme->GetColour(ThemeEntryID_TEXT_INFO), m_title_sub_heading.c_str());
m_scroll_sub_heading.Draw(vg, true, 80, 685, text_w - 160, 18, NVG_ALIGN_LEFT, theme->GetColour(ThemeEntryID_TEXT), m_sub_heading.c_str());
m_scroll_sub_heading.Draw(vg, true, 80, 675, text_w - 160, 18, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), m_sub_heading.c_str());
}
} // namespace sphaira::ui::menu

View File

@@ -10,13 +10,13 @@
namespace sphaira::ui::menu::mtp {
Menu::Menu(u32 flags) : stream::Menu{"MTP Install"_i18n, flags} {
m_was_mtp_enabled = haze::IsInit();
m_was_mtp_enabled = libhaze::IsInit();
if (!m_was_mtp_enabled) {
log_write("[MTP] wasn't enabled, forcefully enabling\n");
haze::Init();
libhaze::Init();
}
haze::InitInstallMode(
libhaze::InitInstallMode(
[this](const char* path){ return OnInstallStart(path); },
[this](const void *buf, size_t size){ return OnInstallWrite(buf, size); },
[this](){ return OnInstallClose(); }
@@ -25,11 +25,11 @@ Menu::Menu(u32 flags) : stream::Menu{"MTP Install"_i18n, flags} {
Menu::~Menu() {
// signal for thread to exit and wait.
haze::DisableInstallMode();
libhaze::DisableInstallMode();
if (!m_was_mtp_enabled) {
log_write("[MTP] disabling on exit\n");
haze::Exit();
libhaze::Exit();
}
}
@@ -53,7 +53,7 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
}
void Menu::OnDisableInstallMode() {
haze::DisableInstallMode();
libhaze::DisableInstallMode();
}
} // namespace sphaira::ui::menu::mtp

View File

@@ -9,6 +9,7 @@
#include "threaded_file_transfer.hpp"
#include "minizip_helper.hpp"
#include "dumper.hpp"
#include "swkbd.hpp"
#include "utils/devoptab.hpp"
@@ -40,7 +41,7 @@ constexpr u32 NX_SAVE_META_MAGIC = 0x4A4B5356; // JKSV
constexpr u32 NX_SAVE_META_VERSION = 1;
constexpr const char* NX_SAVE_META_NAME = ".nx_save_meta.bin";
constinit UEvent g_change_uevent;
std::atomic_bool g_change_signalled{};
struct DumpSource final : dump::BaseSource {
DumpSource(std::span<const std::reference_wrapper<Entry>> entries, std::span<const fs::FsPath> paths)
@@ -319,7 +320,7 @@ void FreeEntry(NVGcontext* vg, Entry& e) {
} // namespace
void SignalChange() {
ueventSignal(&g_change_uevent);
g_change_signalled = true;
}
Menu::Menu(u32 flags) : grid::Menu{"Saves"_i18n, flags} {
@@ -388,7 +389,6 @@ Menu::Menu(u32 flags) : grid::Menu{"Saves"_i18n, flags} {
}
title::Init();
ueventCreate(&g_change_uevent, true);
}
Menu::~Menu() {
@@ -399,7 +399,7 @@ Menu::~Menu() {
}
void Menu::Update(Controller* controller, TouchInfo* touch) {
if (R_SUCCEEDED(waitSingle(waiterForUEvent(&g_change_uevent), 0))) {
if (g_change_signalled.exchange(false)) {
m_dirty = true;
}
@@ -508,9 +508,9 @@ void Menu::ScanHomebrew() {
constexpr auto ENTRY_CHUNK_COUNT = 1000;
TimeStamp ts;
g_change_signalled = false;
FreeEntries();
ClearSelection();
ueventClear(&g_change_uevent);
m_entries.reserve(ENTRY_CHUNK_COUNT);
m_is_reversed = false;
m_dirty = false;
@@ -690,13 +690,38 @@ void Menu::DisplayOptions() {
entries.emplace_back(m_entries[m_index]);
}
BackupSaves(entries);
}, true);
BackupSaves(entries, BackupFlag_None);
}, true,
"Backup the selected save(s) to a location of your choice."_i18n
);
if (!m_selected_count || m_selected_count == 1) {
options->Add<SidebarEntryCallback>("Backup to..."_i18n, [this](){
std::vector<std::reference_wrapper<Entry>> entries;
if (m_selected_count) {
for (auto& e : m_entries) {
if (e.selected) {
entries.emplace_back(e);
}
}
} else {
entries.emplace_back(m_entries[m_index]);
}
BackupSaves(entries, BackupFlag_SetName);
}, true,
"Backup the selected save(s) to a location of your choice, and set the name of the backup."_i18n
);
}
if (m_entries[m_index].save_data_type == FsSaveDataType_Account || m_entries[m_index].save_data_type == FsSaveDataType_Bcat) {
options->Add<SidebarEntryCallback>("Restore"_i18n, [this](){
RestoreSave();
}, true);
}, true,
"Restore the save for the current title.\n"
"if \"Auto backup\" is enabled, the save will first be backed up and then restored. "
"Saves that are auto backed up will have \"Auto\" in their name."_i18n
);
}
}
@@ -706,18 +731,21 @@ void Menu::DisplayOptions() {
options->Add<SidebarEntryBool>("Auto backup on restore"_i18n, m_auto_backup_on_restore.Get(), [this](bool& v_out){
m_auto_backup_on_restore.Set(v_out);
});
}, "If enabled, when restoring a save, the current save will first be backed up."_i18n);
options->Add<SidebarEntryBool>("Compress backup"_i18n, m_compress_save_backup.Get(), [this](bool& v_out){
m_compress_save_backup.Set(v_out);
});
}, "If enabled, backups will be compressed to a zip file.\n\n"
"NOTE: Disabling this option does not disable the zip file, it only disables compressing "
"the files stored in the zip.\n"
"Disabling will result in a much faster backup, at the cost of the file size."_i18n);
});
}
void Menu::BackupSaves(std::vector<std::reference_wrapper<Entry>>& entries) {
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 {
return BackupSaveInternal(pbox, location, entries, m_compress_save_backup.Get());
void Menu::BackupSaves(std::vector<std::reference_wrapper<Entry>>& entries, u32 flags) {
dump::DumpGetLocation("Select backup location"_i18n, dump::DumpLocationFlag_SdCard|dump::DumpLocationFlag_Stdio|dump::DumpLocationFlag_Usb, [this, entries, flags](const dump::DumpLocation& location){
App::Push<ProgressBox>(0, "Backup"_i18n, "", [this, entries, location, flags](auto pbox) -> Result {
return BackupSaveInternal(pbox, location, entries, flags);
}, [](Result rc){
App::PushErrorBox(rc, "Backup failed!"_i18n);
@@ -730,17 +758,23 @@ void Menu::BackupSaves(std::vector<std::reference_wrapper<Entry>>& entries) {
void Menu::RestoreSave() {
dump::DumpGetLocation("Select restore location"_i18n, dump::DumpLocationFlag_SdCard|dump::DumpLocationFlag_Stdio, [this](const dump::DumpLocation& location){
std::unique_ptr<fs::Fs> fs;
std::unique_ptr<fs::Fs> fs{};
fs::FsPath mount{};
if (location.entry.type == dump::DumpLocationType_Stdio) {
mount = fs::AppendPath(location.stdio[location.entry.index].mount, location.stdio[location.entry.index].dump_path);
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 {
App::PushErrorBox(MAKERESULT(Module_Libnx, LibnxError_BadInput), "Invalid location type!"_i18n);
return;
}
// get saves in /Saves/Name and /Saves/app_id
filebrowser::FsDirCollection collections[2]{};
for (auto i = 0; i < std::size(collections); i++) {
const auto save_path = fs::AppendPath(fs->Root(), BuildSaveBasePath(m_entries[m_index], i != 0));
const auto save_path = fs::AppendPath(mount, BuildSaveBasePath(m_entries[m_index], i != 0));
filebrowser::FsView::get_collection(fs.get(), save_path, "", collections[i], true, false, false);
// reverse as they will be sorted in oldest -> newest.
// todo: better impl when both id and normal app folders are used.
@@ -763,7 +797,7 @@ void Menu::RestoreSave() {
if (paths.empty()) {
App::Push<ui::OptionBox>(
"No saves found in "_i18n + fs::AppendPath(fs->Root(), BuildSaveBasePath(m_entries[m_index])).toString(),
"No saves found in "_i18n + fs::AppendPath(mount, BuildSaveBasePath(m_entries[m_index])).toString(),
"OK"_i18n
);
return;
@@ -789,7 +823,7 @@ void Menu::RestoreSave() {
if (m_auto_backup_on_restore.Get()) {
pbox->SetActionName("Auto backup"_i18n);
R_TRY(BackupSaveInternal(pbox, location, m_entries[m_index], m_compress_save_backup.Get(), true));
R_TRY(BackupSaveInternal(pbox, location, m_entries[m_index], BackupFlag_IsAuto));
}
pbox->SetActionName("Restore"_i18n);
@@ -809,7 +843,7 @@ void Menu::RestoreSave() {
});
}
auto Menu::BuildSavePath(const Entry& e, bool is_auto) const -> fs::FsPath {
auto Menu::BuildSavePath(const Entry& e, u32 flags) const -> fs::FsPath {
const auto t = std::time(NULL);
const auto tm = std::localtime(&t);
const auto base = BuildSaveBasePath(e);
@@ -817,27 +851,39 @@ auto Menu::BuildSavePath(const Entry& e, bool is_auto) const -> fs::FsPath {
char time[64];
std::snprintf(time, sizeof(time), "%u.%02u.%02u @ %02u.%02u.%02u", tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec);
fs::FsPath path;
fs::FsPath name;
if (e.save_data_type == FsSaveDataType_Account) {
const auto acc = m_accounts[m_account_index];
fs::FsPath name_buf;
if (is_auto) {
if (flags & BackupFlag_IsAuto) {
std::snprintf(name_buf, sizeof(name_buf), "AUTO - %s", acc.nickname);
} else {
std::snprintf(name_buf, sizeof(name_buf), "%s", acc.nickname);
}
title::utilsReplaceIllegalCharacters(name_buf, true);
std::snprintf(path, sizeof(path), "%s/%s - %s.zip", base.s, name_buf.s, time);
std::snprintf(name, sizeof(name), "%s - %s.zip", name_buf.s, time);
} else {
std::snprintf(path, sizeof(path), "%s/%s.zip", base.s, time);
std::snprintf(name, sizeof(name), "%s.zip", time);
}
return path;
if (flags & BackupFlag_SetName) {
std::string out;
while (out.empty()) {
const auto header = "Set name for "_i18n + e.GetName();
if (R_FAILED(swkbd::ShowText(out, header.c_str(), "Set backup name", name, 1, 128))) {
out.clear();
}
}
name = out;
}
return fs::AppendPath(base, name);
}
Result Menu::RestoreSaveInternal(ProgressBox* pbox, const Entry& e, const fs::FsPath& path) const {
Result Menu::RestoreSaveInternal(ProgressBox* pbox, const Entry& e, const fs::FsPath& path) {
pbox->SetTitle(e.GetName());
if (e.image) {
pbox->SetImage(e.image);
@@ -955,14 +1001,15 @@ Result Menu::RestoreSaveInternal(ProgressBox* pbox, const Entry& e, const fs::Fs
R_SUCCEED();
}
Result Menu::BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& location, std::span<const std::reference_wrapper<Entry>> entries, bool compressed, bool is_auto) const {
Result Menu::BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& location, std::span<const std::reference_wrapper<Entry>> entries, u32 flags) {
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));
paths.emplace_back(BuildSavePath(e, flags));
}
const auto compressed = m_compress_save_backup.Get();
auto source = std::make_shared<DumpSource>(entries, paths);
return dump::Dump(pbox, source, location, paths, [&](ui::ProgressBox* pbox, dump::BaseSource* _source, dump::WriteSource* writer, const fs::FsPath& path) -> Result {
@@ -1032,7 +1079,7 @@ Result Menu::BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& loc
{
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));
ON_SCOPE_EXIT(zipClose(zfile, "sphaira v" APP_DISPLAY_VERSION));
// add save meta.
{
@@ -1111,11 +1158,11 @@ Result Menu::BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& loc
});
}
Result Menu::BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& location, Entry& e, bool compressed, bool is_auto) const {
Result Menu::BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& location, Entry& e, u32 flags) {
std::vector<std::reference_wrapper<Entry>> entries;
entries.emplace_back(e);
return BackupSaveInternal(pbox, location, entries, compressed, is_auto);
return BackupSaveInternal(pbox, location, entries, flags);
}
Result Menu::MountSaveFs() {
@@ -1125,8 +1172,8 @@ Result Menu::MountSaveFs() {
fs::FsPath 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);
auto fs = std::make_shared<filebrowser::FsStdioWrapper>(root, [root](){
devoptab::UmountNeworkDevice(root);
});
filebrowser::MountFsHelper(fs, e.GetName());

View File

@@ -5,7 +5,6 @@
#include "log.hpp"
#include "ui/nvg_util.hpp"
#include "i18n.hpp"
#include "haze_helper.hpp"
#include "utils/thread.hpp"
@@ -35,10 +34,10 @@ Menu::Menu(u32 flags) : MenuBase{"USB"_i18n, flags} {
}});
// if mtp is enabled, disable it for now.
m_was_mtp_enabled = haze::IsInit();
m_was_mtp_enabled = App::GetMtpEnable();
if (m_was_mtp_enabled) {
App::Notify("Disable MTP for usb install"_i18n);
haze::Exit();
App::SetMtpEnable(false);
}
// 3 second timeout for transfers.
@@ -70,7 +69,7 @@ Menu::~Menu() {
if (m_was_mtp_enabled) {
App::Notify("Re-enabled MTP"_i18n);
haze::Init();
App::SetMtpEnable(true);
}
}
@@ -97,6 +96,9 @@ 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 {
pbox->AddCancelEvent(m_usb_source->GetCancelEvent());
ON_SCOPE_EXIT(pbox->RemoveCancelEvent(m_usb_source->GetCancelEvent()));
log_write("inside progress box\n");
for (u32 i = 0; i < std::size(m_names); i++) {
const auto& file_name = m_names[i];

View File

@@ -387,6 +387,36 @@ void drawAppLable(NVGcontext* vg, const Theme* theme, ScrollingText& st, float x
st.Draw(vg, true, text_x, text_y, box_w - text_pad * 2, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_SELECTED), name);
}
// https://github.com/memononen/nanovg/blob/f93799c078fa11ed61c078c65a53914c8782c00b/example/demo.c#L500
void drawSpinner(NVGcontext* vg, const 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);
}
#define HIGHLIGHT_SPEED 350.0
static double highlightGradientX = 0;

View File

@@ -110,6 +110,7 @@ auto PopupList::Draw(NVGcontext* vg, Theme* theme) -> void {
gfx::drawText(vg, m_pos + m_title_pos, 24.f, theme->GetColour(ThemeEntryID_TEXT), m_title.c_str());
gfx::drawRect(vg, 30.f, m_line_top, m_line_width, 1.f, theme->GetColour(ThemeEntryID_LINE));
gfx::drawRect(vg, 30.f, m_line_bottom, m_line_width, 1.f, theme->GetColour(ThemeEntryID_LINE));
gfx::drawTextArgs(vg, 80, 675, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%zu / %zu", m_index + 1, m_items.size());
m_list->Draw(vg, theme, m_items.size(), [this](auto* vg, auto* theme, auto& v, auto i) {
const auto& [x, y, w, h] = v;

View File

@@ -7,6 +7,7 @@
#include "threaded_file_transfer.hpp"
#include "i18n.hpp"
#include "utils/utils.hpp"
#include "utils/thread.hpp"
#include <cstring>
@@ -21,36 +22,6 @@ 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)
@@ -180,17 +151,7 @@ auto ProgressBox::Draw(NVGcontext* vg, Theme* theme) -> void {
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);
char speed_str[32];
if (speed_mb >= 0.01) {
std::snprintf(speed_str, sizeof(speed_str), "%.2f MiB/s", speed_mb);
} else {
std::snprintf(speed_str, sizeof(speed_str), "%.2f KiB/s", speed_kb);
}
gfx::drawSpinner(vg, theme, prog_bar.x - pad - rad, prog_bar.y + prog_bar.h / 2, rad, armTicksToNs(armGetSystemTick()) / 1e+9);
const auto left = size - last_offset;
const auto left_seconds = left / speed;
@@ -207,7 +168,7 @@ auto ProgressBox::Draw(NVGcontext* vg, Theme* theme) -> void {
std::snprintf(time_str, sizeof(time_str), "%zu seconds remaining"_i18n.c_str(), seconds);
}
gfx::drawTextArgs(vg, center_x, prog_bar.y + prog_bar.h + 30, 18, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%s (%s)", time_str, speed_str);
gfx::drawTextArgs(vg, center_x, prog_bar.y + prog_bar.h + 30, 18, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%s (%s)", time_str, utils::formatSizeNetwork(speed).c_str());
}
gfx::drawTextArgs(vg, center_x, m_pos.y + 40, 24, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), action.c_str());
@@ -232,79 +193,72 @@ auto ProgressBox::Draw(NVGcontext* vg, Theme* theme) -> void {
}
auto ProgressBox::SetActionName(const std::string& action) -> ProgressBox& {
mutexLock(&m_mutex);
SCOPED_MUTEX(&m_mutex);
m_action = action;
mutexUnlock(&m_mutex);
Yield();
return *this;
}
auto ProgressBox::SetTitle(const std::string& title) -> ProgressBox& {
mutexLock(&m_mutex);
SCOPED_MUTEX(&m_mutex);
m_title = title;
mutexUnlock(&m_mutex);
Yield();
return *this;
}
auto ProgressBox::NewTransfer(const std::string& transfer) -> ProgressBox& {
mutexLock(&m_mutex);
SCOPED_MUTEX(&m_mutex);
m_transfer = transfer;
m_size = 0;
m_offset = 0;
m_last_offset = 0;
m_timestamp.Update();
mutexUnlock(&m_mutex);
Yield();
return *this;
}
auto ProgressBox::ResetTranfser() -> ProgressBox& {
mutexLock(&m_mutex);
SCOPED_MUTEX(&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);
SCOPED_MUTEX(&m_mutex);
m_size = size;
m_offset = offset;
mutexUnlock(&m_mutex);
Yield();
return *this;
}
auto ProgressBox::SetImage(int image) -> ProgressBox& {
mutexLock(&m_mutex);
SCOPED_MUTEX(&m_mutex);
m_image_pending = image;
m_is_image_pending = true;
mutexUnlock(&m_mutex);
return *this;
}
auto ProgressBox::SetImageData(std::vector<u8>& data) -> ProgressBox& {
mutexLock(&m_mutex);
SCOPED_MUTEX(&m_mutex);
std::swap(m_image_data, data);
mutexUnlock(&m_mutex);
return *this;
}
auto ProgressBox::SetImageDataConst(std::span<const u8> data) -> ProgressBox& {
mutexLock(&m_mutex);
SCOPED_MUTEX(&m_mutex);
m_image_data.resize(data.size());
std::memcpy(m_image_data.data(), data.data(), m_image_data.size());
mutexUnlock(&m_mutex);
return *this;
}
void ProgressBox::RequestExit() {
SCOPED_MUTEX(&m_mutex);
m_stop_source.request_stop();
ueventSignal(GetCancelEvent());
// cancel any registered events.
for (auto& e : m_cancel_events) {
ueventSignal(e);
}
}
auto ProgressBox::ShouldExit() -> bool {
@@ -318,6 +272,26 @@ auto ProgressBox::ShouldExitResult() -> Result {
R_SUCCEED();
}
void ProgressBox::AddCancelEvent(UEvent* event) {
if (!event) {
return;
}
SCOPED_MUTEX(&m_mutex);
if (std::ranges::find(m_cancel_events, event) == m_cancel_events.end()) {
m_cancel_events.emplace_back(event);
}
}
void ProgressBox::RemoveCancelEvent(const UEvent* event) {
if (!event) {
return;
}
SCOPED_MUTEX(&m_mutex);
m_cancel_events.erase(std::remove(m_cancel_events.begin(), m_cancel_events.end(), event), m_cancel_events.end());
}
auto ProgressBox::CopyFile(fs::Fs* fs_src, fs::Fs* fs_dst, const fs::FsPath& src_path, const fs::FsPath& dst_path, bool single_threaded) -> Result {
const auto is_file_based_emummc = App::IsFileBaseEmummc();
const auto is_both_native = fs_src->IsNative() && fs_dst->IsNative();

View File

@@ -124,24 +124,27 @@ SidebarEntryBool::SidebarEntryBool(const std::string& title, bool option, const
} else {
m_option ^= 1;
m_callback(m_option);
SetDirty();
} }
});
}
SidebarEntryBool::SidebarEntryBool(const std::string& title, bool& option, const std::string& info, const std::string& true_str, const std::string& false_str)
: SidebarEntryBool{title, option, Callback{}, info, true_str, false_str} {
m_callback = [&option](bool&){
m_callback = [this, &option](bool&){
option ^= 1;
SetDirty();
};
}
SidebarEntryBool::SidebarEntryBool(const std::string& title, option::OptionBool& option, const Callback& cb, const std::string& info, const std::string& true_str, const std::string& false_str)
: SidebarEntryBool{title, option.Get(), Callback{}, info, true_str, false_str} {
m_callback = [&option, cb](bool& v_out){
m_callback = [this, &option, cb](bool& v_out){
if (cb) {
cb(v_out);
}
option.Set(v_out);
SetDirty();
};
}
@@ -166,6 +169,7 @@ SidebarEntrySlider::SidebarEntrySlider(const std::string& title, float value, fl
DependsClick();
} else {
m_value = std::clamp(m_value - m_inc, m_min, m_max);
SetDirty();
// m_callback(m_option);
} }
});
@@ -174,6 +178,7 @@ SidebarEntrySlider::SidebarEntrySlider(const std::string& title, float value, fl
DependsClick();
} else {
m_value = std::clamp(m_value + m_inc, m_min, m_max);
SetDirty();
// m_callback(m_option);
} }
});
@@ -240,6 +245,8 @@ SidebarEntryArray::SidebarEntryArray(const std::string& title, const Items& item
App::Push<PopupList>(
m_title, m_items, index, m_index
);
SetDirty();
};
}
@@ -275,6 +282,7 @@ SidebarEntryArray::SidebarEntryArray(const std::string& title, const Items& item
} else {
// m_callback(m_index);
m_list_callback();
SetDirty();
}}
});
}
@@ -291,6 +299,7 @@ SidebarEntryTextBase::SidebarEntryTextBase(const std::string& title, const std::
SetAction(Button::A, Action{"OK"_i18n, [this](){
if (m_callback) {
m_callback();
SetDirty();
}
}});
}
@@ -300,16 +309,36 @@ void SidebarEntryTextBase::Draw(NVGcontext* vg, Theme* theme, const Vec4& root_p
SidebarEntryBase::DrawEntry(vg, theme, m_title, m_value, true);
}
SidebarEntryTextInput::SidebarEntryTextInput(const std::string& title, const std::string& value, const std::string& guide, s64 len_min, s64 len_max, const std::string& info)
SidebarEntryTextInput::SidebarEntryTextInput(const std::string& title, const std::string& value, const std::string& header, const std::string& guide, s64 len_min, s64 len_max, const std::string& info, const Callback& callback)
: SidebarEntryTextBase{title, value, {}, info}
, m_guide{guide}
, m_header{header.empty() ? title : header}
, m_guide{guide.empty() ? title : guide}
, m_len_min{len_min}
, m_len_max{len_max} {
, m_len_max{len_max}
, m_callback{callback} {
SetCallback([this](){
std::string out;
if (R_SUCCEEDED(swkbd::ShowText(out, m_guide.c_str(), GetValue().c_str(), m_len_min, m_len_max))) {
if (R_SUCCEEDED(swkbd::ShowText(out, m_header.c_str(), m_guide.c_str(), GetValue().c_str(), m_len_min, m_len_max))) {
SetValue(out);
if (m_callback) {
m_callback(this);
}
}
});
}
SidebarEntryTextInput::SidebarEntryTextInput(const std::string& title, s64 value, const std::string& header, const std::string& guide, s64 len_min, s64 len_max, const std::string& info, const Callback& callback)
: SidebarEntryTextInput{title, std::to_string(value), header, guide, len_min, len_max, info, callback} {
SetCallback([this](){
s64 out = std::stoul(GetValue());
if (R_SUCCEEDED(swkbd::ShowNumPad(out, m_header.c_str(), m_guide.c_str(), GetValue().c_str(), m_len_min, m_len_max))) {
SetValue(std::to_string(out));
if (m_callback) {
m_callback(this);
}
}
});
}
@@ -321,6 +350,7 @@ SidebarEntryFilePicker::SidebarEntryFilePicker(const std::string& title, const s
App::Push<menu::filebrowser::picker::Menu>(
[this](const fs::FsPath& path) {
SetValue(path);
SetDirty();
return true;
},
m_filter
@@ -328,26 +358,21 @@ SidebarEntryFilePicker::SidebarEntryFilePicker(const std::string& title, const s
});
}
Sidebar::Sidebar(const std::string& title, Side side, Items&& items)
: Sidebar{title, "", side, std::forward<decltype(items)>(items)} {
Sidebar::Sidebar(const std::string& title, Side side, float width)
: Sidebar{title, "", side, width} {
}
Sidebar::Sidebar(const std::string& title, Side side)
: Sidebar{title, "", side, {}} {
}
Sidebar::Sidebar(const std::string& title, const std::string& sub, Side side, Items&& items)
Sidebar::Sidebar(const std::string& title, const std::string& sub, Side side, float width)
: m_title{title}
, m_sub{sub}
, m_side{side}
, m_items{std::forward<decltype(items)>(items)} {
, m_side{side} {
switch (m_side) {
case Side::LEFT:
SetPos(Vec4{0.f, 0.f, 450.f, SCREEN_HEIGHT});
SetPos(Vec4{0.f, 0.f, width, SCREEN_HEIGHT});
break;
case Side::RIGHT:
SetPos(Vec4{SCREEN_WIDTH - 450.f, 0.f, 450.f, SCREEN_HEIGHT});
SetPos(Vec4{SCREEN_WIDTH - width, 0.f, width, SCREEN_HEIGHT});
break;
}
@@ -365,11 +390,17 @@ Sidebar::Sidebar(const std::string& title, const std::string& sub, Side side, It
m_list->SetScrollBarPos(GetX() + GetW() - 20, m_base_pos.y - 10, pos.h - m_base_pos.y + 48);
}
Sidebar::Sidebar(const std::string& title, const std::string& sub, Side side)
: Sidebar{title, sub, side, {}} {
Sidebar::~Sidebar() {
if (m_on_exit_when_changed) {
for (const auto& item : m_items) {
if (item->IsDirty()) {
m_on_exit_when_changed();
break;
}
}
}
}
auto Sidebar::Update(Controller* controller, TouchInfo* touch) -> void {
Widget::Update(controller, touch);
@@ -405,6 +436,7 @@ auto Sidebar::Draw(NVGcontext* vg, Theme* theme) -> void {
}
gfx::drawRect(vg, m_top_bar, theme->GetColour(ThemeEntryID_LINE));
gfx::drawRect(vg, m_bottom_bar, theme->GetColour(ThemeEntryID_LINE));
gfx::drawTextArgs(vg, m_pos.x + 30, 675, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%zu / %zu", m_index + 1, m_items.size());
Widget::Draw(vg, theme);

View File

@@ -36,7 +36,7 @@ Base::Base(u64 transfer_timeout) {
App::SetAutoSleepDisabled(true);
m_transfer_timeout = transfer_timeout;
ueventCreate(GetCancelEvent(), true);
ueventCreate(GetCancelEvent(), false);
m_aligned = std::make_unique<u8*>(new(std::align_val_t{TRANSFER_ALIGN}) u8[TRANSFER_MAX]);
}

View File

@@ -20,7 +20,7 @@ Usb::Usb(u64 transfer_timeout) {
Usb::~Usb() {
if (m_was_connected && R_SUCCEEDED(m_usb->IsUsbConnected(0))) {
const auto send_header = SendPacket::Build(CMD_QUIT);
SendAndVerify(&send_header, sizeof(send_header));
SendAndVerify(&send_header, sizeof(send_header), 1e+9);
}
}

View File

@@ -20,7 +20,7 @@ Usb::Usb(u64 transfer_timeout) {
Usb::~Usb() {
if (m_was_connected && R_SUCCEEDED(m_usb->IsUsbConnected(0))) {
const auto send_header = SendPacket::Build(CMD_QUIT);
SendAndVerify(&send_header, sizeof(send_header));
SendAndVerify(&send_header, sizeof(send_header), 1e+9);
}
}

View File

@@ -65,7 +65,7 @@ bool GetMountPoint(location::StdioEntry& out) {
out.mount = fs.mountpoint;
out.name = display_name;
out.write_protect = true;
out.flags = location::FsEntryFlag::FsEntryFlag_ReadOnly;
return true;
}

View File

@@ -21,52 +21,55 @@
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wcast-qual"
#pragma GCC diagnostic ignored "-Wunused-function"
#if 0
#define DR_FLAC_IMPLEMENTATION
#define DR_FLAC_NO_OGG
#define DR_FLAC_NO_STDIO
#define DRFLAC_API static
#define DRFLAC_PRIVATE static
#include <dr_flac.h>
#endif
#define DR_WAV_IMPLEMENTATION
#define DR_WAV_NO_STDIO
#define DRWAV_API static
#define DRWAV_PRIVATE static
#include <dr_wav.h>
#define DR_MP3_IMPLEMENTATION
#define DR_MP3_NO_STDIO
#define DRMP3_API static
#define DRMP3_PRIVATE static
// improves load / seek times.
// hopefully drmp3 will have binary seek rather than linear.
// this also improves
#define DRMP3_DATA_CHUNK_SIZE (1024*64)
#include <dr_mp3.h>
#if 0
#define DR_VORBIS_IMPLEMENTATION
#define DR_VORBIS_NO_STDIO
#define DR_VORBIS_API static
#include "dr_vorbis.h"
#endif
#pragma GCC diagnostic pop
#pragma GCC diagnostic pop
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-variable"
#pragma GCC diagnostic ignored "-Walloca"
#pragma GCC diagnostic ignored "-Wunused-variable"
#define STB_VORBIS_NO_PUSHDATA_API
#define STB_VORBIS_NO_STDIO
#define STB_VORBIS_NO_OPENMEM
#include "stb_vorbis.h"
#pragma GCC diagnostic pop
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-variable"
#include <id3v2lib.h>
#ifdef ENABLE_AUDIO_FLAC
#define DR_FLAC_IMPLEMENTATION
#define DR_FLAC_NO_OGG
#define DR_FLAC_NO_STDIO
#define DRFLAC_API static
#define DRFLAC_PRIVATE static
#include <dr_flac.h>
#endif // ENABLE_AUDIO_FLAC
#ifdef ENABLE_AUDIO_WAV
#define DR_WAV_IMPLEMENTATION
#define DR_WAV_NO_STDIO
#define DRWAV_API static
#define DRWAV_PRIVATE static
#include <dr_wav.h>
#endif // ENABLE_AUDIO_WAV
#ifdef ENABLE_AUDIO_MP3
#define DR_MP3_IMPLEMENTATION
#define DR_MP3_NO_STDIO
#define DRMP3_API static
#define DRMP3_PRIVATE static
// improves load / seek times.
// hopefully drmp3 will have binary seek rather than linear.
// this also improves
#define DRMP3_DATA_CHUNK_SIZE (1024*64)
#include <dr_mp3.h>
#include <id3v2lib.h>
#endif // ENABLE_AUDIO_MP3
#ifdef ENABLE_AUDIO_OGG
#if 0
#define DR_VORBIS_IMPLEMENTATION
#define DR_VORBIS_NO_STDIO
#define DR_VORBIS_API static
#include "dr_vorbis.h"
#endif
#define STB_VORBIS_NO_PUSHDATA_API
#define STB_VORBIS_NO_STDIO
#define STB_VORBIS_NO_OPENMEM
#include "stb_vorbis.h"
#endif // ENABLE_AUDIO_OGG
#pragma GCC diagnostic pop
#pragma GCC diagnostic pop
#pragma GCC diagnostic pop
#pragma GCC diagnostic pop
#include <pulsar.h>
@@ -130,6 +133,7 @@ private:
s64 m_size{};
};
#ifdef ENABLE_AUDIO_MP3
// gta vice "encrypted" mp3's using xor 0x22, very cool.
struct GTAViceCityFile final : File {
size_t ReadFile(void* _buf, size_t read_size) override {
@@ -164,6 +168,7 @@ auto convert_utf16(const ID3v2_TextFrameData* data) -> std::string{
buf[sz] = 0;
return buf;
}
#endif // ENABLE_AUDIO_MP3
struct Base {
virtual ~Base() = default;
@@ -458,6 +463,7 @@ struct PlsrBFWAV final : PlsrBase {
}
};
#ifdef ENABLE_AUDIO_WAV
struct DrWAV final : CustomBase {
~DrWAV() {
drwav_uninit(&m_wav);
@@ -532,7 +538,9 @@ private:
drwav m_wav{};
File m_file{};
};
#endif // ENABLE_AUDIO_WAV
#ifdef ENABLE_AUDIO_MP3
struct DrMP3 final : CustomBase {
DrMP3(std::unique_ptr<File>&& file = std::make_unique<File>()) : m_file{std::forward<decltype(file)>(file)} {
@@ -668,8 +676,9 @@ private:
std::vector<drmp3_seek_point> m_seek_points{};
#endif
};
#endif // ENABLE_AUDIO_MP3
#if 0
#ifdef ENABLE_AUDIO_FLAC
struct DrFLAC final : CustomBase {
~DrFLAC() {
drflac_close(m_flac);
@@ -718,8 +727,9 @@ private:
drflac* m_flac{};
File m_file{};
};
#endif
#endif // ENABLE_AUDIO_FLAC
#ifdef ENABLE_AUDIO_OGG
// api is not ready, leaving this here for when it is.
#if 0
struct DrOGG final : CustomBase {
@@ -864,6 +874,7 @@ private:
stb_vorbis* m_ogg{};
File m_file{};
};
#endif // ENABLE_AUDIO_OGG
constexpr u32 MAX_SONGS = 4;
@@ -886,11 +897,12 @@ struct SongEntry {
}
};
Mutex g_mutex;
SongEntry g_songs[MAX_SONGS];
Mutex g_mutex{};
SongEntry g_songs[MAX_SONGS]{};
PLSR_PlayerSoundId g_sound_ids[std::to_underlying(SoundEffect::MAX)]{};
Thread g_thread{};
UEvent g_cancel_uevent{};
std::atomic_bool g_is_init{};
void thread_func(void* arg) {
auto player = plsrPlayerGetInstance();
@@ -950,6 +962,10 @@ void thread_func(void* arg) {
} // namespace
Result Init() {
if (g_is_init) {
R_SUCCEED();
}
SCOPED_MUTEX(&g_mutex);
R_TRY(plsrPlayerInit());
@@ -996,14 +1012,21 @@ Result Init() {
R_TRY(utils::CreateThread(&g_thread, thread_func, nullptr, 1024*128, 0x20));
R_TRY(threadStart(&g_thread));
g_is_init = true;
R_SUCCEED();
}
void ExitSignal() {
ueventSignal(&g_cancel_uevent);
if (g_is_init) {
ueventSignal(&g_cancel_uevent);
}
}
void Exit() {
if (!g_is_init) {
return;
}
ExitSignal();
threadWaitForExit(&g_thread);
threadClose(&g_thread);
@@ -1025,9 +1048,12 @@ void Exit() {
std::memset(g_songs, 0, sizeof(g_songs));
std::memset(g_sound_ids, 0, sizeof(g_sound_ids));
g_is_init = false;
}
Result PlaySoundEffect(SoundEffect effect) {
R_UNLESS(g_is_init, 0x1);
SCOPED_MUTEX(&g_mutex);
const auto id = g_sound_ids[std::to_underlying(effect)];
@@ -1040,6 +1066,8 @@ Result PlaySoundEffect(SoundEffect effect) {
}
Result OpenSong(fs::Fs* fs, const fs::FsPath& path, u32 flags, SongID* id) {
R_UNLESS(g_is_init, 0x1);
SCOPED_MUTEX(&g_mutex);
R_UNLESS(fs && id && !path.empty(), 0x1);
@@ -1055,24 +1083,32 @@ Result OpenSong(fs::Fs* fs, const fs::FsPath& path, u32 flags, SongID* id) {
else if (path.ends_with(".bfwav")) {
source = std::make_unique<PlsrBFWAV>();
}
#ifdef ENABLE_AUDIO_WAV
else if (path.ends_with(".wav")) {
source = std::make_unique<DrWAV>();
}
#endif // ENABLE_AUDIO_WAV
#ifdef ENABLE_AUDIO_MP3
else if (path.ends_with(".mp3") || path.ends_with(".mp2") || path.ends_with(".mp1")) {
source = std::make_unique<DrMP3>();
}
else if (path.ends_with(".adf")) {
source = std::make_unique<DrMP3>(std::make_unique<GTAViceCityFile>());
}
// else if (path.ends_with(".flac")) {
// source = std::make_unique<DrFLAC>();
// }
#endif // ENABLE_AUDIO_MP3
#ifdef ENABLE_AUDIO_FLAC
else if (path.ends_with(".flac")) {
source = std::make_unique<DrFLAC>();
}
#endif // ENABLE_AUDIO_FLAC
#ifdef ENABLE_AUDIO_OGG
// else if (path.ends_with(".ogg")) {
// source = std::make_unique<DrOGG>();
// }
else if (path.ends_with(".ogg")) {
source = std::make_unique<stbOGG>();
}
#endif // ENABLE_AUDIO_OGG
R_UNLESS(source, 0x1);
R_TRY(source->LoadFile(fs, path, flags));
@@ -1090,6 +1126,8 @@ Result OpenSong(fs::Fs* fs, const fs::FsPath& path, u32 flags, SongID* id) {
}
Result CloseSong(SongID* id) {
R_UNLESS(g_is_init, 0x1);
R_UNLESS(id && *id, 0x1);
auto e = static_cast<SongEntry*>(*id);
@@ -1104,6 +1142,8 @@ Result CloseSong(SongID* id) {
}
#define LockSongAndDo(cond_func, ...) do { \
R_UNLESS(g_is_init, 0x1); \
\
R_UNLESS(id, 0x1); \
auto e = static_cast<SongEntry*>(id); \
\

View File

@@ -0,0 +1,342 @@
#include "utils/devoptab_common.hpp"
#include "utils/thread.hpp"
#include "ui/sidebar.hpp"
#include "ui/popup_list.hpp"
#include "ui/option_box.hpp"
#include "app.hpp"
#include "defines.hpp"
#include "log.hpp"
#include "download.hpp"
#include "i18n.hpp"
#include <cstring>
#include <algorithm>
#include <fcntl.h>
#include <minIni.h>
#include <curl/curl.h>
namespace sphaira::devoptab {
namespace {
#define MOUNT_PATH "/config/sphaira/mount/"
using namespace sphaira::ui;
using namespace sphaira::devoptab::common;
// todo: support for disabling some / all mounts.
enum class DevoptabType {
HTTP,
FTP,
#ifdef ENABLE_DEVOPTAB_SFTP
SFTP,
#endif
NFS,
SMB,
WEBDAV,
};
struct TypeEntry {
const char* name;
const char* scheme;
long port;
DevoptabType type;
};
const TypeEntry TYPE_ENTRIES[] = {
{"HTTP", "http://", 80, DevoptabType::HTTP},
{"FTP", "ftp://", 21, DevoptabType::FTP},
#ifdef ENABLE_DEVOPTAB_SFTP
{"SFTP", "sftp://", 22, DevoptabType::SFTP},
#endif
{"NFS", "nfs://", 2049, DevoptabType::NFS},
{"SMB", "smb://", 445, DevoptabType::SMB},
{"WEBDAV", "webdav://", 80, DevoptabType::WEBDAV},
};
struct TypeConfig {
TypeEntry type;
MountConfig config;
};
using TypeConfigs = std::vector<TypeConfig>;
auto BuildIniPathFromType(DevoptabType type) -> fs::FsPath {
switch (type) {
case DevoptabType::HTTP: return MOUNT_PATH "/http.ini";
case DevoptabType::FTP: return MOUNT_PATH "/ftp.ini";
#ifdef ENABLE_DEVOPTAB_SFTP
case DevoptabType::SFTP: return MOUNT_PATH "/sftp.ini";
#endif
case DevoptabType::NFS: return MOUNT_PATH "/nfs.ini";
case DevoptabType::SMB: return MOUNT_PATH "/smb.ini";
case DevoptabType::WEBDAV: return MOUNT_PATH "/webdav.ini";
}
std::unreachable();
}
auto GetTypeName(const TypeConfig& type_config) -> std::string {
char name[128]{};
std::snprintf(name, sizeof(name), "[%s] %s", type_config.type.name, type_config.config.name.c_str());
return name;
}
void LoadAllConfigs(TypeConfigs& out_configs) {
out_configs.clear();
for (const auto& e : TYPE_ENTRIES) {
const auto ini_path = BuildIniPathFromType(e.type);
MountConfigs configs{};
LoadConfigsFromIni(ini_path, configs);
for (const auto& config : configs) {
out_configs.emplace_back(e, config);
}
}
}
struct DevoptabForm final : public FormSidebar {
// create new.
explicit DevoptabForm();
// modify existing.
explicit DevoptabForm(DevoptabType type, const MountConfig& config);
private:
void SetupButtons(bool type_change);
void UpdateSchemeURL();
private:
DevoptabType m_type{};
MountConfig m_config{};
SidebarEntryTextInput* m_name{};
SidebarEntryTextInput* m_url{};
SidebarEntryTextInput* m_port{};
// SidebarEntryTextInput* m_timeout{};
SidebarEntryTextInput* m_user{};
SidebarEntryTextInput* m_pass{};
SidebarEntryTextInput* m_dump_path{};
};
DevoptabForm::DevoptabForm(DevoptabType type, const MountConfig& config)
: FormSidebar{"Mount Creator"}
, m_type{type}
, m_config{config} {
SetupButtons(false);
}
DevoptabForm::DevoptabForm() : FormSidebar{"Mount Creator"} {
SetupButtons(true);
}
void DevoptabForm::UpdateSchemeURL() {
for (const auto& e : TYPE_ENTRIES) {
if (e.type == m_type) {
const auto scheme_start = m_url->GetValue().find("://");
if (scheme_start != std::string::npos) {
m_url->SetValue(e.scheme + m_url->GetValue().substr(scheme_start + 3));
} else if (m_url->GetValue().starts_with("://")) {
m_url->SetValue(e.scheme + m_url->GetValue().substr(3));
} else if (m_url->GetValue().empty()) {
m_url->SetValue(e.scheme);
}
m_port->SetNumValue(e.port);
break;
}
}
}
void DevoptabForm::SetupButtons(bool type_change) {
if (type_change) {
SidebarEntryArray::Items items;
for (const auto& e : TYPE_ENTRIES) {
items.emplace_back(e.name);
}
this->Add<SidebarEntryArray>(
"Type", items, [this](s64& index) {
m_type = TYPE_ENTRIES[index].type;
UpdateSchemeURL();
},
(s64)m_type,
"Select the type of the forwarder."_i18n
);
}
m_name = this->Add<SidebarEntryTextInput>(
"Name", m_config.name, "", "", -1, 32,
"Set the name of the application"_i18n
);
m_url = this->Add<SidebarEntryTextInput>(
"URL", m_config.url, "", "", -1, PATH_MAX,
"Set the URL of the application"_i18n
);
m_port = this->Add<SidebarEntryTextInput>(
"Port", m_config.port, "", "", 1, 5,
"Optional: Set the port of the server. If left empty, the default port for the protocol will be used."_i18n
);
#if 0
m_timeout = this->Add<SidebarEntryTextInput>(
"Timeout", m_config.timeout, "Timeout in milliseconds", 1, 5,
"Optional: Set the timeout in seconds."_i18n
);
#endif
m_user = this->Add<SidebarEntryTextInput>(
"User", m_config.user, "", "", -1, PATH_MAX,
"Optional: Set the username of the application"_i18n
);
m_pass = this->Add<SidebarEntryTextInput>(
"Pass", m_config.pass, "", "", -1, PATH_MAX,
"Optional: Set the password of the application"_i18n
);
m_dump_path = this->Add<SidebarEntryTextInput>(
"Dump path", m_config.dump_path, "", "", -1, PATH_MAX,
"Optional: Set the dump path used when exporting games and saves."_i18n
);
this->Add<SidebarEntryBool>(
"Read only", m_config.read_only,
"Mount the filesystem as read only.\n\n"
"Setting this option also hidens the mount from being show as an export option."_i18n
);
this->Add<SidebarEntryBool>(
"No stat file", m_config.no_stat_file,
"Enabling stops the file browser from checking the file size and timestamp of each file. "
"This improves browsing performance."_i18n
);
this->Add<SidebarEntryBool>(
"No stat dir", m_config.no_stat_dir,
"Enabling stops the file browser from checking how many files and folders are in a folder. "
"This improves browsing performance, especially for servers that has slow directory listing."_i18n
);
this->Add<SidebarEntryBool>(
"FS hidden", m_config.fs_hidden,
"Hide the mount from being visible in the file browser."_i18n
);
this->Add<SidebarEntryBool>(
"Export hidden", m_config.dump_hidden,
"Hide the mount from being visible as a export option for games and saves."_i18n
);
// set default scheme when creating a new entry.
if (type_change) {
UpdateSchemeURL();
}
const auto callback = this->Add<SidebarEntryCallback>("Save", [this](){
m_config.name = m_name->GetValue();
m_config.url = m_url->GetValue();
m_config.user = m_user->GetValue();
m_config.pass = m_pass->GetValue();
m_config.dump_path = m_dump_path->GetValue();
m_config.port = std::stoul(m_port->GetValue());
// m_config.timeout = m_timeout->GetValue();
const auto ini_path = BuildIniPathFromType(m_type);
fs::FsNativeSd().CreateDirectoryRecursively(MOUNT_PATH);
ini_puts(m_config.name.c_str(), "url", m_config.url.c_str(), ini_path);
ini_puts(m_config.name.c_str(), "user", m_config.user.c_str(), ini_path);
ini_puts(m_config.name.c_str(), "pass", m_config.pass.c_str(), ini_path);
ini_puts(m_config.name.c_str(), "dump_path", m_config.dump_path.c_str(), ini_path);
ini_putl(m_config.name.c_str(), "port", m_config.port, ini_path);
ini_putl(m_config.name.c_str(), "timeout", m_config.timeout, ini_path);
// todo: update minini to have put_bool.
ini_puts(m_config.name.c_str(), "read_only", m_config.read_only ? "true" : "false", ini_path);
ini_puts(m_config.name.c_str(), "no_stat_file", m_config.no_stat_file ? "true" : "false", ini_path);
ini_puts(m_config.name.c_str(), "no_stat_dir", m_config.no_stat_dir ? "true" : "false", ini_path);
ini_puts(m_config.name.c_str(), "fs_hidden", m_config.fs_hidden ? "true" : "false", ini_path);
ini_puts(m_config.name.c_str(), "dump_hidden", m_config.dump_hidden ? "true" : "false", ini_path);
App::Notify("Mount entry saved. Restart Sphaira to apply changes."_i18n);
this->SetPop();
}, "Saves the mount entry.\n\n"
"NOTE: You must restart Sphaira for changes to take effect!"_i18n);
// ensure that all fields are valid.
callback->Depends([this](){
return
!m_name->GetValue().empty() &&
!m_url->GetValue().empty() &&
!m_url->GetValue().ends_with("://");
}, "Name and URL must be set!"_i18n);
}
} // namespace
void DisplayDevoptabSideBar() {
auto options = std::make_unique<Sidebar>("Devoptab Options"_i18n, Sidebar::Side::LEFT);
ON_SCOPE_EXIT(App::Push(std::move(options)));
options->Add<SidebarEntryCallback>("Create New Entry"_i18n, [](){
App::Push<DevoptabForm>();
}, "Creates a new mount option.\n\n"
"NOTE: You must restart Sphaira for changes to take effect!"_i18n);
options->Add<SidebarEntryCallback>("Modify Existing Entry"_i18n, [](){
PopupList::Items items;
TypeConfigs configs;
LoadAllConfigs(configs);
for (const auto& e : configs) {
items.emplace_back(GetTypeName(e));
}
if (items.empty()) {
App::Notify("No mount entries found."_i18n);
return;
}
App::Push<PopupList>("Modify Entry"_i18n, items, [configs](std::optional<s64> index){
if (!index.has_value()) {
return;
}
const auto& entry = configs[index.value()];
App::Push<DevoptabForm>(entry.type.type, entry.config);
});
}, "Modify an existing mount option.\n\n"
"NOTE: You must restart Sphaira for changes to take effect!"_i18n);
options->Add<SidebarEntryCallback>("Delete Existing Entry"_i18n, [](){
PopupList::Items items;
TypeConfigs configs;
LoadAllConfigs(configs);
for (const auto& e : configs) {
items.emplace_back(GetTypeName(e));
}
if (items.empty()) {
App::Notify("No mount entries found."_i18n);
return;
}
App::Push<PopupList>("Delete Entry"_i18n, items, [configs](std::optional<s64> index){
if (!index.has_value()) {
return;
}
const auto& entry = configs[index.value()];
const auto ini_path = BuildIniPathFromType(entry.type.type);
ini_puts(entry.config.name.c_str(), nullptr, nullptr, ini_path);
});
}, "Delete an existing mount option.\n\n"
"NOTE: You must restart Sphaira for changes to take effect!"_i18n);
}
} // namespace sphaira::devoptab

View File

@@ -4,8 +4,6 @@
#include "defines.hpp"
#include "log.hpp"
#include "yati/container/nsp.hpp"
#include "yati/container/xci.hpp"
#include "yati/source/file.hpp"
#include <pulsar.h>
@@ -15,24 +13,16 @@
#include <array>
#include <memory>
#include <algorithm>
#include <sys/iosupport.h>
namespace sphaira::devoptab {
namespace {
struct Device {
PLSR_BFSAR bfsar;
std::FILE* file; // points to archive file.
};
struct File {
Device* device;
PLSR_BFWARFileInfo info;
size_t off;
};
struct Dir {
Device* device;
u32 index;
};
@@ -82,56 +72,69 @@ PLSR_RC GetFileInfo(const PLSR_BFSAR *bfsar, std::string_view path, PLSR_BFWARFi
}
int set_errno(struct _reent *r, int err) {
r->_errno = err;
return -1;
}
int devoptab_open(struct _reent *r, void *fileStruct, const char *_path, int flags, int mode) {
auto device = (Device*)r->deviceData;
auto file = static_cast<File*>(fileStruct);
std::memset(file, 0, sizeof(*file));
char path[FS_MAX_PATH];
if (!common::fix_path(_path, path)) {
return set_errno(r, ENOENT);
struct Device final : common::MountDevice {
Device(const PLSR_BFSAR& _bfsar, const common::MountConfig& _config)
: MountDevice{_config}
, bfsar{_bfsar} {
this->file = this->bfsar.ar.handle->f;
}
~Device() {
plsrBFSARClose(&bfsar);
}
private:
bool Mount() override { return true; }
int devoptab_open(void *fileStruct, const char *path, int flags, int mode) override;
int devoptab_close(void *fd) override;
ssize_t devoptab_read(void *fd, char *ptr, size_t len) override;
ssize_t devoptab_seek(void *fd, off_t pos, int dir) override;
int devoptab_fstat(void *fd, struct stat *st) override;
int devoptab_diropen(void* fd, const char *path) override;
int devoptab_dirreset(void* fd) override;
int devoptab_dirnext(void* fd, char *filename, struct stat *filestat) override;
int devoptab_dirclose(void* fd) override;
int devoptab_lstat(const char *path, struct stat *st) override;
private:
PLSR_BFSAR bfsar;
std::FILE* file; // points to archive file.
};
int Device::devoptab_open(void *fileStruct, const char *path, int flags, int mode) {
auto file = static_cast<File*>(fileStruct);
PLSR_BFWARFileInfo info;
if (R_FAILED(GetFileInfo(&device->bfsar, path, info))) {
return set_errno(r, ENOENT);
if (R_FAILED(GetFileInfo(&this->bfsar, path, info))) {
return -ENOENT;
}
file->device = device;
file->info = info;
return r->_errno = 0;
return 0;
}
int devoptab_close(struct _reent *r, void *fd) {
int Device::devoptab_close(void *fd) {
auto file = static_cast<File*>(fd);
std::memset(file, 0, sizeof(*file));
return r->_errno = 0;
return 0;
}
ssize_t devoptab_read(struct _reent *r, void *fd, char *ptr, size_t len) {
ssize_t Device::devoptab_read(void *fd, char *ptr, size_t len) {
auto file = static_cast<File*>(fd);
const auto& info = file->info;
// const auto real_len = len;
// plsr seems to read oob, so allow for some tollerance.
const auto oob_allowed = 64;
len = std::min(len, info.size + oob_allowed - file->off);
std::fseek(file->device->file, file->info.offset + file->off, SEEK_SET);
const auto bytes_read = std::fread(ptr, 1, len, file->device->file);
// log_write("bytes read: %zu len: %zu real_len: %zu off: %zu size: %u\n", bytes_read, len, real_len, file->off, info.size);
std::fseek(this->file, file->info.offset + file->off, SEEK_SET);
const auto bytes_read = std::fread(ptr, 1, len, this->file);
file->off += bytes_read;
return bytes_read;
}
off_t devoptab_seek(struct _reent *r, void *fd, off_t pos, int dir) {
ssize_t Device::devoptab_seek(void *fd, off_t pos, int dir) {
auto file = static_cast<File*>(fd);
const auto& info = file->info;
@@ -141,11 +144,10 @@ off_t devoptab_seek(struct _reent *r, void *fd, off_t pos, int dir) {
pos = info.size;
}
r->_errno = 0;
return file->off = std::clamp<u64>(pos, 0, info.size);
}
int devoptab_fstat(struct _reent *r, void *fd, struct stat *st) {
int Device::devoptab_fstat(void *fd, struct stat *st) {
auto file = static_cast<File*>(fd);
const auto& info = file->info;
@@ -153,51 +155,36 @@ int devoptab_fstat(struct _reent *r, void *fd, struct stat *st) {
st->st_nlink = 1;
st->st_size = info.size;
st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
return r->_errno = 0;
return 0;
}
DIR_ITER* devoptab_diropen(struct _reent *r, DIR_ITER *dirState, const char *_path) {
auto device = (Device*)r->deviceData;
auto dir = static_cast<Dir*>(dirState->dirStruct);
std::memset(dir, 0, sizeof(*dir));
char path[FS_MAX_PATH];
if (!common::fix_path(_path, path)) {
set_errno(r, ENOENT);
return NULL;
}
int Device::devoptab_diropen(void* fd, const char *path) {
if (!std::strcmp(path, "/")) {
dir->device = device;
} else {
set_errno(r, ENOENT);
return NULL;
return 0;
}
r->_errno = 0;
return dirState;
return -ENOENT;
}
int devoptab_dirreset(struct _reent *r, DIR_ITER *dirState) {
auto dir = static_cast<Dir*>(dirState->dirStruct);
int Device::devoptab_dirreset(void* fd) {
auto dir = static_cast<Dir*>(fd);
dir->index = 0;
return r->_errno = 0;
return 0;
}
int devoptab_dirnext(struct _reent *r, DIR_ITER *dirState, char *filename, struct stat *filestat) {
auto dir = static_cast<Dir*>(dirState->dirStruct);
std::memset(filestat, 0, sizeof(*filestat));
int Device::devoptab_dirnext(void* fd, char *filename, struct stat *filestat) {
auto dir = static_cast<Dir*>(fd);
do {
if (dir->index >= plsrBFSARSoundCount(&dir->device->bfsar)) {
log_write("finished getting call entries: %u vs %u\n", dir->index, plsrBFSARSoundCount(&dir->device->bfsar));
return set_errno(r, ENOENT);
if (dir->index >= plsrBFSARSoundCount(&this->bfsar)) {
log_write("finished getting call entries: %u vs %u\n", dir->index, plsrBFSARSoundCount(&this->bfsar));
return -ENOENT;
}
PLSR_BFSARSoundInfo info{};
if (R_FAILED(plsrBFSARSoundGet(&dir->device->bfsar, dir->index, &info))) {
if (R_FAILED(plsrBFSARSoundGet(&this->bfsar, dir->index, &info))) {
continue;
}
@@ -206,7 +193,7 @@ int devoptab_dirnext(struct _reent *r, DIR_ITER *dirState, char *filename, struc
continue;
}
if (R_FAILED(plsrBFSARStringGet(&dir->device->bfsar, info.stringIndex, filename, NAME_MAX))) {
if (R_FAILED(plsrBFSARStringGet(&this->bfsar, info.stringIndex, filename, NAME_MAX))) {
continue;
}
@@ -228,141 +215,54 @@ int devoptab_dirnext(struct _reent *r, DIR_ITER *dirState, char *filename, struc
filestat->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
dir->index++;
break;
} while (dir->index++);
} while (++dir->index);
return r->_errno = 0;
return 0;
}
int devoptab_dirclose(struct _reent *r, DIR_ITER *dirState) {
auto dir = static_cast<Dir*>(dirState->dirStruct);
int Device::devoptab_dirclose(void* fd) {
auto dir = static_cast<Dir*>(fd);
std::memset(dir, 0, sizeof(*dir));
log_write("[BFSAR] devoptab_dirclose\n");
return r->_errno = 0;
return 0;
}
int devoptab_lstat(struct _reent *r, const char *_path, struct stat *st) {
auto device = (Device*)r->deviceData;
char path[FS_MAX_PATH];
if (!common::fix_path(_path, path)) {
return set_errno(r, ENOENT);
}
std::memset(st, 0, sizeof(*st));
int Device::devoptab_lstat(const char *path, struct stat *st) {
st->st_nlink = 1;
if (!std::strcmp(path, "/")) {
st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
} else {
PLSR_BFWARFileInfo info{};
if (R_FAILED(GetFileInfo(&device->bfsar, path, info))) {
return set_errno(r, ENOENT);
if (R_FAILED(GetFileInfo(&this->bfsar, path, info))) {
return -ENOENT;
}
st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
st->st_size = info.size;
}
st->st_nlink = 1;
return r->_errno = 0;
return 0;
}
constexpr devoptab_t DEVOPTAB = {
.structSize = sizeof(File),
.open_r = devoptab_open,
.close_r = devoptab_close,
.read_r = devoptab_read,
.seek_r = devoptab_seek,
.fstat_r = devoptab_fstat,
.stat_r = devoptab_lstat,
.dirStateSize = sizeof(Dir),
.diropen_r = devoptab_diropen,
.dirreset_r = devoptab_dirreset,
.dirnext_r = devoptab_dirnext,
.dirclose_r = devoptab_dirclose,
.lstat_r = devoptab_lstat,
};
struct Entry {
Device device{};
devoptab_t devoptab{};
fs::FsPath path{};
fs::FsPath mount{};
char name[32]{};
s32 ref_count{};
~Entry() {
log_write("[BFSAR] entry called\n");
RemoveDevice(mount);
plsrBFSARClose(&device.bfsar);
}
};
Mutex g_mutex;
std::array<std::unique_ptr<Entry>, common::MAX_ENTRIES> g_entries;
} // namespace
Result MountBfsar(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path) {
SCOPED_MUTEX(&g_mutex);
PLSR_BFSAR bfsar{};
PLSR_RC_TRY(plsrBFSAROpen(path, &bfsar));
// check if we already have the save mounted.
for (auto& e : g_entries) {
if (e && e->path == path) {
e->ref_count++;
out_path = e->mount;
R_SUCCEED();
}
if (!common::MountReadOnlyIndexDevice(
[&bfsar](const common::MountConfig& config) {
return std::make_unique<Device>(bfsar, config);
},
sizeof(File), sizeof(Dir),
"BFSAR", out_path
)) {
log_write("[BFSAR] Failed to mount %s\n", path.s);
R_THROW(0x1);
}
// otherwise, find next free entry.
auto itr = std::ranges::find_if(g_entries, [](auto& e){
return !e;
});
R_UNLESS(itr != g_entries.end(), 0x1);
const auto index = std::distance(g_entries.begin(), itr);
auto entry = std::make_unique<Entry>();
entry->path = path;
entry->devoptab = DEVOPTAB;
entry->devoptab.name = entry->name;
entry->devoptab.deviceData = &entry->device;
std::snprintf(entry->name, sizeof(entry->name), "BFSAR_%zu", index);
std::snprintf(entry->mount, sizeof(entry->mount), "BFSAR_%zu:/", index);
PLSR_RC_TRY(plsrBFSAROpen(path, &entry->device.bfsar));
entry->device.file = entry->device.bfsar.ar.handle->f;
R_UNLESS(AddDevice(&entry->devoptab) >= 0, 0x1);
log_write("[BFSAR] DEVICE SUCCESS %s %s\n", path.s, entry->name);
out_path = entry->mount;
entry->ref_count++;
*itr = std::move(entry);
R_SUCCEED();
}
void UmountBfsar(const fs::FsPath& mount) {
SCOPED_MUTEX(&g_mutex);
auto itr = std::ranges::find_if(g_entries, [&mount](auto& e){
return e && e->mount == mount;
});
if (itr == g_entries.end()) {
return;
}
if ((*itr)->ref_count) {
(*itr)->ref_count--;
}
if (!(*itr)->ref_count) {
itr->reset();
}
}
} // namespace sphaira::devoptab

File diff suppressed because it is too large Load Diff

View File

@@ -1,27 +1,49 @@
#include "utils/devoptab.hpp"
#include "utils/devoptab_common.hpp"
#include "fatfs.hpp"
#include "defines.hpp"
#include "utils/profile.hpp"
#include "log.hpp"
#include "ff.h"
#include "defines.hpp"
#include <array>
#include <algorithm>
#include <span>
#include <fcntl.h>
#include <string>
#include <vector>
#include <memory>
#include <cstring>
#include <cstdio>
#include <cerrno>
#include <sys/iosupport.h>
#include <sys/stat.h>
#include <ff.h>
namespace sphaira::fatfs {
namespace sphaira::devoptab {
namespace {
auto is_archive(BYTE attr) -> bool {
const auto archive_attr = AM_DIR | AM_ARC;
return (attr & archive_attr) == archive_attr;
}
enum BisMountType {
BisMountType_PRODINFOF,
BisMountType_SAFE,
BisMountType_USER,
BisMountType_SYSTEM,
};
struct FatStorageEntry {
FsStorage storage;
std::unique_ptr<common::LruBufferedData> buffered;
FATFS fs;
};
struct BisMountEntry {
const FsBisPartitionId id;
const char* volume_name;
const char* mount_name;
};
constexpr BisMountEntry BIS_MOUNT_ENTRIES[] {
[BisMountType_PRODINFOF] = { FsBisPartitionId_CalibrationFile, "PRODINFOF", "PRODINFOF:/" },
[BisMountType_SAFE] = { FsBisPartitionId_SafeMode, "SAFE", "SAFE:/" },
[BisMountType_USER] = { FsBisPartitionId_User, "USER", "USER:/" },
[BisMountType_SYSTEM] = { FsBisPartitionId_System, "SYSTEM", "SYSTEM:/" },
};
static_assert(std::size(BIS_MOUNT_ENTRIES) == FF_VOLUMES);
FatStorageEntry g_fat_storage[FF_VOLUMES];
// todo: replace with off+size and have the data be in another struct
// in order to be more lcache efficient.
@@ -48,14 +70,51 @@ struct File {
FIL* files;
u32 file_count;
size_t off;
char path[256];
char path[PATH_MAX];
};
struct Dir {
FDIR dir;
char path[256];
char path[PATH_MAX];
};
struct Device final : common::MountDevice {
Device(BisMountType type, const common::MountConfig& _config)
: MountDevice{_config}
, m_type{type} {
}
~Device();
private:
bool fix_path(const char* str, char* out, bool strip_leading_slash = false) override {
std::strcpy(out, str);
return true;
}
bool Mount() override;
int devoptab_open(void *fileStruct, const char *path, int flags, int mode) override;
int devoptab_close(void *fd) override;
ssize_t devoptab_read(void *fd, char *ptr, size_t len) override;
ssize_t devoptab_seek(void *fd, off_t pos, int dir) override;
int devoptab_fstat(void *fd, struct stat *st) override;
int devoptab_diropen(void* fd, const char *path) override;
int devoptab_dirreset(void* fd) override;
int devoptab_dirnext(void* fd, char *filename, struct stat *filestat) override;
int devoptab_dirclose(void* fd) override;
int devoptab_lstat(const char *path, struct stat *st) override;
private:
const BisMountType m_type;
bool mounted{};
};
auto is_archive(BYTE attr) -> bool {
const auto archive_attr = AM_DIR | AM_ARC;
return (attr & archive_attr) == archive_attr;
}
u64 get_size_from_files(const File* file) {
u64 size = 0;
for (u32 i = 0; i < file->file_count; i++) {
@@ -90,38 +149,8 @@ void set_current_file_pos(File* file) {
}
}
enum BisMountType {
BisMountType_PRODINFOF,
BisMountType_SAFE,
BisMountType_USER,
BisMountType_SYSTEM,
};
struct FatStorageEntry {
FsStorage storage;
std::unique_ptr<devoptab::common::LruBufferedData> buffered;
FATFS fs;
devoptab_t devoptab;
};
struct BisMountEntry {
const FsBisPartitionId id;
const char* volume_name;
const char* mount_name;
};
constexpr BisMountEntry BIS_MOUNT_ENTRIES[] {
[BisMountType_PRODINFOF] = { FsBisPartitionId_CalibrationFile, "PRODINFOF", "PRODINFOF:/" },
[BisMountType_SAFE] = { FsBisPartitionId_SafeMode, "SAFE", "SAFE:/" },
[BisMountType_USER] = { FsBisPartitionId_User, "USER", "USER:/" },
[BisMountType_SYSTEM] = { FsBisPartitionId_System, "SYSTEM", "SYSTEM:/" },
};
static_assert(std::size(BIS_MOUNT_ENTRIES) == FF_VOLUMES);
FatStorageEntry g_fat_storage[FF_VOLUMES];
void fill_stat(const char* path, const FILINFO* fno, struct stat *st) {
memset(st, 0, sizeof(*st));
std::memset(st, 0, sizeof(*st));
st->st_nlink = 1;
@@ -133,7 +162,7 @@ void fill_stat(const char* path, const FILINFO* fno, struct stat *st) {
tm.tm_mon = ((fno->fdate >> 5) & 0xF) - 1;
tm.tm_year = (fno->fdate >> 9) + 80;
st->st_atime = mktime(&tm);
st->st_atime = std::mktime(&tm);
st->st_mtime = st->st_atime;
st->st_ctime = st->st_atime;
@@ -143,7 +172,7 @@ void fill_stat(const char* path, const FILINFO* fno, struct stat *st) {
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;
FILINFO file_info{};
if (FR_OK != f_stat(file_path, &file_info)) {
break;
}
@@ -152,8 +181,7 @@ void fill_stat(const char* path, const FILINFO* fno, struct stat *st) {
}
st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
} else
if (fno->fattrib & AM_DIR) {
} else if (fno->fattrib & AM_DIR) {
st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
} else {
st->st_size = fno->fsize;
@@ -161,31 +189,80 @@ void fill_stat(const char* path, const FILINFO* fno, struct stat *st) {
}
}
static int set_errno(struct _reent *r, int err) {
r->_errno = err;
return -1;
Device::~Device() {
if (mounted) {
auto& fat = g_fat_storage[m_type];
f_unmount(BIS_MOUNT_ENTRIES[m_type].mount_name);
fat.buffered.reset();
fsStorageClose(&fat.storage);
}
}
int fat_open(struct _reent *r, void *fileStruct, const char *path, int flags, int mode) {
bool Device::Mount() {
if (mounted) {
return true;
}
auto& fat = g_fat_storage[m_type];
if (!serviceIsActive(&fat.storage.s)) {
const auto res = fsOpenBisStorage(&fat.storage, BIS_MOUNT_ENTRIES[m_type].id);
if (R_FAILED(res)) {
log_write("[FATFS] fsOpenBisStorage(%d) failed: 0x%x\n", BIS_MOUNT_ENTRIES[m_type].id, res);
return false;
}
} else {
log_write("[FATFS] Storage for %s already opened\n", BIS_MOUNT_ENTRIES[m_type].mount_name);
}
if (!fat.buffered) {
auto source = std::make_shared<FsStorageSource>(&fat.storage);
s64 size;
if (R_FAILED(source->GetSize(&size))) {
log_write("[FATFS] Failed to get size of storage source\n");
return false;
}
fat.buffered = std::make_unique<common::LruBufferedData>(source, size);
if (!fat.buffered) {
log_write("[FATFS] Failed to create LruBufferedData\n");
return false;
}
}
if (FR_OK != f_mount(&fat.fs, BIS_MOUNT_ENTRIES[m_type].mount_name, 1)) {
log_write("[FATFS] f_mount(%s) failed\n", BIS_MOUNT_ENTRIES[m_type].mount_name);
return false;
}
log_write("[FATFS] Mounted %s at %s\n", BIS_MOUNT_ENTRIES[m_type].volume_name, BIS_MOUNT_ENTRIES[m_type].mount_name);
return mounted = true;
}
int Device::devoptab_open(void *fileStruct, const char *path, int flags, int mode) {
auto file = static_cast<File*>(fileStruct);
std::memset(file, 0, sizeof(*file));
// 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));
if (!file->files) {
return -ENOMEM;
}
file->file_count = 1;
std::memcpy(file->files, &fil, sizeof(*file->files));
// todo: check what error code is returned here.
} else {
FILINFO info{};
if (FR_OK != f_stat(path, &info)) {
return set_errno(r, ENOENT);
return -ENOENT;
}
if (!(info.fattrib & AM_ARC)) {
return set_errno(r, ENOENT);
return -ENOENT;
}
char file_path[256];
@@ -198,33 +275,35 @@ int fat_open(struct _reent *r, void *fileStruct, const char *path, int flags, in
}
file->files = (FIL*)std::realloc(file->files, (i + 1) * sizeof(*file->files));
if (!file->files) {
return -ENOMEM;
}
std::memcpy(&file->files[i], &fil, sizeof(fil));
file->file_count++;
}
}
if (!file->files) {
return set_errno(r, ENOENT);
return -ENOENT;
}
std::snprintf(file->path, sizeof(file->path), "%s", path);
return r->_errno = 0;
return 0;
}
int fat_close(struct _reent *r, void *fd) {
int Device::devoptab_close(void *fd) {
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);
for (u32 i = 0; i < file->file_count; i++) {
f_close(&file->files[i]);
}
return r->_errno = 0;
std::free(file->files);
return 0;
}
ssize_t fat_read(struct _reent *r, void *fd, char *ptr, size_t len) {
ssize_t Device::devoptab_read(void *fd, char *ptr, size_t len) {
auto file = static_cast<File*>(fd);
UINT total_bytes_read = 0;
@@ -233,11 +312,11 @@ ssize_t fat_read(struct _reent *r, void *fd, char *ptr, size_t len) {
auto fil = get_current_file(file);
if (!fil) {
log_write("[FATFS] failed to get fil\n");
return set_errno(r, ENOENT);
return -EIO;
}
if (FR_OK != f_read(fil, ptr, len, &bytes_read)) {
return set_errno(r, ENOENT);
return -EIO;
}
if (!bytes_read) {
@@ -252,7 +331,7 @@ ssize_t fat_read(struct _reent *r, void *fd, char *ptr, size_t len) {
return total_bytes_read;
}
off_t fat_seek(struct _reent *r, void *fd, off_t pos, int dir) {
ssize_t Device::devoptab_seek(void *fd, off_t pos, int dir) {
auto file = static_cast<File*>(fd);
const auto size = get_size_from_files(file);
@@ -265,11 +344,10 @@ off_t fat_seek(struct _reent *r, void *fd, off_t pos, int dir) {
file->off = std::clamp<u64>(pos, 0, size);
set_current_file_pos(file);
r->_errno = 0;
return file->off;
}
int fat_fstat(struct _reent *r, void *fd, struct stat *st) {
int Device::devoptab_fstat(void *fd, struct stat *st) {
auto file = static_cast<File*>(fd);
/* Only fill the attr and size field, leaving the timestamp blank. */
@@ -279,174 +357,118 @@ int fat_fstat(struct _reent *r, void *fd, struct stat *st) {
/* Fill stat info. */
fill_stat(nullptr, &info, st);
return r->_errno = 0;
return 0;
}
DIR_ITER* fat_diropen(struct _reent *r, DIR_ITER *dirState, const char *path) {
auto dir = static_cast<Dir*>(dirState->dirStruct);
std::memset(dir, 0, sizeof(*dir));
int Device::devoptab_diropen(void* fd, const char *path) {
auto dir = static_cast<Dir*>(fd);
log_write("[FATFS] diropen: %s\n", path);
if (FR_OK != f_opendir(&dir->dir, path)) {
set_errno(r, ENOENT);
return NULL;
log_write("[FATFS] f_opendir(%s) failed\n", path);
return -ENOENT;
}
r->_errno = 0;
return dirState;
log_write("[FATFS] Opened dir: %s\n", path);
return 0;
}
int fat_dirreset(struct _reent *r, DIR_ITER *dirState) {
auto dir = static_cast<Dir*>(dirState->dirStruct);
int Device::devoptab_dirreset(void* fd) {
auto dir = static_cast<Dir*>(fd);
if (FR_OK != f_rewinddir(&dir->dir)) {
return set_errno(r, ENOENT);
return -EIO;
}
return r->_errno = 0;
return 0;
}
int fat_dirnext(struct _reent *r, DIR_ITER *dirState, char *filename, struct stat *filestat) {
auto dir = static_cast<Dir*>(dirState->dirStruct);
int Device::devoptab_dirnext(void* fd, char *filename, struct stat *filestat) {
auto dir = static_cast<Dir*>(fd);
FILINFO fno{};
if (FR_OK != f_readdir(&dir->dir, &fno)) {
return set_errno(r, ENOENT);
return -EIO;
}
if (!fno.fname[0]) {
return set_errno(r, ENOENT);
return -EIO;
}
strcpy(filename, fno.fname);
std::strcpy(filename, fno.fname);
fill_stat(dir->path, &fno, filestat);
return r->_errno = 0;
return 0;
}
int fat_dirclose(struct _reent *r, DIR_ITER *dirState) {
auto dir = static_cast<Dir*>(dirState->dirStruct);
int Device::devoptab_dirclose(void* fd) {
auto dir = static_cast<Dir*>(fd);
if (FR_OK != f_closedir(&dir->dir)) {
return set_errno(r, ENOENT);
return -EIO;
}
return r->_errno = 0;
return 0;
}
int fat_statvfs(struct _reent *r, const char *path, struct statvfs *buf) {
memset(buf, 0, sizeof(*buf));
// todo: find out how to calculate free size in read only.
const auto fat = (FatStorageEntry*)r->deviceData;
buf->f_bsize = FF_MAX_SS;
buf->f_frsize = FF_MAX_SS;
buf->f_blocks = ((fat->fs.n_fatent - 2) * (DWORD)fat->fs.csize);
buf->f_namemax = FF_LFN_BUF;
return r->_errno = 0;
}
int fat_lstat(struct _reent *r, const char *file, struct stat *st) {
int Device::devoptab_lstat(const char *path, struct stat *st) {
FILINFO fno;
if (FR_OK != f_stat(file, &fno)) {
return set_errno(r, ENOENT);
if (FR_OK != f_stat(path, &fno)) {
return -ENOENT;
}
fill_stat(file, &fno, st);
return r->_errno = 0;
fill_stat(path, &fno, st);
return 0;
}
constexpr devoptab_t DEVOPTAB = {
.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(Dir),
.diropen_r = fat_diropen,
.dirreset_r = fat_dirreset,
.dirnext_r = fat_dirnext,
.dirclose_r = fat_dirclose,
.statvfs_r = fat_statvfs,
.lstat_r = fat_lstat,
};
Mutex g_mutex{};
bool g_is_init{};
} // namespace
Result MountAll() {
SCOPED_MUTEX(&g_mutex);
if (g_is_init) {
R_SUCCEED();
}
Result MountFatfsAll() {
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);
common::MountConfig config{};
config.read_only = true;
config.dump_hidden = true;
fat.devoptab = DEVOPTAB;
fat.devoptab.name = bis.volume_name;
fat.devoptab.deviceData = &fat;
R_TRY(fsOpenBisStorage(&fat.storage, bis.id));
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);
R_UNLESS(AddDevice(&fat.devoptab) >= 0, 0x1);
// log_write("[FAT] DEVICE SUCCESS %s\n", bis.volume_name);
if (!common::MountNetworkDevice2(
std::make_unique<Device>((BisMountType)i, config),
config,
sizeof(File), sizeof(Dir),
bis.volume_name, bis.mount_name
)) {
log_write("[FATFS] Failed to mount %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];
RemoveDevice(bis.mount_name);
f_unmount(bis.mount_name);
fsStorageClose(&fat.storage);
}
}
} // namespace sphaira::fatfs
} // namespace sphaira::devoptab
extern "C" {
const char* VolumeStr[] {
sphaira::fatfs::BIS_MOUNT_ENTRIES[0].volume_name,
sphaira::fatfs::BIS_MOUNT_ENTRIES[1].volume_name,
sphaira::fatfs::BIS_MOUNT_ENTRIES[2].volume_name,
sphaira::fatfs::BIS_MOUNT_ENTRIES[3].volume_name,
sphaira::devoptab::BIS_MOUNT_ENTRIES[0].volume_name,
sphaira::devoptab::BIS_MOUNT_ENTRIES[1].volume_name,
sphaira::devoptab::BIS_MOUNT_ENTRIES[2].volume_name,
sphaira::devoptab::BIS_MOUNT_ENTRIES[3].volume_name,
};
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];
auto& fat = sphaira::devoptab::g_fat_storage[num];
return fat.buffered->Read2(dst, offset, size);
}
// libusbhsfs also defines these, so only define if not using it.
#ifndef ENABLE_LIBUSBHSFS
void* ff_memalloc (UINT msize) {
return std::malloc(msize);
}
void ff_memfree (void* mblock) {
std::free(mblock);
}
#endif // ENABLE_LIBUSBHSFS
} // extern "C"

View File

@@ -0,0 +1,717 @@
#include "utils/devoptab_common.hpp"
#include "utils/profile.hpp"
#include "fs.hpp"
#include "log.hpp"
#include "defines.hpp"
#include <fcntl.h>
#include <curl/curl.h>
#include <string>
#include <vector>
#include <memory>
#include <cstring>
#include <optional>
#include <ctime>
#include <ranges>
#include <sys/stat.h>
namespace sphaira::devoptab {
namespace {
struct DirEntry {
std::string name{};
bool is_dir{};
};
using DirEntries = std::vector<DirEntry>;
struct FileEntry {
std::string path{};
struct stat st{};
};
struct Device final : common::MountCurlDevice {
using MountCurlDevice::MountCurlDevice;
private:
bool Mount() override;
int devoptab_open(void *fileStruct, const char *path, int flags, int mode) override;
int devoptab_close(void *fd) override;
ssize_t devoptab_read(void *fd, char *ptr, size_t len) override;
ssize_t devoptab_write(void *fd, const char *ptr, size_t len) override;
ssize_t devoptab_seek(void *fd, off_t pos, int dir) override;
int devoptab_fstat(void *fd, struct stat *st) override;
int devoptab_unlink(const char *path) override;
int devoptab_rename(const char *oldName, const char *newName) override;
int devoptab_mkdir(const char *path, int mode) override;
int devoptab_rmdir(const char *path) override;
int devoptab_diropen(void* fd, const char *path) override;
int devoptab_dirreset(void* fd) override;
int devoptab_dirnext(void* fd, char *filename, struct stat *filestat) override;
int devoptab_dirclose(void* fd) override;
int devoptab_lstat(const char *path, struct stat *st) override;
int devoptab_ftruncate(void *fd, off_t len) override;
int devoptab_fsync(void *fd) override;
void curl_set_common_options(CURL* curl, const std::string& url) override;
static bool ftp_parse_mlst_line(std::string_view line, struct stat* st, std::string* file_out, bool type_only);
static void ftp_parse_mlsd(std::string_view chunk, DirEntries& out);
static bool ftp_parse_mlist(std::string_view chunk, struct stat* st);
std::pair<bool, long> ftp_quote(std::span<const std::string> commands, bool is_dir, std::vector<char>* response_data = nullptr);
int ftp_dirlist(const std::string& path, DirEntries& out);
int ftp_stat(const std::string& path, struct stat* st, bool is_dir);
int ftp_remove_file_folder(const std::string& path, bool is_dir);
int ftp_unlink(const std::string& path);
int ftp_rename(const std::string& old_path, const std::string& new_path, bool is_dir);
int ftp_mkdir(const std::string& path);
int ftp_rmdir(const std::string& path);
private:
bool mounted{};
};
struct File {
FileEntry* entry;
common::PushPullThreadData* push_pull_thread_data;
size_t off;
size_t last_off;
bool write_mode;
bool append_mode;
};
struct Dir {
DirEntries* entries;
size_t index;
};
void Device::curl_set_common_options(CURL* curl, const std::string& url) {
MountCurlDevice::curl_set_common_options(curl, url);
curl_easy_setopt(curl, CURLOPT_FTP_CREATE_MISSING_DIRS, CURLFTP_CREATE_DIR_NONE);
curl_easy_setopt(curl, CURLOPT_FTP_FILEMETHOD, CURLFTPMETHOD_NOCWD);
}
bool Device::ftp_parse_mlst_line(std::string_view line, struct stat* st, std::string* file_out, bool type_only) {
// trim leading white space.
while (line.size() > 0 && std::isspace(line[0])) {
line = line.substr(1);
}
auto file_name_pos = line.rfind(';');
if (file_name_pos == std::string_view::npos || file_name_pos + 1 >= line.size()) {
return false;
}
// trim white space.
while (file_name_pos + 1 < line.size() && std::isspace(line[file_name_pos + 1])) {
file_name_pos++;
}
auto file_name = line.substr(file_name_pos + 1);
auto facts = line.substr(0, file_name_pos);
if (file_name.empty()) {
return false;
}
bool found_type = false;
while (!facts.empty()) {
const auto sep = facts.find(';');
if (sep == std::string_view::npos) {
break;
}
const auto fact = facts.substr(0, sep);
facts = facts.substr(sep + 1);
const auto eq = fact.find('=');
if (eq == std::string_view::npos || eq + 1 >= fact.size()) {
continue;
}
const auto key = fact.substr(0, eq);
const auto val = fact.substr(eq + 1);
if (fs::FsPath::path_equal(key, "type")) {
if (fs::FsPath::path_equal(val, "file")) {
st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
} else if (fs::FsPath::path_equal(val, "dir")) {
st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
} else {
log_write("[FTP] Unknown type fact value: %.*s\n", (int)val.size(), val.data());
return false;
}
found_type = true;
} else if (!type_only) {
if (fs::FsPath::path_equal(key, "size")) {
st->st_size = std::stoull(std::string(val));
} else if (fs::FsPath::path_equal(key, "modify")) {
if (val.size() >= 14) {
struct tm tm{};
tm.tm_year = std::stoi(std::string(val.substr(0, 4))) - 1900;
tm.tm_mon = std::stoi(std::string(val.substr(4, 2))) - 1;
tm.tm_mday = std::stoi(std::string(val.substr(6, 2)));
tm.tm_hour = std::stoi(std::string(val.substr(8, 2)));
tm.tm_min = std::stoi(std::string(val.substr(10, 2)));
tm.tm_sec = std::stoi(std::string(val.substr(12, 2)));
st->st_mtime = std::mktime(&tm);
st->st_atime = st->st_mtime;
st->st_ctime = st->st_mtime;
}
}
}
}
if (!found_type) {
log_write("[FTP] MLST line missing type fact\n");
return false;
}
st->st_nlink = 1;
if (file_out) {
*file_out = std::string(file_name.data(), file_name.size());
}
return true;
}
/*
C> MLst file1
S> 250- Listing file1
S> Type=file;Modify=19990929003355.237; file1
S> 250 End
*/
bool Device::ftp_parse_mlist(std::string_view chunk, struct stat* st) {
// sometimes the header data includes the full login exchange
// so we need to find the actual start of the MLST response.
const auto start_pos = chunk.find("250-");
const auto end_pos = chunk.rfind("\n250");
if (start_pos == std::string_view::npos || end_pos == std::string_view::npos) {
log_write("[FTP] MLST response missing start or end\n");
return false;
}
const auto end_line = chunk.find('\n', start_pos + 1);
if (end_line == std::string_view::npos || end_line > end_pos) {
log_write("[FTP] MLST response missing end line\n");
return false;
}
chunk = chunk.substr(end_line + 1, end_pos - (end_line + 1));
return ftp_parse_mlst_line(chunk, st, nullptr, false);
}
/*
C> MLSD tmp
S> 150 BINARY connection open for MLSD tmp
D> Type=cdir;Modify=19981107085215;Perm=el; tmp
D> Type=cdir;Modify=19981107085215;Perm=el; /tmp
D> Type=pdir;Modify=19990112030508;Perm=el; ..
D> Type=file;Size=25730;Modify=19940728095854;Perm=; capmux.tar.z
D> Type=file;Size=1024990;Modify=19980130010322;Perm=r; cap60.pl198.tar.gz
S> 226 MLSD completed
*/
void Device::ftp_parse_mlsd(std::string_view chunk, DirEntries& out) {
if (chunk.ends_with("\r\n")) {
chunk = chunk.substr(0, chunk.size() - 2);
} else if (chunk.ends_with('\n')) {
chunk = chunk.substr(0, chunk.size() - 1);
}
for (const auto line : std::views::split(chunk, '\n')) {
std::string_view line_str(line.data(), line.size());
if (line_str.empty() || line_str == "\r") {
continue;
}
DirEntry entry{};
struct stat st{};
if (!ftp_parse_mlst_line(line_str, &st, &entry.name, true)) {
log_write("[FTP] Failed to parse MLSD line: %.*s\n", (int)line.size(), line.data());
continue;
}
entry.is_dir = S_ISDIR(st.st_mode);
out.emplace_back(entry);
}
}
std::pair<bool, long> Device::ftp_quote(std::span<const std::string> commands, bool is_dir, std::vector<char>* response_data) {
const auto url = build_url("/", is_dir);
curl_slist* cmdlist{};
ON_SCOPE_EXIT(curl_slist_free_all(cmdlist));
for (const auto& cmd : commands) {
cmdlist = curl_slist_append(cmdlist, cmd.c_str());
}
curl_set_common_options(this->curl, url);
curl_easy_setopt(this->curl, CURLOPT_QUOTE, cmdlist);
curl_easy_setopt(this->curl, CURLOPT_NOBODY, 1L);
if (response_data) {
response_data->clear();
curl_easy_setopt(this->curl, CURLOPT_HEADERFUNCTION, write_memory_callback);
curl_easy_setopt(this->curl, CURLOPT_HEADERDATA, (void *)response_data);
}
const auto res = curl_easy_perform(this->curl);
if (res != CURLE_OK) {
log_write("[FTP] curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
return {false, 0};
}
long response_code = 0;
curl_easy_getinfo(this->curl, CURLINFO_RESPONSE_CODE, &response_code);
return {true, response_code};
}
int Device::ftp_dirlist(const std::string& path, DirEntries& out) {
const auto url = build_url(path, true);
std::vector<char> chunk;
curl_set_common_options(this->curl, url);
curl_easy_setopt(this->curl, CURLOPT_WRITEFUNCTION, write_memory_callback);
curl_easy_setopt(this->curl, CURLOPT_WRITEDATA, (void *)&chunk);
curl_easy_setopt(this->curl, CURLOPT_CUSTOMREQUEST, "MLSD");
const auto res = curl_easy_perform(this->curl);
if (res != CURLE_OK) {
log_write("[FTP] curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
return -EIO;
}
long response_code = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);
switch (response_code) {
case 125: // Data connection already open; transfer starting.
case 150: // File status okay; about to open data connection.
case 226: // Closing data connection. Requested file action successful.
break;
case 450: // Requested file action not taken. File unavailable (e.g., file busy).
case 550: // Requested action not taken. File unavailable (e.g., file not found, no access).
return -ENOENT;
default:
return -EIO;
}
ftp_parse_mlsd({chunk.data(), chunk.size()}, out);
return 0;
}
int Device::ftp_stat(const std::string& path, struct stat* st, bool is_dir) {
std::memset(st, 0, sizeof(*st));
std::vector<char> chunk;
const auto [success, response_code] = ftp_quote({"MLST " + path}, is_dir, &chunk);
if (!success) {
return -EIO;
}
switch (response_code) {
case 250: // Requested file action okay, completed.
break;
case 450: // Requested file action not taken. File unavailable (e.g., file busy).
case 550: // Requested action not taken. File unavailable (e.g., file not found, no access).
return -ENOENT;
default:
return -EIO;
}
if (!ftp_parse_mlist({chunk.data(), chunk.size()}, st)) {
log_write("[FTP] Failed to parse MLST response for path: %s\n", path.c_str());
return -EIO;
}
return 0;
}
int Device::ftp_remove_file_folder(const std::string& path, bool is_dir) {
const auto cmd = (is_dir ? "RMD " : "DELE ") + path;
const auto [success, response_code] = ftp_quote({cmd}, is_dir);
if (!success) {
return -EIO;
}
switch (response_code) {
case 250: // Requested file action okay, completed.
case 200: // Command okay.
break;
case 450: // Requested file action not taken. File unavailable (e.g., file busy).
case 550: // Requested action not taken. File unavailable (e.g., file not found, no access).
return -ENOENT;
default:
return -EIO;
}
return 0;
}
int Device::ftp_unlink(const std::string& path) {
return ftp_remove_file_folder(path, false);
}
int Device::ftp_rename(const std::string& old_path, const std::string& new_path, bool is_dir) {
const auto url = build_url("/", is_dir);
std::vector<std::string> commands;
commands.emplace_back("RNFR " + old_path);
commands.emplace_back("RNTO " + new_path);
const auto [success, response_code] = ftp_quote(commands, is_dir);
if (!success) {
return -EIO;
}
switch (response_code) {
case 250: // Requested file action okay, completed.
case 200: // Command okay.
break;
case 450: // Requested file action not taken. File unavailable (e.g., file busy).
case 550: // Requested action not taken. File unavailable (e.g., file not found, no access).
return -ENOENT;
case 553: // Requested action not taken. File name not allowed.
return -EEXIST;
default:
return -EIO;
}
return 0;
}
int Device::ftp_mkdir(const std::string& path) {
std::vector<char> chunk;
const auto [success, response_code] = ftp_quote({"MKD " + path}, true);
if (!success) {
return -EIO;
}
switch (response_code) {
case 257: // "PATHNAME" created.
case 250: // Requested file action okay, completed.
case 200: // Command okay.
break;
case 550: // Requested action not taken. File unavailable (e.g., file not found, no access).
return -ENOENT; // Parent directory does not exist or no permission.
case 521: // Directory already exists.
return -EEXIST;
default:
return -EIO;
}
return 0;
}
int Device::ftp_rmdir(const std::string& path) {
return ftp_remove_file_folder(path, true);
}
bool Device::Mount() {
if (mounted) {
return true;
}
if (!MountCurlDevice::Mount()) {
return false;
}
// issue FEAT command to see if we support MLST/MLSD.
std::vector<char> chunk;
const auto [success, response_code] = ftp_quote({"FEAT"}, true, &chunk);
if (!success || response_code != 211) {
log_write("[FTP] FEAT command failed with response code: %ld\n", response_code);
return false;
}
std::string_view view(chunk.data(), chunk.size());
// check for MLST/MLSD support.
// NOTE: RFC 3659 states that servers must support MLSD if they support MLST.
if (view.find("MLST") == std::string_view::npos) {
log_write("[FTP] Server does not support MLST/MLSD commands\n");
return false;
}
// if we support UTF8, enable it.
if (view.find("UTF8") != std::string_view::npos) {
// it doesn't matter if this fails tbh.
// also, i am not sure if this persists between logins or not...
ftp_quote({"OPTS UTF8 ON"}, true);
}
return this->mounted = true;
}
int Device::devoptab_open(void *fileStruct, const char *path, int flags, int mode) {
auto file = static_cast<File*>(fileStruct);
struct stat st{};
if ((flags & O_ACCMODE) == O_RDONLY || (flags & O_APPEND)) {
// ensure the file exists and get its size.
const auto ret = ftp_stat(path, &st, false);
if (ret < 0) {
return ret;
}
if (st.st_mode & S_IFDIR) {
log_write("[FTP] Path is a directory, not a file: %s\n", path);
return -EISDIR;
}
}
file->entry = new FileEntry{path, st};
file->write_mode = (flags & (O_WRONLY | O_RDWR));
file->append_mode = (flags & O_APPEND);
if (file->append_mode) {
file->off = st.st_size;
file->last_off = file->off;
}
return 0;
}
int Device::devoptab_close(void *fd) {
auto file = static_cast<File*>(fd);
delete file->push_pull_thread_data;
delete file->entry;
return 0;
}
ssize_t Device::devoptab_read(void *fd, char *ptr, size_t len) {
auto file = static_cast<File*>(fd);
len = std::min(len, file->entry->st.st_size - file->off);
if (file->write_mode) {
log_write("[FTP] Attempt to read from a write-only file\n");
return -EBADF;
}
if (!len) {
return 0;
}
if (file->off != file->last_off) {
log_write("[FTP] File offset changed from %zu to %zu, resetting download thread\n", file->last_off, file->off);
file->last_off = file->off;
delete file->push_pull_thread_data;
file->push_pull_thread_data = nullptr;
}
if (!file->push_pull_thread_data) {
log_write("[FTP] Creating download thread data for file: %s\n", file->entry->path.c_str());
file->push_pull_thread_data = CreatePushData(this->transfer_curl, build_url(file->entry->path, false), file->off);
if (!file->push_pull_thread_data) {
log_write("[FTP] Failed to create download thread data for file: %s\n", file->entry->path.c_str());
return -EIO;
}
}
const auto ret = file->push_pull_thread_data->PullData(ptr, len);
file->off += ret;
file->last_off = file->off;
return ret;
}
ssize_t Device::devoptab_write(void *fd, const char *ptr, size_t len) {
auto file = static_cast<File*>(fd);
if (!file->write_mode) {
log_write("[FTP] Attempt to write to a read-only file\n");
return -EBADF;
}
if (!len) {
return 0;
}
if (!file->push_pull_thread_data) {
log_write("[FTP] Creating upload thread data for file: %s\n", file->entry->path.c_str());
file->push_pull_thread_data = CreatePullData(this->transfer_curl, build_url(file->entry->path, false), file->append_mode);
if (!file->push_pull_thread_data) {
log_write("[FTP] Failed to create upload thread data for file: %s\n", file->entry->path.c_str());
return -EIO;
}
}
const auto ret = file->push_pull_thread_data->PushData(ptr, len);
file->off += ret;
file->entry->st.st_size = std::max<off_t>(file->entry->st.st_size, file->off);
return ret;
}
ssize_t Device::devoptab_seek(void *fd, off_t pos, int dir) {
auto file = static_cast<File*>(fd);
if (dir == SEEK_CUR) {
pos += file->off;
} else if (dir == SEEK_END) {
pos = file->entry->st.st_size;
}
// for now, random access writes are disabled.
if (file->write_mode && pos != file->off) {
log_write("[FTP] Random access writes are not supported\n");
return file->off;
}
return file->off = std::clamp<u64>(pos, 0, file->entry->st.st_size);
}
int Device::devoptab_fstat(void *fd, struct stat *st) {
auto file = static_cast<File*>(fd);
std::memcpy(st, &file->entry->st, sizeof(*st));
return 0;
}
int Device::devoptab_unlink(const char *path) {
const auto ret = ftp_unlink(path);
if (ret < 0) {
log_write("[FTP] ftp_unlink() failed: %s errno: %s\n", path, std::strerror(-ret));
return ret;
}
return 0;
}
int Device::devoptab_rename(const char *oldName, const char *newName) {
auto ret = ftp_rename(oldName, newName, false);
if (ret == -ENOENT) {
ret = ftp_rename(oldName, newName, true);
}
if (ret < 0) {
log_write("[FTP] ftp_rename() failed: %s -> %s errno: %s\n", oldName, newName, std::strerror(-ret));
return ret;
}
return 0;
}
int Device::devoptab_mkdir(const char *path, int mode) {
const auto ret = ftp_mkdir(path);
if (ret < 0) {
log_write("[FTP] ftp_mkdir() failed: %s errno: %s\n", path, std::strerror(-ret));
return ret;
}
return 0;
}
int Device::devoptab_rmdir(const char *path) {
const auto ret = ftp_rmdir(path);
if (ret < 0) {
log_write("[FTP] ftp_rmdir() failed: %s errno: %s\n", path, std::strerror(-ret));
return ret;
}
return 0;
}
int Device::devoptab_diropen(void* fd, const char *path) {
auto dir = static_cast<Dir*>(fd);
auto entries = new DirEntries();
const auto ret = ftp_dirlist(path, *entries);
if (ret < 0) {
log_write("[FTP] ftp_dirlist() failed: %s errno: %s\n", path, std::strerror(-ret));
delete entries;
return ret;
}
dir->entries = entries;
return 0;
}
int Device::devoptab_dirreset(void* fd) {
auto dir = static_cast<Dir*>(fd);
dir->index = 0;
return 0;
}
int Device::devoptab_dirnext(void* fd, char *filename, struct stat *filestat) {
auto dir = static_cast<Dir*>(fd);
if (dir->index >= dir->entries->size()) {
return -ENOENT;
}
auto& entry = (*dir->entries)[dir->index];
if (entry.is_dir) {
filestat->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
} else {
filestat->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
}
filestat->st_nlink = 1;
std::strcpy(filename, entry.name.c_str());
dir->index++;
return 0;
}
int Device::devoptab_dirclose(void* fd) {
auto dir = static_cast<Dir*>(fd);
delete dir->entries;
return 0;
}
int Device::devoptab_lstat(const char *path, struct stat *st) {
auto ret = ftp_stat(path, st, false);
if (ret == -ENOENT) {
ret = ftp_stat(path, st, true);
}
if (ret < 0) {
log_write("[FTP] ftp_stat() failed: %s errno: %s\n", path, std::strerror(-ret));
return ret;
}
return 0;
}
int Device::devoptab_ftruncate(void *fd, off_t len) {
auto file = static_cast<File*>(fd);
if (!file->write_mode) {
log_write("[FTP] Attempt to truncate a read-only file\n");
return -EBADF;
}
file->entry->st.st_size = len;
return 0;
}
int Device::devoptab_fsync(void *fd) {
auto file = static_cast<File*>(fd);
if (!file->write_mode) {
log_write("[FTP] Attempt to fsync a read-only file\n");
return -EBADF;
}
return 0;
}
} // namespace
Result MountFtpAll() {
return common::MountNetworkDevice([](const common::MountConfig& config) {
return std::make_unique<Device>(config);
},
sizeof(File), sizeof(Dir),
"FTP"
);
}
} // namespace sphaira::devoptab

View File

@@ -0,0 +1,499 @@
#include "utils/devoptab.hpp"
#include "utils/devoptab_common.hpp"
#include "defines.hpp"
#include "log.hpp"
#include "title_info.hpp"
#include "ui/menus/game_menu.hpp"
#include "yati/nx/es.hpp"
#include "yati/nx/ns.hpp"
#include <cstring>
#include <array>
#include <memory>
#include <algorithm>
namespace sphaira::devoptab {
namespace {
namespace game = ui::menu::game;
struct ContentEntry {
NsApplicationContentMetaStatus status{};
std::unique_ptr<game::NspEntry> nsp{};
};
struct Entry final : game::Entry {
std::string name{};
std::vector<ContentEntry> contents{};
};
struct File {
game::NspEntry* nsp;
size_t off;
};
struct Dir {
Entry* entry;
u32 index;
};
void ParseId(std::string_view path, u64& id_out) {
id_out = 0;
const auto start = path.find_first_of('[');
const auto end = path.find_first_of(']', start);
if (start != std::string_view::npos && end != std::string_view::npos && end > start + 1) {
// doesn't alloc because of SSO which is 32 bytes.
const std::string hex_str{path.substr(start + 1, end - start - 1)};
id_out = std::stoull(hex_str, nullptr, 16);
}
}
void ParseIds(std::string_view path, u64& app_id, u64& id) {
app_id = 0;
id = 0;
// strip leading slashes (should only be one anyway).
while (path.starts_with('/')) {
path.remove_prefix(1);
}
// find dir/path.nsp seperator.
const auto dir = path.find('/');
if (dir != std::string_view::npos) {
const auto folder = path.substr(0, dir);
const auto file = path.substr(dir + 1);
ParseId(folder, app_id);
ParseId(file, id);
} else {
ParseId(path, app_id);
}
}
struct Device final : common::MountDevice {
Device(const common::MountConfig& _config)
: MountDevice{_config} {
}
~Device();
private:
bool Mount() override;
int devoptab_open(void *fileStruct, const char *path, int flags, int mode) override;
int devoptab_close(void *fd) override;
ssize_t devoptab_read(void *fd, char *ptr, size_t len) override;
ssize_t devoptab_seek(void *fd, off_t pos, int dir) override;
int devoptab_fstat(void *fd, struct stat *st) override;
int devoptab_diropen(void* fd, const char *path) override;
int devoptab_dirreset(void* fd) override;
int devoptab_dirnext(void* fd, char *filename, struct stat *filestat) override;
int devoptab_dirclose(void* fd) override;
int devoptab_lstat(const char *path, struct stat *st) override;
game::NspEntry* FindNspFromEntry(Entry& entry, u64 id) const;
Entry* FindEntry(u64 app_id);
Result LoadMetaEntries(Entry& entry) const;
private:
std::vector<Entry> m_entries{};
keys::Keys m_keys{};
bool m_title_init{};
bool m_es_init{};
bool m_ns_init{};
bool m_keys_init{};
bool m_mounted{};
};
Device::~Device() {
if (m_title_init) {
title::Exit();
}
if (m_es_init) {
es::Exit();
}
if (m_ns_init) {
ns::Exit();
}
}
Result Device::LoadMetaEntries(Entry& entry) const {
// check if we have already loaded the meta entries.
if (!entry.contents.empty()) {
R_SUCCEED();
}
title::MetaEntries entry_status{};
R_TRY(title::GetMetaEntries(entry.app_id, entry_status, title::ContentFlag_All));
for (const auto& status : entry_status) {
entry.contents.emplace_back(status);
}
R_SUCCEED();
}
game::NspEntry* Device::FindNspFromEntry(Entry& entry, u64 id) const {
// load all meta entries if not yet loaded.
if (R_FAILED(LoadMetaEntries(entry))) {
log_write("[GAME] failed to load meta entries for app id: %016lx\n", entry.app_id);
return nullptr;
}
// try and find the matching nsp entry.
for (auto& content : entry.contents) {
if (content.status.application_id == id) {
// build nsp entry if not yet built.
if (!content.nsp) {
game::ContentInfoEntry info;
if (R_FAILED(game::BuildContentEntry(content.status, info))) {
log_write("[GAME] failed to build content info for app id: %016lx\n", entry.app_id);
return nullptr;
}
content.nsp = std::make_unique<game::NspEntry>();
if (R_FAILED(game::BuildNspEntry(entry, info, m_keys, *content.nsp))) {
log_write("[GAME] failed to build nsp entry for app id: %016lx\n", entry.app_id);
content.nsp.reset();
return nullptr;
}
// update path to strip the folder, if it has one.
const auto slash = std::strchr(content.nsp->path, '/');
if (slash) {
std::memmove(content.nsp->path, slash + 1, std::strlen(slash));
}
}
return content.nsp.get();
}
}
log_write("[GAME] failed to find content for id: %016lx\n", id);
return nullptr;
}
Entry* Device::FindEntry(u64 app_id) {
for (auto& entry : m_entries) {
if (entry.app_id == app_id) {
// the error doesn't matter here, the fs will just report an empty dir.
LoadMetaEntries(entry);
return &entry;
}
}
log_write("[GAME] failed to find entry for app id: %016lx\n", app_id);
return nullptr;
}
bool Device::Mount() {
if (m_mounted) {
return true;
}
log_write("[GAME] Mounting...\n");
if (!m_title_init) {
if (R_FAILED(title::Init())) {
log_write("[GAME] Failed to init title info\n");
return false;
}
m_title_init = true;
}
if (!m_es_init) {
if (R_FAILED(es::Initialize())) {
log_write("[GAME] Failed to init es\n");
return false;
}
m_es_init = true;
}
if (!m_ns_init) {
if (R_FAILED(ns::Initialize())) {
log_write("[GAME] Failed to init ns\n");
return false;
}
m_ns_init = true;
}
if (!m_keys_init) {
keys::parse_keys(m_keys, true);
}
if (m_entries.empty()) {
m_entries.reserve(1000);
std::vector<NsApplicationRecord> record_list(1000);
s32 offset{};
while (true) {
s32 record_count{};
if (R_FAILED(nsListApplicationRecord(record_list.data(), record_list.size(), offset, &record_count))) {
log_write("failed to list application records at offset: %d\n", offset);
}
// finished parsing all entries.
if (!record_count) {
break;
}
title::PushAsync(std::span(record_list.data(), record_count));
for (s32 i = 0; i < record_count; i++) {
const auto& e = record_list[i];
m_entries.emplace_back(game::Entry{e.application_id, e.last_event});
}
offset += record_count;
}
}
log_write("[GAME] mounted with %zu entries\n", m_entries.size());
m_mounted = true;
return true;
}
int Device::devoptab_open(void *fileStruct, const char *path, int flags, int mode) {
auto file = static_cast<File*>(fileStruct);
u64 app_id{}, id{};
ParseIds(path, app_id, id);
if (!app_id || !id) {
log_write("[GAME] invalid path %s\n", path);
return -ENOENT;
}
auto entry = FindEntry(app_id);
if (!entry) {
log_write("[GAME] failed to find entry for app id: %016lx\n", app_id);
return -ENOENT;
}
// try and find the matching nsp entry.
auto nsp = FindNspFromEntry(*entry, id);
if (!nsp) {
log_write("[GAME] failed to find nsp for content id: %016lx\n", id);
return -ENOENT;
}
file->nsp = nsp;
return 0;
}
int Device::devoptab_close(void *fd) {
auto file = static_cast<File*>(fd);
std::memset(file, 0, sizeof(*file));
return 0;
}
ssize_t Device::devoptab_read(void *fd, char *ptr, size_t len) {
auto file = static_cast<File*>(fd);
const auto& nsp = file->nsp;
len = std::min<u64>(len, nsp->nsp_size - file->off);
if (!len) {
return 0;
}
u64 bytes_read;
if (R_FAILED(nsp->Read(ptr, file->off, len, &bytes_read))) {
log_write("[GAME] failed to read from nsp %s off: %zu len: %zu size: %zu\n", nsp->path.s, file->off, len, nsp->nsp_size);
return -EIO;
}
file->off += bytes_read;
return bytes_read;
}
ssize_t Device::devoptab_seek(void *fd, off_t pos, int dir) {
auto file = static_cast<File*>(fd);
const auto& nsp = file->nsp;
if (dir == SEEK_CUR) {
pos += file->off;
} else if (dir == SEEK_END) {
pos = nsp->nsp_size;
}
return file->off = std::clamp<u64>(pos, 0, nsp->nsp_size);
}
int Device::devoptab_fstat(void *fd, struct stat *st) {
auto file = static_cast<File*>(fd);
const auto& nsp = file->nsp;
st->st_nlink = 1;
st->st_size = nsp->nsp_size;
st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
return 0;
}
int Device::devoptab_diropen(void* fd, const char *path) {
auto dir = static_cast<Dir*>(fd);
if (!std::strcmp(path, "/")) {
return 0;
} else {
u64 app_id{}, id{};
ParseIds(path, app_id, id);
if (!app_id || id) {
log_write("[GAME] invalid folder path %s\n", path);
return -ENOENT;
}
auto entry = FindEntry(app_id);
if (!entry) {
log_write("[GAME] failed to find entry for app id: %016lx\n", app_id);
return -ENOENT;
}
dir->entry = entry;
return 0;
}
}
int Device::devoptab_dirreset(void* fd) {
auto dir = static_cast<Dir*>(fd);
dir->index = 0;
return 0;
}
int Device::devoptab_dirnext(void* fd, char *filename, struct stat *filestat) {
auto dir = static_cast<Dir*>(fd);
if (!dir->entry) {
if (dir->index >= m_entries.size()) {
log_write("[GAME] dirnext: no more entries\n");
return -ENOENT;
}
auto& entry = m_entries[dir->index];
if (entry.status == title::NacpLoadStatus::None) {
// this will never be null as it blocks until a valid entry is loaded.
auto result = title::Get(entry.app_id);
entry.lang = result->lang;
entry.status = result->status;
char name[NAME_MAX]{};
if (result->status == title::NacpLoadStatus::Loaded) {
fs::FsPath name_buf = result->lang.name;
title::utilsReplaceIllegalCharacters(name_buf, true);
const int name_max = sizeof(name) - 33;
std::snprintf(name, sizeof(name), "%.*s [%016lX]", name_max, name_buf.s, entry.app_id);
} else {
std::snprintf(name, sizeof(name), "[%016lX]", entry.app_id);
log_write("[GAME] failed to get title info for %s\n", name);
}
entry.name = name;
}
filestat->st_nlink = 1;
filestat->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
std::strcpy(filename, entry.name.c_str());
dir->index++;
} else {
auto& entry = dir->entry;
do {
if (dir->index >= entry->contents.size()) {
log_write("[GAME] dirnext: no more entries\n");
return -ENOENT;
}
const auto& content = entry->contents[dir->index];
if (!content.nsp) {
if (!FindNspFromEntry(*entry, content.status.application_id)) {
log_write("[GAME] failed to find nsp for content id: %016lx\n", content.status.application_id);
continue;
}
}
filestat->st_nlink = 1;
filestat->st_size = content.nsp->nsp_size;
filestat->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
std::snprintf(filename, NAME_MAX, "%s", content.nsp->path.s);
dir->index++;
break;
} while (++dir->index);
}
return 0;
}
int Device::devoptab_dirclose(void* fd) {
auto dir = static_cast<Dir*>(fd);
std::memset(dir, 0, sizeof(*dir));
return 0;
}
int Device::devoptab_lstat(const char *path, struct stat *st) {
st->st_nlink = 1;
if (!std::strcmp(path, "/")) {
st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
return 0;
} else {
u64 app_id{}, id{};
ParseIds(path, app_id, id);
if (!app_id) {
log_write("[GAME] invalid path %s\n", path);
return -ENOENT;
}
auto entry = FindEntry(app_id);
if (!entry) {
log_write("[GAME] failed to find entry for app id: %016lx\n", app_id);
return -ENOENT;
}
if (!id) {
st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
return 0;
}
auto nsp = FindNspFromEntry(*entry, id);
if (!nsp) {
log_write("[GAME] failed to find nsp for content id: %016lx\n", id);
return -ENOENT;
}
st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
st->st_size = nsp->nsp_size;
return 0;
}
}
} // namespace
Result MountGameAll() {
common::MountConfig config{};
config.read_only = true;
config.dump_hidden = true;
config.no_stat_file = false;;
if (!common::MountNetworkDevice2(
std::make_unique<Device>(config),
config,
sizeof(File), sizeof(Dir),
"games", "games:/"
)) {
log_write("[GAME] Failed to mount GAME\n");
R_THROW(0x1);
}
R_SUCCEED();
}
} // namespace sphaira::devoptab

View File

@@ -0,0 +1,443 @@
#include "utils/devoptab_common.hpp"
#include "utils/profile.hpp"
#include "location.hpp"
#include "log.hpp"
#include "defines.hpp"
#include <sys/iosupport.h>
#include <fcntl.h>
#include <curl/curl.h>
#include <minIni.h>
#include <string>
#include <vector>
#include <memory>
#include <cstring>
#include <optional>
#include <sys/stat.h>
namespace sphaira::devoptab {
namespace {
struct DirEntry {
// deprecated because the names can be truncated and really set to anything.
std::string name_deprecated{};
// url decoded href.
std::string href{};
bool is_dir{};
};
using DirEntries = std::vector<DirEntry>;
struct FileEntry {
std::string path{};
struct stat st{};
};
struct File {
FileEntry* entry;
common::PushPullThreadData* push_pull_thread_data;
size_t off;
size_t last_off;
};
struct Dir {
DirEntries* entries;
size_t index;
};
struct Device final : common::MountCurlDevice {
using MountCurlDevice::MountCurlDevice;
private:
bool Mount() override;
int devoptab_open(void *fileStruct, const char *path, int flags, int mode) override;
int devoptab_close(void *fd) override;
ssize_t devoptab_read(void *fd, char *ptr, size_t len) override;
ssize_t devoptab_seek(void *fd, off_t pos, int dir) override;
int devoptab_fstat(void *fd, struct stat *st) override;
int devoptab_diropen(void* fd, const char *path) override;
int devoptab_dirreset(void* fd) override;
int devoptab_dirnext(void* fd, char *filename, struct stat *filestat) override;
int devoptab_dirclose(void* fd) override;
int devoptab_lstat(const char *path, struct stat *st) override;
int http_dirlist(const std::string& path, DirEntries& out);
int http_stat(const std::string& path, struct stat* st, bool is_dir);
private:
bool mounted{};
};
int Device::http_dirlist(const std::string& path, DirEntries& out) {
const auto url = build_url(path, true);
std::vector<char> chunk;
log_write("[HTTP] Listing URL: %s path: %s\n", url.c_str(), path.c_str());
curl_set_common_options(this->curl, url);
curl_easy_setopt(this->curl, CURLOPT_WRITEFUNCTION, write_memory_callback);
curl_easy_setopt(this->curl, CURLOPT_WRITEDATA, (void *)&chunk);
const auto res = curl_easy_perform(this->curl);
if (res != CURLE_OK) {
log_write("[HTTP] curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
return -EIO;
}
long response_code = 0;
curl_easy_getinfo(this->curl, CURLINFO_RESPONSE_CODE, &response_code);
switch (response_code) {
case 200: // OK
case 206: // Partial Content
break;
case 301: // Moved Permanently
case 302: // Found
case 303: // See Other
case 307: // Temporary Redirect
case 308: // Permanent Redirect
return -EIO;
case 401: // Unauthorized
case 403: // Forbidden
return -EACCES;
case 404: // Not Found
return -ENOENT;
default:
return -EIO;
}
log_write("[HTTP] Received %zu bytes for directory listing\n", chunk.size());
SCOPED_TIMESTAMP("http_dirlist parse");
// very fast/basic html parsing.
// takes 17ms to parse 3MB html with 7641 entries.
// todo: if i ever add an xml parser to sphaira, use that instead.
// todo: for the above, benchmark the parser to ensure its faster than the my code.
std::string_view chunk_view{chunk.data(), chunk.size()};
const auto body_start = chunk_view.find("<body");
const auto body_end = chunk_view.rfind("</body>");
const auto table_start = chunk_view.find("<table");
const auto table_end = chunk_view.rfind("</table>");
std::string_view table_view{};
// try and find the body, if this doesn't exist, fallback it's not a valid html page.
if (body_start != std::string_view::npos && body_end != std::string_view::npos && body_end > body_start) {
table_view = chunk_view.substr(body_start, body_end - body_start);
}
// try and find the table, massively speeds up parsing if it exists.
// todo: this may cause issues with some web servers that don't use a table for listings.
// todo: if table fails to fine anything, fallback to body_view.
if (table_start != std::string_view::npos && table_end != std::string_view::npos && table_end > table_start) {
table_view = chunk_view.substr(table_start, table_end - table_start);
}
if (!table_view.empty()) {
const std::string_view href_tag_start = "<a href=\"";
const std::string_view href_tag_end = "\">";
const std::string_view anchor_tag_end = "</a>";
size_t pos = 0;
out.reserve(10000);
for (;;) {
const auto href_pos = table_view.find(href_tag_start, pos);
if (href_pos == std::string_view::npos) {
break; // no more href.
}
pos = href_pos + href_tag_start.length();
const auto href_begin = pos;
const auto href_end = table_view.find(href_tag_end, href_begin);
if (href_end == std::string_view::npos) {
break; // no more href.
}
const auto href_name_end = table_view.find('"', href_begin);
if (href_name_end == std::string_view::npos || href_name_end < href_begin || href_name_end > href_end) {
break; // invalid href.
}
const auto name_begin = href_end + href_tag_end.length();
const auto name_end = table_view.find(anchor_tag_end, name_begin);
if (name_end == std::string_view::npos) {
break; // no more names.
}
pos = name_end + anchor_tag_end.length();
auto href = url_decode(std::string{table_view.substr(href_begin, href_name_end - href_begin)});
auto name = url_decode(std::string{table_view.substr(name_begin, name_end - name_begin)});
// skip empty names/links, root dir entry and links that are not actual files/dirs (e.g. sorting/filter controls).
if (name.empty() || href.empty() || name == "/" || href.starts_with('?') || href.starts_with('#')) {
continue;
}
// skip parent directory entry and external links.
if (href == ".." || name == ".." || href.starts_with("../") || name.starts_with("../") || href.find("://") != std::string::npos) {
continue;
}
const auto is_dir = href.ends_with('/');
if (is_dir) {
href.pop_back(); // remove the trailing '/'
}
out.emplace_back(name, href, is_dir);
}
}
log_write("[HTTP] Parsed %zu entries from directory listing\n", out.size());
return 0;
}
int Device::http_stat(const std::string& path, struct stat* st, bool is_dir) {
std::memset(st, 0, sizeof(*st));
const auto url = build_url(path, is_dir);
curl_set_common_options(this->curl, url);
curl_easy_setopt(this->curl, CURLOPT_NOBODY, 1L);
curl_easy_setopt(this->curl, CURLOPT_FILETIME, 1L);
const auto res = curl_easy_perform(this->curl);
if (res != CURLE_OK) {
log_write("[HTTP] curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
return -EIO;
}
long response_code = 0;
curl_easy_getinfo(this->curl, CURLINFO_RESPONSE_CODE, &response_code);
curl_off_t file_size = 0;
curl_easy_getinfo(this->curl, CURLINFO_CONTENT_LENGTH_DOWNLOAD_T, &file_size);
curl_off_t file_time = 0;
curl_easy_getinfo(this->curl, CURLINFO_FILETIME_T, &file_time);
const char* content_type{};
curl_easy_getinfo(this->curl, CURLINFO_CONTENT_TYPE, &content_type);
const char* effective_url{};
curl_easy_getinfo(this->curl, CURLINFO_EFFECTIVE_URL, &effective_url);
switch (response_code) {
case 200: // OK
case 206: // Partial Content
break;
case 301: // Moved Permanently
case 302: // Found
case 303: // See Other
case 307: // Temporary Redirect
case 308: // Permanent Redirect
return -EIO;
case 401: // Unauthorized
case 403: // Forbidden
return -EACCES;
case 404: // Not Found
return -ENOENT;
default:
return -EIO;
}
if (effective_url) {
if (std::string_view{effective_url}.ends_with('/')) {
is_dir = true;
}
}
if (content_type && !std::strcmp(content_type, "text/html")) {
is_dir = true;
}
if (is_dir) {
st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
} else {
st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
st->st_size = file_size > 0 ? file_size : 0;
}
st->st_mtime = file_time > 0 ? file_time : 0;
st->st_atime = st->st_mtime;
st->st_ctime = st->st_mtime;
st->st_nlink = 1;
return 0;
}
bool Device::Mount() {
if (mounted) {
return true;
}
if (!MountCurlDevice::Mount()) {
return false;
}
// todo: query server with OPTIONS to see if it supports range requests.
// todo: see ftp for example.
return mounted = true;
}
int Device::devoptab_open(void *fileStruct, const char *path, int flags, int mode) {
auto file = static_cast<File*>(fileStruct);
struct stat st;
const auto ret = http_stat(path, &st, false);
if (ret < 0) {
log_write("[HTTP] http_stat() failed for file: %s errno: %s\n", path, std::strerror(-ret));
return ret;
}
if (st.st_mode & S_IFDIR) {
log_write("[HTTP] Attempted to open a directory as a file: %s\n", path);
return -EISDIR;
}
file->entry = new FileEntry{path, st};
return 0;
}
int Device::devoptab_close(void *fd) {
auto file = static_cast<File*>(fd);
delete file->push_pull_thread_data;
delete file->entry;
return 0;
}
ssize_t Device::devoptab_read(void *fd, char *ptr, size_t len) {
auto file = static_cast<File*>(fd);
len = std::min(len, file->entry->st.st_size - file->off);
if (!len) {
return 0;
}
if (file->off != file->last_off) {
log_write("[HTTP] File offset changed from %zu to %zu, resetting download thread\n", file->last_off, file->off);
file->last_off = file->off;
delete file->push_pull_thread_data;
file->push_pull_thread_data = nullptr;
}
if (!file->push_pull_thread_data) {
log_write("[HTTP] Creating download thread data for file: %s\n", file->entry->path.c_str());
file->push_pull_thread_data = CreatePushData(this->transfer_curl, build_url(file->entry->path, false), file->off);
if (!file->push_pull_thread_data) {
log_write("[HTTP] Failed to create download thread data for file: %s\n", file->entry->path.c_str());
return -EIO;
}
}
const auto ret = file->push_pull_thread_data->PullData(ptr, len);
file->off += ret;
file->last_off = file->off;
return ret;
}
ssize_t Device::devoptab_seek(void *fd, off_t pos, int dir) {
auto file = static_cast<File*>(fd);
if (dir == SEEK_CUR) {
pos += file->off;
} else if (dir == SEEK_END) {
pos = file->entry->st.st_size;
}
return file->off = std::clamp<u64>(pos, 0, file->entry->st.st_size);
}
int Device::devoptab_fstat(void *fd, struct stat *st) {
auto file = static_cast<File*>(fd);
std::memcpy(st, &file->entry->st, sizeof(*st));
return 0;
}
int Device::devoptab_diropen(void* fd, const char *path) {
auto dir = static_cast<Dir*>(fd);
log_write("[HTTP] Opening directory: %s\n", path);
auto entries = new DirEntries();
const auto ret = http_dirlist(path, *entries);
if (ret < 0) {
log_write("[HTTP] http_dirlist() failed for directory: %s errno: %s\n", path, std::strerror(-ret));
delete entries;
return ret;
}
log_write("[HTTP] Opened directory: %s with %zu entries\n", path, entries->size());
dir->entries = entries;
return 0;
}
int Device::devoptab_dirreset(void* fd) {
auto dir = static_cast<Dir*>(fd);
dir->index = 0;
return 0;
}
int Device::devoptab_dirnext(void* fd, char *filename, struct stat *filestat) {
auto dir = static_cast<Dir*>(fd);
if (dir->index >= dir->entries->size()) {
return -ENOENT;
}
auto& entry = (*dir->entries)[dir->index];
if (entry.is_dir) {
filestat->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
} else {
filestat->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
}
// <a href="Compass_2.0.7.1-Release_ScVi3.0.1-Standalone-21-2-0-7-1-1729820977.zip">Compass_2.0.7.1-Release_ScVi3.0.1-Standalone-21..&gt;</a>
filestat->st_nlink = 1;
// std::strcpy(filename, entry.name.c_str());
std::strcpy(filename, entry.href.c_str());
dir->index++;
return 0;
}
int Device::devoptab_dirclose(void* fd) {
auto dir = static_cast<Dir*>(fd);
delete dir->entries;
return 0;
}
int Device::devoptab_lstat(const char *path, struct stat *st) {
auto ret = http_stat(path, st, false);
if (ret < 0) {
ret = http_stat(path, st, true);
}
if (ret < 0) {
log_write("[HTTP] http_stat() failed for path: %s errno: %s\n", path, std::strerror(-ret));
return ret;
}
return 0;
}
} // namespace
Result MountHttpAll() {
return common::MountNetworkDevice([](const common::MountConfig& config) {
return std::make_unique<Device>(config);
},
sizeof(File), sizeof(Dir),
"HTTP",
true
);
}
} // namespace sphaira::devoptab

View File

@@ -0,0 +1,311 @@
#include "utils/devoptab.hpp"
#include "utils/devoptab_common.hpp"
#include "defines.hpp"
#include "log.hpp"
#include "location.hpp"
#include <cstring>
#include <cerrno>
#include <array>
#include <memory>
#include <algorithm>
#include <unistd.h>
#include <fcntl.h>
#include <dirent.h>
namespace sphaira::devoptab {
namespace {
struct File {
int fd;
};
struct Dir {
DIR* dir;
location::StdioEntries* entries;
u32 index;
};
struct Device final : common::MountDevice {
Device(const common::MountConfig& _config)
: MountDevice{_config} {
}
private:
bool Mount() override { return true; }
int devoptab_open(void *fileStruct, const char *path, int flags, int mode) override;
int devoptab_close(void *fd) override;
ssize_t devoptab_read(void *fd, char *ptr, size_t len) override;
ssize_t devoptab_seek(void *fd, off_t pos, int dir) override;
int devoptab_fstat(void *fd, struct stat *st) override;
int devoptab_unlink(const char *path) override;
int devoptab_rename(const char *oldName, const char *newName) override;
int devoptab_mkdir(const char *path, int mode) override;
int devoptab_rmdir(const char *path) override;
int devoptab_diropen(void* fd, const char *path) override;
int devoptab_dirreset(void* fd) override;
int devoptab_dirnext(void* fd, char *filename, struct stat *filestat) override;
int devoptab_dirclose(void* fd) override;
int devoptab_lstat(const char *path, struct stat *st) override;
int devoptab_ftruncate(void *fd, off_t len) override;
int devoptab_statvfs(const char *path, struct statvfs *buf) override;
int devoptab_fsync(void *fd) override;
int devoptab_utimes(const char *path, const struct timeval times[2]) override;
};
// converts "/[SMB] pi:/folder/file.txt" to "pi:"
auto FixPath(const char* path) -> std::pair<fs::FsPath, std::string_view> {
while (*path == '/') {
path++;
}
std::string_view mount_name = path;
const auto dilem = mount_name.find_first_of(':');
if (dilem == std::string_view::npos) {
return {path, {}};
}
mount_name = mount_name.substr(0, dilem + 1);
fs::FsPath fixed_path = path;
if (fixed_path.ends_with(":")) {
fixed_path += '/';
}
log_write("[MOUNTS] FixPath: %s -> %s, mount: %.*s\n", path, fixed_path.s, (int)mount_name.size(), mount_name.data());
return {fixed_path, mount_name};
}
int Device::devoptab_open(void *fileStruct, const char *_path, int flags, int mode) {
auto file = static_cast<File*>(fileStruct);
const auto [path, mount_name] = FixPath(_path);
if (mount_name.empty()) {
log_write("[MOUNTS] devoptab_open: invalid path: %s\n", _path);
return -ENOENT;
}
file->fd = open(path, flags, mode);
if (file->fd < 0) {
log_write("[MOUNTS] devoptab_open: failed to open %s: %s\n", path.s, std::strerror(errno));
return -errno;
}
return 0;
}
int Device::devoptab_close(void *fd) {
auto file = static_cast<File*>(fd);
std::memset(file, 0, sizeof(*file));
return 0;
}
ssize_t Device::devoptab_read(void *fd, char *ptr, size_t len) {
auto file = static_cast<File*>(fd);
return read(file->fd, ptr, len);
}
ssize_t Device::devoptab_seek(void *fd, off_t pos, int dir) {
auto file = static_cast<File*>(fd);
return lseek(file->fd, pos, dir);
}
int Device::devoptab_fstat(void *fd, struct stat *st) {
auto file = static_cast<File*>(fd);
return fstat(file->fd, st);
}
int Device::devoptab_unlink(const char *_path) {
const auto [path, mount_name] = FixPath(_path);
if (mount_name.empty()) {
log_write("[MOUNTS] devoptab_unlink: invalid path: %s\n", _path);
return -ENOENT;
}
return unlink(path);
}
int Device::devoptab_rename(const char *_oldName, const char *_newName) {
const auto [oldName, old_mount_name] = FixPath(_oldName);
const auto [newName, new_mount_name] = FixPath(_newName);
if (old_mount_name.empty() || new_mount_name.empty() || old_mount_name != new_mount_name) {
log_write("[MOUNTS] devoptab_rename: invalid path: %s or %s\n", _oldName, _newName);
return -ENOENT;
}
return rename(oldName, newName);
}
int Device::devoptab_mkdir(const char *_path, int mode) {
const auto [path, mount_name] = FixPath(_path);
if (mount_name.empty()) {
log_write("[MOUNTS] devoptab_mkdir: invalid path: %s\n", _path);
return -ENOENT;
}
return mkdir(path, mode);
}
int Device::devoptab_rmdir(const char *_path) {
const auto [path, mount_name] = FixPath(_path);
if (mount_name.empty()) {
log_write("[MOUNTS] devoptab_rmdir: invalid path: %s\n", _path);
return -ENOENT;
}
return rmdir(path);
}
int Device::devoptab_diropen(void* fd, const char *_path) {
auto dir = static_cast<Dir*>(fd);
const auto [path, mount_name] = FixPath(_path);
if (mount_name.empty()) {
dir->entries = new location::StdioEntries();
const auto entries = location::GetStdio(false);
for (auto& entry : entries) {
if (entry.fs_hidden) {
continue;
}
dir->entries->emplace_back(std::move(entry));
}
return 0;
} else {
dir->dir = opendir(path);
if (!dir->dir) {
log_write("[MOUNTS] devoptab_diropen: failed to open dir %s: %s\n", path.s, std::strerror(errno));
return -errno;
}
return 0;
}
return -ENOENT;
}
int Device::devoptab_dirreset(void* fd) {
auto dir = static_cast<Dir*>(fd);
if (dir->dir) {
rewinddir(dir->dir);
} else {
dir->index = 0;
}
return 0;
}
int Device::devoptab_dirnext(void* fd, char *filename, struct stat *filestat) {
log_write("[MOUNTS] devoptab_dirnext\n");
auto dir = static_cast<Dir*>(fd);
if (dir->dir) {
const auto entry = readdir(dir->dir);
if (!entry) {
log_write("[MOUNTS] devoptab_dirnext: no more entries\n");
return -ENOENT;
}
// todo: verify this.
filestat->st_nlink = 1;
filestat->st_mode = entry->d_type == DT_DIR ? S_IFDIR : S_IFREG;
std::snprintf(filename, NAME_MAX, "%s", entry->d_name);
} else {
if (dir->index >= dir->entries->size()) {
return -ENOENT;
}
const auto& entry = (*dir->entries)[dir->index];
filestat->st_nlink = 1;
filestat->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
if (entry.mount.ends_with(":/")) {
std::snprintf(filename, NAME_MAX, "%s", entry.mount.substr(0, entry.mount.size() - 1).c_str());
} else {
std::snprintf(filename, NAME_MAX, "%s", entry.mount.c_str());
}
}
dir->index++;
return 0;
}
int Device::devoptab_dirclose(void* fd) {
auto dir = static_cast<Dir*>(fd);
if (dir->dir) {
closedir(dir->dir);
} else if (dir->entries) {
delete dir->entries;
}
return 0;
}
int Device::devoptab_lstat(const char *_path, struct stat *st) {
const auto [path, mount_name] = FixPath(_path);
if (mount_name.empty()) {
st->st_nlink = 1;
st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
} else {
return lstat(path, st);
}
return -ENOENT;
}
int Device::devoptab_ftruncate(void *fd, off_t len) {
auto file = static_cast<File*>(fd);
return ftruncate(file->fd, len);
}
int Device::devoptab_statvfs(const char *_path, struct statvfs *buf) {
const auto [path, mount_name] = FixPath(_path);
if (mount_name.empty()) {
log_write("[MOUNTS] devoptab_statvfs: invalid path: %s\n", _path);
return -ENOENT;
}
return statvfs(path, buf);
}
int Device::devoptab_fsync(void *fd) {
auto file = static_cast<File*>(fd);
return fsync(file->fd);
}
int Device::devoptab_utimes(const char *_path, const struct timeval times[2]) {
const auto [path, mount_name] = FixPath(_path);
if (mount_name.empty()) {
log_write("[MOUNTS] devoptab_utimes: invalid path: %s\n", _path);
return -ENOENT;
}
return utimes(path, times);
}
} // namespace
Result MountInternalMounts() {
common::MountConfig config{};
config.fs_hidden = true;
config.dump_hidden = true;
if (!common::MountNetworkDevice2(
std::make_unique<Device>(config),
config,
sizeof(File), sizeof(Dir),
"mounts", "mounts:/"
)) {
log_write("[MOUNTS] Failed to mount\n");
R_THROW(0x1);
}
R_SUCCEED();
}
} // namespace sphaira::devoptab

View File

@@ -20,7 +20,6 @@
#include <array>
#include <memory>
#include <algorithm>
#include <sys/iosupport.h>
namespace sphaira::devoptab {
namespace {
@@ -74,25 +73,18 @@ struct DirEntry {
const yati::container::Collections* pfs0;
};
struct Device {
std::vector<NamedCollection> collections;
std::unique_ptr<yati::source::Base> source;
};
struct File {
Device* device;
FileEntry entry;
size_t off;
};
struct Dir {
Device* device;
DirEntry entry;
u32 index;
bool is_root;
};
bool find_file(std::span<NamedCollection> named, std::string_view path, FileEntry& out) {
bool find_file(std::span<const NamedCollection> named, std::string_view path, FileEntry& out) {
for (auto& e : named) {
if (path.starts_with("/" + e.name)) {
out.fs_type = e.fs_type;
@@ -154,55 +146,68 @@ bool find_dir(std::span<const NamedCollection> named, std::string_view path, Dir
return false;
}
int set_errno(struct _reent *r, int err) {
r->_errno = err;
return -1;
}
struct Device final : common::MountDevice {
Device(std::unique_ptr<yati::source::Base>&& _source, const std::vector<NamedCollection>& _collections, const common::MountConfig& _config)
: MountDevice{_config}
, source{std::forward<decltype(_source)>(_source)}
, collections{_collections} {
int devoptab_open(struct _reent *r, void *fileStruct, const char *_path, int flags, int mode) {
auto device = (Device*)r->deviceData;
}
private:
bool Mount() override { return true; }
int devoptab_open(void *fileStruct, const char *path, int flags, int mode) override;
int devoptab_close(void *fd) override;
ssize_t devoptab_read(void *fd, char *ptr, size_t len) override;
ssize_t devoptab_seek(void *fd, off_t pos, int dir) override;
int devoptab_fstat(void *fd, struct stat *st) override;
int devoptab_diropen(void* fd, const char *path) override;
int devoptab_dirreset(void* fd) override;
int devoptab_dirnext(void* fd, char *filename, struct stat *filestat) override;
int devoptab_dirclose(void* fd) override;
int devoptab_lstat(const char *path, struct stat *st) override;
private:
std::unique_ptr<yati::source::Base> source;
const std::vector<NamedCollection> collections;
};
int Device::devoptab_open(void *fileStruct, const char *path, int flags, int mode) {
auto file = static_cast<File*>(fileStruct);
std::memset(file, 0, sizeof(*file));
char path[FS_MAX_PATH];
if (!common::fix_path(_path, path)) {
return set_errno(r, ENOENT);
FileEntry entry{};
if (!find_file(this->collections, path, entry)) {
log_write("[NCAFS] failed to find file entry: %s\n", path);
return -ENOENT;
}
FileEntry entry;
if (!find_file(device->collections, path, entry)) {
log_write("[NCAFS] failed to find file entry\n");
return set_errno(r, ENOENT);
}
file->device = device;
file->entry = entry;
return r->_errno = 0;
return 0;
}
int devoptab_close(struct _reent *r, void *fd) {
int Device::devoptab_close(void *fd) {
auto file = static_cast<File*>(fd);
std::memset(file, 0, sizeof(*file));
return r->_errno = 0;
return 0;
}
ssize_t devoptab_read(struct _reent *r, void *fd, char *ptr, size_t len) {
ssize_t Device::devoptab_read(void *fd, char *ptr, size_t len) {
auto file = static_cast<File*>(fd);
const auto& entry = file->entry;
u64 bytes_read;
len = std::min(len, entry.size - file->off);
if (R_FAILED(file->device->source->Read(ptr, entry.offset + file->off, len, &bytes_read))) {
return set_errno(r, ENOENT);
if (R_FAILED(this->source->Read(ptr, entry.offset + file->off, len, &bytes_read))) {
return -EIO;
}
file->off += bytes_read;
return bytes_read;
}
off_t devoptab_seek(struct _reent *r, void *fd, off_t pos, int dir) {
ssize_t Device::devoptab_seek(void *fd, off_t pos, int dir) {
auto file = static_cast<File*>(fd);
const auto& entry = file->entry;
@@ -212,54 +217,39 @@ off_t devoptab_seek(struct _reent *r, void *fd, off_t pos, int dir) {
pos = entry.size;
}
r->_errno = 0;
return file->off = std::clamp<u64>(pos, 0, entry.size);
}
int devoptab_fstat(struct _reent *r, void *fd, struct stat *st) {
int Device::devoptab_fstat(void *fd, struct stat *st) {
auto file = static_cast<File*>(fd);
const auto& entry = file->entry;
std::memset(st, 0, sizeof(*st));
st->st_nlink = 1;
st->st_size = entry.size;
st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
return r->_errno = 0;
return 0;
}
DIR_ITER* devoptab_diropen(struct _reent *r, DIR_ITER *dirState, const char *_path) {
auto device = (Device*)r->deviceData;
auto dir = static_cast<Dir*>(dirState->dirStruct);
int Device::devoptab_diropen(void* fd, const char *path) {
auto dir = static_cast<Dir*>(fd);
std::memset(dir, 0, sizeof(*dir));
char path[FS_MAX_PATH];
if (!common::fix_path(_path, path)) {
set_errno(r, ENOENT);
return NULL;
}
if (!std::strcmp(path, "/")) {
dir->device = device;
dir->is_root = true;
r->_errno = 0;
return dirState;
return 0;
} else {
DirEntry entry;
if (!find_dir(device->collections, path, entry)) {
set_errno(r, ENOENT);
return NULL;
DirEntry entry{};
if (!find_dir(this->collections, path, entry)) {
return -ENOENT;
}
dir->device = device;
dir->entry = entry;
r->_errno = 0;
return dirState;
return 0;
}
}
int devoptab_dirreset(struct _reent *r, DIR_ITER *dirState) {
auto dir = static_cast<Dir*>(dirState->dirStruct);
int Device::devoptab_dirreset(void* fd) {
auto dir = static_cast<Dir*>(fd);
auto& entry = dir->entry;
if (dir->is_root) {
@@ -272,30 +262,29 @@ int devoptab_dirreset(struct _reent *r, DIR_ITER *dirState) {
}
}
return r->_errno = 0;
return 0;
}
int devoptab_dirnext(struct _reent *r, DIR_ITER *dirState, char *filename, struct stat *filestat) {
auto dir = static_cast<Dir*>(dirState->dirStruct);
int Device::devoptab_dirnext(void* fd, char *filename, struct stat *filestat) {
auto dir = static_cast<Dir*>(fd);
auto& entry = dir->entry;
std::memset(filestat, 0, sizeof(*filestat));
if (dir->is_root) {
if (dir->index >= dir->device->collections.size()) {
return set_errno(r, ENOENT);
if (dir->index >= this->collections.size()) {
return -ENOENT;
}
filestat->st_nlink = 1;
filestat->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
std::strcpy(filename, dir->device->collections[dir->index].name.c_str());
std::strcpy(filename, this->collections[dir->index].name.c_str());
} else {
if (entry.fs_type == nca::FileSystemType_RomFS) {
if (!romfs::dirnext(entry.romfs, filename, filestat)) {
return set_errno(r, ENOENT);
return -ENOENT;
}
} else {
if (dir->index >= entry.pfs0->size()) {
return set_errno(r, ENOENT);
return -ENOENT;
}
const auto& collection = (*entry.pfs0)[dir->index];
@@ -307,123 +296,65 @@ int devoptab_dirnext(struct _reent *r, DIR_ITER *dirState, char *filename, struc
}
dir->index++;
return r->_errno = 0;
return 0;
}
int devoptab_dirclose(struct _reent *r, DIR_ITER *dirState) {
auto dir = static_cast<Dir*>(dirState->dirStruct);
int Device::devoptab_dirclose(void* fd) {
auto dir = static_cast<Dir*>(fd);
std::memset(dir, 0, sizeof(*dir));
return r->_errno = 0;
return 0;
}
int devoptab_lstat(struct _reent *r, const char *_path, struct stat *st) {
auto device = (Device*)r->deviceData;
char path[FS_MAX_PATH];
if (!common::fix_path(_path, path)) {
return set_errno(r, ENOENT);
}
std::memset(st, 0, sizeof(*st));
int Device::devoptab_lstat(const char *path, struct stat *st) {
st->st_nlink = 1;
if (!std::strcmp(path, "/")) {
st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
} else {
// can be optimised for romfs.
FileEntry file_entry;
DirEntry dir_entry;
if (find_file(device->collections, path, file_entry)) {
FileEntry file_entry{};
DirEntry dir_entry{};
if (find_file(this->collections, path, file_entry)) {
st->st_size = file_entry.size;
st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
} else if (find_dir(device->collections, path, dir_entry)) {
} else if (find_dir(this->collections, path, dir_entry)) {
st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
} else {
return set_errno(r, ENOENT);
return -ENOENT;
}
}
return r->_errno = 0;
}
constexpr devoptab_t DEVOPTAB = {
.structSize = sizeof(File),
.open_r = devoptab_open,
.close_r = devoptab_close,
.read_r = devoptab_read,
.seek_r = devoptab_seek,
.fstat_r = devoptab_fstat,
.stat_r = devoptab_lstat,
.dirStateSize = sizeof(Dir),
.diropen_r = devoptab_diropen,
.dirreset_r = devoptab_dirreset,
.dirnext_r = devoptab_dirnext,
.dirclose_r = devoptab_dirclose,
.lstat_r = devoptab_lstat,
};
struct Entry {
Device device{};
devoptab_t devoptab{};
fs::FsPath path{};
fs::FsPath mount{};
char name[32]{};
s32 ref_count{};
~Entry() {
RemoveDevice(mount);
}
};
Mutex g_mutex;
std::array<std::unique_ptr<Entry>, common::MAX_ENTRIES> g_entries;
bool IsAlreadyMounted(const fs::FsPath& path, fs::FsPath& out_path) {
// check if we already have the save mounted.
for (auto& e : g_entries) {
if (e && e->path == path) {
e->ref_count++;
out_path = e->mount;
return true;
}
}
return false;
return 0;
}
Result MountNcaInternal(fs::Fs* fs, const std::shared_ptr<yati::source::Base>& source, s64 size, const fs::FsPath& path, fs::FsPath& out_path) {
// otherwise, find next free entry.
auto itr = std::ranges::find_if(g_entries, [](auto& e){
return !e;
});
R_UNLESS(itr != g_entries.end(), 0x1);
const auto index = std::distance(g_entries.begin(), itr);
// todo: rather than manually fetching tickets, use spl to
// decrypt the nca for use (somehow, look how ams does it?).
keys::Keys keys;
R_TRY(keys::parse_keys(keys, true));
nca::Header header;
nca::Header header{};
R_TRY(source->Read2(&header, 0, sizeof(header)));
R_TRY(nca::DecryptHeader(&header, keys, header));
std::unique_ptr<yati::source::Base> nca_reader;
std::unique_ptr<yati::source::Base> nca_reader{};
log_write("[NCA] got header, type: %s\n", nca::GetContentTypeStr(header.content_type));
// check if this is a ncz.
ncz::Header ncz_header;
R_TRY(source->Read2(&ncz_header, NCZ_NORMAL_SIZE, sizeof(ncz_header)));
ncz::Header ncz_header{};
if (size >= NCZ_NORMAL_SIZE) {
R_TRY(source->Read2(&ncz_header, NCZ_NORMAL_SIZE, sizeof(ncz_header)));
}
if (ncz_header.magic == NCZ_SECTION_MAGIC) {
if (size >= NCZ_NORMAL_SIZE && ncz_header.magic == NCZ_SECTION_MAGIC) {
// read all the sections.
s64 ncz_offset = NCZ_SECTION_OFFSET;
ncz::Sections ncz_sections(ncz_header.total_sections);
R_TRY(source->Read2(ncz_sections.data(), ncz_offset, ncz_sections.size() * sizeof(ncz::Section)));
ncz_offset += ncz_sections.size() * sizeof(ncz::Section);
ncz::BlockHeader ncz_block_header;
ncz::BlockHeader ncz_block_header{};
R_TRY(source->Read2(&ncz_block_header, ncz_offset, sizeof(ncz_block_header)));
// ensure this is a block compressed nsz, otherwise bail out
@@ -450,7 +381,7 @@ Result MountNcaInternal(fs::Fs* fs, const std::shared_ptr<yati::source::Base>& s
);
}
std::vector<NamedCollection> collections;
std::vector<NamedCollection> collections{};
const auto& content_type_fs = CONTENT_TYPE_FS_NAMES[header.content_type];
for (u32 i = 0; i < NCA_SECTION_TOTAL; i++) {
@@ -489,7 +420,7 @@ Result MountNcaInternal(fs::Fs* fs, const std::shared_ptr<yati::source::Base>& s
continue;
}
NamedCollection collection;
NamedCollection collection{};
collection.name = content_type_fs[i].name;
collection.fs_type = fs_header.fs_type;
@@ -535,22 +466,16 @@ Result MountNcaInternal(fs::Fs* fs, const std::shared_ptr<yati::source::Base>& s
R_UNLESS(!collections.empty(), 0x9);
auto entry = std::make_unique<Entry>();
entry->path = path;
entry->devoptab = DEVOPTAB;
entry->devoptab.name = entry->name;
entry->devoptab.deviceData = &entry->device;
entry->device.source = std::move(nca_reader);
entry->device.collections = std::move(collections);
std::snprintf(entry->name, sizeof(entry->name), "nca_%zu", index);
std::snprintf(entry->mount, sizeof(entry->mount), "nca_%zu:/", index);
R_UNLESS(AddDevice(&entry->devoptab) >= 0, 0x1);
log_write("[NCA] DEVICE SUCCESS %s %s\n", path.s, entry->name);
out_path = entry->mount;
entry->ref_count++;
*itr = std::move(entry);
if (!common::MountReadOnlyIndexDevice(
[&nca_reader, &collections](const common::MountConfig& config) {
return std::make_unique<Device>(std::move(nca_reader), collections, config);
},
sizeof(File), sizeof(Dir),
"NCA", out_path
)) {
log_write("[NCA] Failed to mount %s\n", path.s);
R_THROW(0x1);
}
R_SUCCEED();
}
@@ -558,12 +483,6 @@ Result MountNcaInternal(fs::Fs* fs, const std::shared_ptr<yati::source::Base>& s
} // namespace
Result MountNca(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path) {
SCOPED_MUTEX(&g_mutex);
if (IsAlreadyMounted(path, out_path)) {
R_SUCCEED();
}
s64 size;
auto source = std::make_shared<yati::source::File>(fs, path);
R_TRY(source->GetSize(&size));
@@ -572,42 +491,11 @@ Result MountNca(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path) {
}
Result MountNcaNcm(NcmContentStorage* cs, const NcmContentId* id, fs::FsPath& out_path) {
SCOPED_MUTEX(&g_mutex);
fs::FsPath path;
const auto id_lower = std::byteswap(*(const u64*)id->c);
const auto id_upper = std::byteswap(*(const u64*)(id->c + 0x8));
std::snprintf(path, sizeof(path), "%016lx%016lx", id_lower, id_upper);
if (IsAlreadyMounted(path, out_path)) {
R_SUCCEED();
}
s64 size;
auto source = std::make_shared<ncm::NcmSource>(cs, id);
R_TRY(source->GetSize(&size));
return MountNcaInternal(nullptr, source, size, path, out_path);
}
void UmountNca(const fs::FsPath& mount) {
SCOPED_MUTEX(&g_mutex);
auto itr = std::ranges::find_if(g_entries, [&mount](auto& e){
return e && e->mount == mount;
});
if (itr == g_entries.end()) {
return;
}
if ((*itr)->ref_count) {
(*itr)->ref_count--;
}
if (!(*itr)->ref_count) {
itr->reset();
}
return MountNcaInternal(nullptr, source, size, {}, out_path);
}
} // namespace sphaira::devoptab

View File

@@ -0,0 +1,416 @@
#include "utils/devoptab_common.hpp"
#include "defines.hpp"
#include "log.hpp"
#include <sys/stat.h>
#include <fcntl.h>
#include <cstring>
#include <string>
#include <cstring>
#include <libnfs.h>
#include <minIni.h>
namespace sphaira::devoptab {
namespace {
struct Device final : common::MountDevice {
using MountDevice::MountDevice;
~Device();
private:
bool Mount() override;
int devoptab_open(void *fileStruct, const char *path, int flags, int mode) override;
int devoptab_close(void *fd) override;
ssize_t devoptab_read(void *fd, char *ptr, size_t len) override;
ssize_t devoptab_write(void *fd, const char *ptr, size_t len) override;
ssize_t devoptab_seek(void *fd, off_t pos, int dir) override;
int devoptab_fstat(void *fd, struct stat *st) override;
int devoptab_unlink(const char *path) override;
int devoptab_rename(const char *oldName, const char *newName) override;
int devoptab_mkdir(const char *path, int mode) override;
int devoptab_rmdir(const char *path) override;
int devoptab_diropen(void* fd, const char *path) override;
int devoptab_dirreset(void* fd) override;
int devoptab_dirnext(void* fd, char *filename, struct stat *filestat) override;
int devoptab_dirclose(void* fd) override;
int devoptab_lstat(const char *path, struct stat *st) override;
int devoptab_ftruncate(void *fd, off_t len) override;
int devoptab_statvfs(const char *path, struct statvfs *buf) override;
int devoptab_fsync(void *fd) override;
int devoptab_utimes(const char *path, const struct timeval times[2]) override;
private:
nfs_context* nfs{};
bool mounted{};
};
struct File {
nfsfh* fd;
};
struct Dir {
nfsdir* dir;
};
Device::~Device() {
if (nfs) {
if (mounted) {
nfs_umount(nfs);
}
nfs_destroy_context(nfs);
}
}
bool Device::Mount() {
if (mounted) {
return true;
}
log_write("[NFS] Mounting %s\n", this->config.url.c_str());
if (!nfs) {
nfs = nfs_init_context();
if (!nfs) {
log_write("[NFS] nfs_init_context() failed\n");
return false;
}
const auto uid = this->config.extra.find("uid");
if (uid != this->config.extra.end()) {
const auto uid_val = ini_parse_getl(uid->second.c_str(), -1);
if (uid_val < 0) {
log_write("[NFS] Invalid uid value: %s\n", uid->second.c_str());
} else {
log_write("[NFS] Setting uid: %ld\n", uid_val);
nfs_set_uid(nfs, uid_val);
}
}
const auto gid = this->config.extra.find("gid");
if (gid != this->config.extra.end()) {
const auto gid_val = ini_parse_getl(gid->second.c_str(), -1);
if (gid_val < 0) {
log_write("[NFS] Invalid gid value: %s\n", gid->second.c_str());
} else {
log_write("[NFS] Setting gid: %ld\n", gid_val);
nfs_set_gid(nfs, gid_val);
}
}
const auto version = this->config.extra.find("version");
if (version != this->config.extra.end()) {
const auto version_val = ini_parse_getl(version->second.c_str(), -1);
if (version_val != 3 && version_val != 4) {
log_write("[NFS] Invalid version value: %s\n", version->second.c_str());
} else {
log_write("[NFS] Setting version: %ld\n", version_val);
nfs_set_version(nfs, version_val);
}
}
if (this->config.timeout > 0) {
nfs_set_timeout(nfs, this->config.timeout);
nfs_set_readonly(nfs, this->config.read_only);
}
// nfs_set_mountport(nfs, url->port);
}
// fix the url if needed.
auto url = this->config.url;
if (!url.starts_with("nfs://")) {
log_write("[NFS] Prepending nfs:// to url: %s\n", url.c_str());
url = "nfs://" + url;
}
auto nfs_url = nfs_parse_url_full(nfs, url.c_str());
if (!nfs_url) {
log_write("[NFS] nfs_parse_url() failed for url: %s\n", url.c_str());
return false;
}
ON_SCOPE_EXIT(nfs_destroy_url(nfs_url));
const auto ret = nfs_mount(nfs, nfs_url->server, nfs_url->path);
if (ret) {
log_write("[NFS] nfs_mount() failed: %s errno: %s\n", nfs_get_error(nfs), std::strerror(-ret));
return false;
}
log_write("[NFS] Mounted %s\n", this->config.url.c_str());
return mounted = true;
}
int Device::devoptab_open(void *fileStruct, const char *path, int flags, int mode) {
auto file = static_cast<File*>(fileStruct);
const auto ret = nfs_open(nfs, path, flags, &file->fd);
if (ret) {
log_write("[NFS] nfs_open() failed: %s errno: %s\n", nfs_get_error(nfs), std::strerror(-ret));
return ret;
}
return 0;
}
int Device::devoptab_close(void *fd) {
auto file = static_cast<File*>(fd);
nfs_close(nfs, file->fd);
return 0;
}
ssize_t Device::devoptab_read(void *fd, char *ptr, size_t len) {
auto file = static_cast<File*>(fd);
// todo: uncomment this when it's fixed upstream.
#if 0
const auto ret = nfs_read(nfs, file->fd, ptr, len);
if (ret < 0) {
log_write("[NFS] nfs_read() failed: %s errno: %s\n", nfs_get_error(nfs), std::strerror(-ret));
return ret;
}
return ret;
#else
// work around for bug upsteam.
const auto max_read = nfs_get_readmax(nfs);
size_t bytes_read = 0;
while (bytes_read < len) {
const auto to_read = std::min<size_t>(len - bytes_read, max_read);
const auto ret = nfs_read(nfs, file->fd, ptr, to_read);
if (ret < 0) {
log_write("[NFS] nfs_read() failed: %s errno: %s\n", nfs_get_error(nfs), std::strerror(-ret));
return ret;
}
ptr += ret;
bytes_read += ret;
if (ret < to_read) {
break;
}
}
return bytes_read;
#endif
}
ssize_t Device::devoptab_write(void *fd, const char *ptr, size_t len) {
auto file = static_cast<File*>(fd);
// unlike read, writing the max size seems to work fine.
const auto max_write = nfs_get_writemax(nfs);
size_t written = 0;
while (written < len) {
const auto to_write = std::min<size_t>(len - written, max_write);
const auto ret = nfs_write(nfs, file->fd, ptr, to_write);
if (ret < 0) {
log_write("[NFS] nfs_write() failed: %s errno: %s\n", nfs_get_error(nfs), std::strerror(-ret));
return ret;
}
ptr += ret;
written += ret;
if (ret < to_write) {
break;
}
}
return written;
}
ssize_t Device::devoptab_seek(void *fd, off_t pos, int dir) {
auto file = static_cast<File*>(fd);
u64 current_offset = 0;
const auto ret = nfs_lseek(nfs, file->fd, pos, dir, &current_offset);
if (ret < 0) {
log_write("[NFS] nfs_lseek() failed: %s errno: %s\n", nfs_get_error(nfs), std::strerror(-ret));
return ret;
}
return current_offset;
}
int Device::devoptab_fstat(void *fd, struct stat *st) {
auto file = static_cast<File*>(fd);
const auto ret = nfs_fstat(nfs, file->fd, st);
if (ret) {
log_write("[NFS] nfs_fstat() failed: %s errno: %s\n", nfs_get_error(nfs), std::strerror(-ret));
return ret;
}
return 0;
}
int Device::devoptab_unlink(const char *path) {
const auto ret = nfs_unlink(nfs, path);
if (ret) {
log_write("[NFS] nfs_unlink() failed: %s errno: %s\n", nfs_get_error(nfs), std::strerror(-ret));
return ret;
}
return 0;
}
int Device::devoptab_rename(const char *oldName, const char *newName) {
const auto ret = nfs_rename(nfs, oldName, newName);
if (ret) {
log_write("[NFS] nfs_rename() failed: %s errno: %s\n", nfs_get_error(nfs), std::strerror(-ret));
return ret;
}
return 0;
}
int Device::devoptab_mkdir(const char *path, int mode) {
const auto ret = nfs_mkdir(nfs, path);
if (ret) {
log_write("[NFS] nfs_mkdir() failed: %s errno: %s\n", nfs_get_error(nfs), std::strerror(-ret));
return ret;
}
return 0;
}
int Device::devoptab_rmdir(const char *path) {
const auto ret = nfs_rmdir(nfs, path);
if (ret) {
log_write("[NFS] nfs_rmdir() failed: %s errno: %s\n", nfs_get_error(nfs), std::strerror(-ret));
return ret;
}
return 0;
}
int Device::devoptab_diropen(void* fd, const char *path) {
auto dir = static_cast<Dir*>(fd);
const auto ret = nfs_opendir(nfs, path, &dir->dir);
if (ret) {
log_write("[NFS] nfs_opendir() failed: %s errno: %s\n", nfs_get_error(nfs), std::strerror(-ret));
return ret;
}
return 0;
}
int Device::devoptab_dirreset(void* fd) {
auto dir = static_cast<Dir*>(fd);
nfs_rewinddir(nfs, dir->dir);
return 0;
}
int Device::devoptab_dirnext(void* fd, char *filename, struct stat *filestat) {
auto dir = static_cast<Dir*>(fd);
const auto entry = nfs_readdir(nfs, dir->dir);
if (!entry) {
return -ENOENT;
}
std::strncpy(filename, entry->name, NAME_MAX);
filename[NAME_MAX - 1] = '\0';
// not everything is needed, however we may as well fill it all in.
filestat->st_dev = entry->dev;
filestat->st_ino = entry->inode;
filestat->st_mode = entry->mode;
filestat->st_nlink = entry->nlink;
filestat->st_uid = entry->uid;
filestat->st_gid = entry->gid;
filestat->st_size = entry->size;
filestat->st_atime = entry->atime.tv_sec;
filestat->st_mtime = entry->mtime.tv_sec;
filestat->st_ctime = entry->ctime.tv_sec;
filestat->st_blksize = entry->blksize;
filestat->st_blocks = entry->blocks;
return 0;
}
int Device::devoptab_dirclose(void* fd) {
auto dir = static_cast<Dir*>(fd);
nfs_closedir(nfs, dir->dir);
return 0;
}
int Device::devoptab_lstat(const char *path, struct stat *st) {
const auto ret = nfs_stat(nfs, path, st);
if (ret) {
log_write("[NFS] nfs_stat() failed: %s errno: %s\n", nfs_get_error(nfs), std::strerror(-ret));
return ret;
}
return 0;
}
int Device::devoptab_ftruncate(void *fd, off_t len) {
auto file = static_cast<File*>(fd);
const auto ret = nfs_ftruncate(nfs, file->fd, len);
if (ret) {
log_write("[NFS] nfs_ftruncate() failed: %s errno: %s\n", nfs_get_error(nfs), std::strerror(-ret));
return ret;
}
return 0;
}
int Device::devoptab_statvfs(const char *path, struct statvfs *buf) {
const auto ret = nfs_statvfs(nfs, path, buf);
if (ret) {
log_write("[NFS] nfs_statvfs() failed: %s errno: %s\n", nfs_get_error(nfs), std::strerror(-ret));
return ret;
}
return 0;
}
int Device::devoptab_fsync(void *fd) {
auto file = static_cast<File*>(fd);
const auto ret = nfs_fsync(nfs, file->fd);
if (ret) {
log_write("[NFS] nfs_fsync() failed: %s errno: %s\n", nfs_get_error(nfs), std::strerror(-ret));
return ret;
}
return 0;
}
int Device::devoptab_utimes(const char *path, const struct timeval times[2]) {
// todo: nfs should accept const times, pr the fix.
struct timeval times_copy[2];
std::memcpy(times_copy, times, sizeof(times_copy));
const auto ret = nfs_utimes(nfs, path, times_copy);
if (ret) {
log_write("[NFS] nfs_utimes() failed: %s errno: %s\n", nfs_get_error(nfs), std::strerror(-ret));
return ret;
}
return 0;
}
} // namespace
Result MountNfsAll() {
return common::MountNetworkDevice([](const common::MountConfig& cfg) {
return std::make_unique<Device>(cfg);
},
sizeof(File), sizeof(Dir),
"NFS"
);
}
} // namespace sphaira::devoptab

View File

@@ -6,11 +6,6 @@
#include "log.hpp"
#include "nro.hpp"
#include "yati/nx/es.hpp"
#include "yati/nx/nca.hpp"
#include "yati/nx/keys.hpp"
#include "yati/nx/crypto.hpp"
#include "yati/container/nsp.hpp"
#include "yati/source/file.hpp"
#include <cstring>
@@ -18,7 +13,6 @@
#include <array>
#include <memory>
#include <algorithm>
#include <sys/iosupport.h>
namespace sphaira::devoptab {
namespace {
@@ -48,26 +42,18 @@ struct DirEntry {
romfs::DirEntry romfs;
};
struct Device {
std::unique_ptr<yati::source::Base> source;
std::vector<NamedCollection> collections;
FsTimeStampRaw timestamp;
};
struct File {
Device* device;
FileEntry entry;
size_t off;
};
struct Dir {
Device* device;
DirEntry entry;
u32 index;
bool is_root;
};
bool find_file(std::span<NamedCollection> named, std::string_view path, FileEntry& out) {
bool find_file(std::span<const NamedCollection> named, std::string_view path, FileEntry& out) {
for (auto& e : named) {
if (path.starts_with("/" + e.name)) {
out.is_romfs = e.is_romfs;
@@ -112,61 +98,75 @@ bool find_dir(std::span<const NamedCollection> named, std::string_view path, Dir
return false;
}
void fill_timestamp_from_device(const Device* device, struct stat *st) {
st->st_atime = device->timestamp.accessed;
st->st_ctime = device->timestamp.created;
st->st_mtime = device->timestamp.modified;
void fill_timestamp_from_device(const FsTimeStampRaw& timestamp, struct stat *st) {
st->st_atime = timestamp.accessed;
st->st_ctime = timestamp.created;
st->st_mtime = timestamp.modified;
}
int set_errno(struct _reent *r, int err) {
r->_errno = err;
return -1;
}
struct Device final : common::MountDevice {
Device(std::unique_ptr<yati::source::Base>&& _source, const std::vector<NamedCollection>& _collections, const FsTimeStampRaw& _timestamp, const common::MountConfig& _config)
: MountDevice{_config}
, source{std::forward<decltype(_source)>(_source)}
, collections{_collections}
, timestamp{_timestamp} {
int devoptab_open(struct _reent *r, void *fileStruct, const char *_path, int flags, int mode) {
auto device = (Device*)r->deviceData;
}
private:
bool Mount() override { return true; }
int devoptab_open(void *fileStruct, const char *path, int flags, int mode) override;
int devoptab_close(void *fd) override;
ssize_t devoptab_read(void *fd, char *ptr, size_t len) override;
ssize_t devoptab_seek(void *fd, off_t pos, int dir) override;
int devoptab_fstat(void *fd, struct stat *st) override;
int devoptab_diropen(void* fd, const char *path) override;
int devoptab_dirreset(void* fd) override;
int devoptab_dirnext(void* fd, char *filename, struct stat *filestat) override;
int devoptab_dirclose(void* fd) override;
int devoptab_lstat(const char *path, struct stat *st) override;
private:
std::unique_ptr<yati::source::Base> source;
const std::vector<NamedCollection> collections;
const FsTimeStampRaw timestamp;
};
int Device::devoptab_open(void *fileStruct, const char *path, int flags, int mode) {
auto file = static_cast<File*>(fileStruct);
std::memset(file, 0, sizeof(*file));
char path[FS_MAX_PATH];
if (!common::fix_path(_path, path)) {
return set_errno(r, ENOENT);
FileEntry entry{};
if (!find_file(this->collections, path, entry)) {
log_write("[NROFS] failed to find file entry: %s\n", path);
return -ENOENT;
}
FileEntry entry;
if (!find_file(device->collections, path, entry)) {
log_write("[NROFS] failed to find file entry\n");
return set_errno(r, ENOENT);
}
file->device = device;
file->entry = entry;
return r->_errno = 0;
return 0;
}
int devoptab_close(struct _reent *r, void *fd) {
int Device::devoptab_close(void *fd) {
auto file = static_cast<File*>(fd);
std::memset(file, 0, sizeof(*file));
return r->_errno = 0;
return 0;
}
ssize_t devoptab_read(struct _reent *r, void *fd, char *ptr, size_t len) {
ssize_t Device::devoptab_read(void *fd, char *ptr, size_t len) {
auto file = static_cast<File*>(fd);
const auto& entry = file->entry;
u64 bytes_read;
len = std::min(len, entry.size - file->off);
if (R_FAILED(file->device->source->Read(ptr, entry.offset + file->off, len, &bytes_read))) {
return set_errno(r, ENOENT);
if (R_FAILED(this->source->Read(ptr, entry.offset + file->off, len, &bytes_read))) {
return -EIO;
}
file->off += bytes_read;
return bytes_read;
}
off_t devoptab_seek(struct _reent *r, void *fd, off_t pos, int dir) {
ssize_t Device::devoptab_seek(void *fd, off_t pos, int dir) {
auto file = static_cast<File*>(fd);
const auto& entry = file->entry;
@@ -176,56 +176,40 @@ off_t devoptab_seek(struct _reent *r, void *fd, off_t pos, int dir) {
pos = entry.size;
}
r->_errno = 0;
return file->off = std::clamp<u64>(pos, 0, entry.size);
}
int devoptab_fstat(struct _reent *r, void *fd, struct stat *st) {
int Device::devoptab_fstat(void *fd, struct stat *st) {
auto file = static_cast<File*>(fd);
const auto& entry = file->entry;
std::memset(st, 0, sizeof(*st));
st->st_nlink = 1;
st->st_size = entry.size;
st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
fill_timestamp_from_device(file->device, st);
fill_timestamp_from_device(this->timestamp, st);
return r->_errno = 0;
return 0;
}
DIR_ITER* devoptab_diropen(struct _reent *r, DIR_ITER *dirState, const char *_path) {
auto device = (Device*)r->deviceData;
auto dir = static_cast<Dir*>(dirState->dirStruct);
std::memset(dir, 0, sizeof(*dir));
char path[FS_MAX_PATH];
if (!common::fix_path(_path, path)) {
set_errno(r, ENOENT);
return NULL;
}
int Device::devoptab_diropen(void* fd, const char *path) {
auto dir = static_cast<Dir*>(fd);
if (!std::strcmp(path, "/")) {
dir->device = device;
dir->is_root = true;
r->_errno = 0;
return dirState;
return 0;
} else {
DirEntry entry;
if (!find_dir(device->collections, path, entry)) {
set_errno(r, ENOENT);
return NULL;
DirEntry entry{};
if (!find_dir(this->collections, path, entry)) {
return -ENOENT;
}
dir->device = device;
dir->entry = entry;
r->_errno = 0;
return dirState;
return 0;
}
}
int devoptab_dirreset(struct _reent *r, DIR_ITER *dirState) {
auto dir = static_cast<Dir*>(dirState->dirStruct);
int Device::devoptab_dirreset(void* fd) {
auto dir = static_cast<Dir*>(fd);
auto& entry = dir->entry;
if (dir->is_root) {
@@ -236,20 +220,19 @@ int devoptab_dirreset(struct _reent *r, DIR_ITER *dirState) {
}
}
return r->_errno = 0;
return 0;
}
int devoptab_dirnext(struct _reent *r, DIR_ITER *dirState, char *filename, struct stat *filestat) {
auto dir = static_cast<Dir*>(dirState->dirStruct);
int Device::devoptab_dirnext(void* fd, char *filename, struct stat *filestat) {
auto dir = static_cast<Dir*>(fd);
auto& entry = dir->entry;
std::memset(filestat, 0, sizeof(*filestat));
if (dir->is_root) {
if (dir->index >= dir->device->collections.size()) {
return set_errno(r, ENOENT);
if (dir->index >= this->collections.size()) {
return -ENOENT;
}
const auto& e = dir->device->collections[dir->index];
const auto& e = this->collections[dir->index];
if (e.is_romfs) {
filestat->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
} else {
@@ -262,126 +245,60 @@ int devoptab_dirnext(struct _reent *r, DIR_ITER *dirState, char *filename, struc
} else {
if (entry.is_romfs) {
if (!romfs::dirnext(entry.romfs, filename, filestat)) {
return set_errno(r, ENOENT);
return -ENOENT;
}
}
}
fill_timestamp_from_device(dir->device, filestat);
fill_timestamp_from_device(this->timestamp, filestat);
dir->index++;
return r->_errno = 0;
return 0;
}
int devoptab_dirclose(struct _reent *r, DIR_ITER *dirState) {
auto dir = static_cast<Dir*>(dirState->dirStruct);
int Device::devoptab_dirclose(void* fd) {
auto dir = static_cast<Dir*>(fd);
std::memset(dir, 0, sizeof(*dir));
return r->_errno = 0;
return 0;
}
int devoptab_lstat(struct _reent *r, const char *_path, struct stat *st) {
auto device = (Device*)r->deviceData;
char path[FS_MAX_PATH];
if (!common::fix_path(_path, path)) {
return set_errno(r, ENOENT);
}
std::memset(st, 0, sizeof(*st));
int Device::devoptab_lstat(const char *path, struct stat *st) {
st->st_nlink = 1;
if (!std::strcmp(path, "/")) {
st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
} else {
// can be optimised for romfs.
FileEntry file_entry;
DirEntry dir_entry;
if (find_file(device->collections, path, file_entry)) {
FileEntry file_entry{};
DirEntry dir_entry{};
if (find_file(this->collections, path, file_entry)) {
st->st_size = file_entry.size;
st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
} else if (find_dir(device->collections, path, dir_entry)) {
} else if (find_dir(this->collections, path, dir_entry)) {
st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
} else {
return set_errno(r, ENOENT);
return -ENOENT;
}
}
fill_timestamp_from_device(device, st);
return r->_errno = 0;
}
constexpr devoptab_t DEVOPTAB = {
.structSize = sizeof(File),
.open_r = devoptab_open,
.close_r = devoptab_close,
.read_r = devoptab_read,
.seek_r = devoptab_seek,
.fstat_r = devoptab_fstat,
.stat_r = devoptab_lstat,
.dirStateSize = sizeof(Dir),
.diropen_r = devoptab_diropen,
.dirreset_r = devoptab_dirreset,
.dirnext_r = devoptab_dirnext,
.dirclose_r = devoptab_dirclose,
.lstat_r = devoptab_lstat,
};
struct Entry {
Device device{};
devoptab_t devoptab{};
fs::FsPath path{};
fs::FsPath mount{};
char name[32]{};
s32 ref_count{};
~Entry() {
RemoveDevice(mount);
}
};
Mutex g_mutex;
std::array<std::unique_ptr<Entry>, common::MAX_ENTRIES> g_entries;
bool IsAlreadyMounted(const fs::FsPath& path, fs::FsPath& out_path) {
// check if we already have the save mounted.
for (auto& e : g_entries) {
if (e && e->path == path) {
e->ref_count++;
out_path = e->mount;
return true;
}
}
return false;
fill_timestamp_from_device(this->timestamp, st);
return 0;
}
} // namespace
Result MountNro(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path) {
SCOPED_MUTEX(&g_mutex);
if (IsAlreadyMounted(path, out_path)) {
R_SUCCEED();
}
// otherwise, find next free entry.
auto itr = std::ranges::find_if(g_entries, [](auto& e){
return !e;
});
R_UNLESS(itr != g_entries.end(), 0x1);
const auto index = std::distance(g_entries.begin(), itr);
auto source = std::make_unique<yati::source::File>(fs, path);
NroData data;
NroData data{};
R_TRY(source->Read2(&data, 0, sizeof(data)));
R_UNLESS(data.header.magic == NROHEADER_MAGIC, Result_NroBadMagic);
NroAssetHeader asset;
NroAssetHeader asset{};
R_TRY(source->Read2(&asset, data.header.size, sizeof(asset)));
R_UNLESS(asset.magic == NROASSETHEADER_MAGIC, Result_NroBadMagic);
std::vector<NamedCollection> collections;
std::vector<NamedCollection> collections{};
if (asset.icon.size) {
NamedCollection collection{"icon.jpg", false, AssetCollection{data.header.size + asset.icon.offset, asset.icon.size}};
@@ -400,45 +317,21 @@ Result MountNro(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path) {
R_UNLESS(!collections.empty(), 0x9);
auto entry = std::make_unique<Entry>();
entry->path = path;
entry->devoptab = DEVOPTAB;
entry->devoptab.name = entry->name;
entry->devoptab.deviceData = &entry->device;
entry->device.source = std::move(source);
entry->device.collections = collections;
fs->GetFileTimeStampRaw(path, &entry->device.timestamp);
std::snprintf(entry->name, sizeof(entry->name), "nro_%zu", index);
std::snprintf(entry->mount, sizeof(entry->mount), "nro_%zu:/", index);
FsTimeStampRaw timestamp{};
fs->GetFileTimeStampRaw(path, &timestamp);
R_UNLESS(AddDevice(&entry->devoptab) >= 0, 0x1);
log_write("[NRO] DEVICE SUCCESS %s %s\n", path.s, entry->name);
out_path = entry->mount;
entry->ref_count++;
*itr = std::move(entry);
if (!common::MountReadOnlyIndexDevice(
[&source, &collections, &timestamp](const common::MountConfig& config) {
return std::make_unique<Device>(std::move(source), collections, timestamp, config);
},
sizeof(File), sizeof(Dir),
"NRO", out_path
)) {
log_write("[NRO] Failed to mount %s\n", path.s);
R_THROW(0x1);
}
R_SUCCEED();
}
void UmountNro(const fs::FsPath& mount) {
SCOPED_MUTEX(&g_mutex);
auto itr = std::ranges::find_if(g_entries, [&mount](auto& e){
return e && e->mount == mount;
});
if (itr == g_entries.end()) {
return;
}
if ((*itr)->ref_count) {
(*itr)->ref_count--;
}
if (!(*itr)->ref_count) {
itr->reset();
}
}
} // namespace sphaira::devoptab

View File

@@ -13,76 +13,84 @@
#include <array>
#include <memory>
#include <algorithm>
#include <sys/iosupport.h>
namespace sphaira::devoptab {
namespace {
struct Device {
std::unique_ptr<common::LruBufferedData> source;
yati::container::Collections collections;
};
using Collections = yati::container::Collections;
struct File {
Device* device;
const yati::container::CollectionEntry* collection;
size_t off;
};
struct Dir {
Device* device;
u32 index;
};
int set_errno(struct _reent *r, int err) {
r->_errno = err;
return -1;
}
struct Device final : common::MountDevice {
Device(std::unique_ptr<common::LruBufferedData>&& _source, const Collections& _collections, const common::MountConfig& _config)
: MountDevice{_config}
, source{std::forward<decltype(_source)>(_source)}
, collections{_collections} {
int devoptab_open(struct _reent *r, void *fileStruct, const char *_path, int flags, int mode) {
auto device = (Device*)r->deviceData;
auto file = static_cast<File*>(fileStruct);
std::memset(file, 0, sizeof(*file));
char path[FS_MAX_PATH];
if (!common::fix_path(_path, path)) {
return set_errno(r, ENOENT);
}
for (const auto& collection : device->collections) {
private:
bool Mount() override { return true; }
int devoptab_open(void *fileStruct, const char *path, int flags, int mode) override;
int devoptab_close(void *fd) override;
ssize_t devoptab_read(void *fd, char *ptr, size_t len) override;
ssize_t devoptab_seek(void *fd, off_t pos, int dir) override;
int devoptab_fstat(void *fd, struct stat *st) override;
int devoptab_diropen(void* fd, const char *path) override;
int devoptab_dirreset(void* fd) override;
int devoptab_dirnext(void* fd, char *filename, struct stat *filestat) override;
int devoptab_dirclose(void* fd) override;
int devoptab_lstat(const char *path, struct stat *st) override;
private:
std::unique_ptr<common::LruBufferedData> source;
const Collections collections;
};
int Device::devoptab_open(void *fileStruct, const char *path, int flags, int mode) {
auto file = static_cast<File*>(fileStruct);
for (const auto& collection : this->collections) {
if (path == "/" + collection.name) {
file->device = device;
file->collection = &collection;
return r->_errno = 0;
return 0;
}
}
return set_errno(r, ENOENT);
log_write("[NSP] failed to open file %s\n", path);
return -ENOENT;
}
int devoptab_close(struct _reent *r, void *fd) {
int Device::devoptab_close(void *fd) {
auto file = static_cast<File*>(fd);
std::memset(file, 0, sizeof(*file));
return r->_errno = 0;
return 0;
}
ssize_t devoptab_read(struct _reent *r, void *fd, char *ptr, size_t len) {
ssize_t Device::devoptab_read(void *fd, char *ptr, size_t len) {
auto file = static_cast<File*>(fd);
const auto& collection = file->collection;
len = std::min(len, collection->size - file->off);
u64 bytes_read;
if (R_FAILED(file->device->source->Read(ptr, collection->offset + file->off, len, &bytes_read))) {
return set_errno(r, ENOENT);
if (R_FAILED(this->source->Read(ptr, collection->offset + file->off, len, &bytes_read))) {
return -EIO;
}
file->off += bytes_read;
return bytes_read;
}
off_t devoptab_seek(struct _reent *r, void *fd, off_t pos, int dir) {
ssize_t Device::devoptab_seek(void *fd, off_t pos, int dir) {
auto file = static_cast<File*>(fd);
const auto& collection = file->collection;
@@ -92,159 +100,83 @@ off_t devoptab_seek(struct _reent *r, void *fd, off_t pos, int dir) {
pos = collection->size;
}
r->_errno = 0;
return file->off = std::clamp<u64>(pos, 0, collection->size);
}
int devoptab_fstat(struct _reent *r, void *fd, struct stat *st) {
int Device::devoptab_fstat(void *fd, struct stat *st) {
auto file = static_cast<File*>(fd);
const auto& collection = file->collection;
std::memset(st, 0, sizeof(*st));
st->st_nlink = 1;
st->st_size = collection->size;
st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
return r->_errno = 0;
return 0;
}
DIR_ITER* devoptab_diropen(struct _reent *r, DIR_ITER *dirState, const char *_path) {
auto device = (Device*)r->deviceData;
auto dir = static_cast<Dir*>(dirState->dirStruct);
std::memset(dir, 0, sizeof(*dir));
char path[FS_MAX_PATH];
if (!common::fix_path(_path, path)) {
set_errno(r, ENOENT);
return NULL;
}
int Device::devoptab_diropen(void* fd, const char *path) {
if (!std::strcmp(path, "/")) {
dir->device = device;
} else {
set_errno(r, ENOENT);
return NULL;
return 0;
}
r->_errno = 0;
return dirState;
log_write("[NSP] failed to open dir %s\n", path);
return -ENOENT;
}
int devoptab_dirreset(struct _reent *r, DIR_ITER *dirState) {
auto dir = static_cast<Dir*>(dirState->dirStruct);
int Device::devoptab_dirreset(void* fd) {
auto dir = static_cast<Dir*>(fd);
dir->index = 0;
return r->_errno = 0;
return 0;
}
int devoptab_dirnext(struct _reent *r, DIR_ITER *dirState, char *filename, struct stat *filestat) {
auto dir = static_cast<Dir*>(dirState->dirStruct);
std::memset(filestat, 0, sizeof(*filestat));
int Device::devoptab_dirnext(void* fd, char *filename, struct stat *filestat) {
auto dir = static_cast<Dir*>(fd);
if (dir->index >= dir->device->collections.size()) {
return set_errno(r, ENOENT);
if (dir->index >= this->collections.size()) {
return -ENOENT;
}
const auto& collection = dir->device->collections[dir->index];
const auto& collection = this->collections[dir->index];
filestat->st_nlink = 1;
filestat->st_size = collection.size;
filestat->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
std::strcpy(filename, collection.name.c_str());
dir->index++;
return r->_errno = 0;
return 0;
}
int devoptab_dirclose(struct _reent *r, DIR_ITER *dirState) {
auto dir = static_cast<Dir*>(dirState->dirStruct);
int Device::devoptab_dirclose(void* fd) {
auto dir = static_cast<Dir*>(fd);
std::memset(dir, 0, sizeof(*dir));
return r->_errno = 0;
return 0;
}
int devoptab_lstat(struct _reent *r, const char *_path, struct stat *st) {
auto device = (Device*)r->deviceData;
char path[FS_MAX_PATH];
if (!common::fix_path(_path, path)) {
return set_errno(r, ENOENT);
}
std::memset(st, 0, sizeof(*st));
int Device::devoptab_lstat(const char *path, struct stat *st) {
st->st_nlink = 1;
if (!std::strcmp(path, "/")) {
st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
} else {
const auto it = std::ranges::find_if(device->collections, [path](auto& e){
const auto it = std::ranges::find_if(this->collections, [path](auto& e){
return path == "/" + e.name;
});
if (it == device->collections.end()) {
return set_errno(r, ENOENT);
if (it == this->collections.end()) {
return -ENOENT;
}
st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
st->st_size = it->size;
}
st->st_nlink = 1;
return r->_errno = 0;
return 0;
}
constexpr devoptab_t DEVOPTAB = {
.structSize = sizeof(File),
.open_r = devoptab_open,
.close_r = devoptab_close,
.read_r = devoptab_read,
.seek_r = devoptab_seek,
.fstat_r = devoptab_fstat,
.stat_r = devoptab_lstat,
.dirStateSize = sizeof(Dir),
.diropen_r = devoptab_diropen,
.dirreset_r = devoptab_dirreset,
.dirnext_r = devoptab_dirnext,
.dirclose_r = devoptab_dirclose,
.lstat_r = devoptab_lstat,
};
struct Entry {
Device device{};
devoptab_t devoptab{};
fs::FsPath path{};
fs::FsPath mount{};
char name[32]{};
s32 ref_count{};
~Entry() {
RemoveDevice(mount);
}
};
Mutex g_mutex;
std::array<std::unique_ptr<Entry>, common::MAX_ENTRIES> g_entries;
} // namespace
Result MountNsp(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path) {
SCOPED_MUTEX(&g_mutex);
// check if we already have the save mounted.
for (auto& e : g_entries) {
if (e && e->path == path) {
e->ref_count++;
out_path = e->mount;
R_SUCCEED();
}
}
// otherwise, find next free entry.
auto itr = std::ranges::find_if(g_entries, [](auto& e){
return !e;
});
R_UNLESS(itr != g_entries.end(), 0x1);
const auto index = std::distance(g_entries.begin(), itr);
auto source = std::make_shared<yati::source::File>(fs, path);
s64 size;
@@ -255,44 +187,18 @@ Result MountNsp(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path) {
yati::container::Collections collections;
R_TRY(nsp.GetCollections(collections));
auto entry = std::make_unique<Entry>();
entry->path = path;
entry->devoptab = DEVOPTAB;
entry->devoptab.name = entry->name;
entry->devoptab.deviceData = &entry->device;
entry->device.source = std::move(buffered);
entry->device.collections = collections;
std::snprintf(entry->name, sizeof(entry->name), "nsp_%zu", index);
std::snprintf(entry->mount, sizeof(entry->mount), "nsp_%zu:/", index);
R_UNLESS(AddDevice(&entry->devoptab) >= 0, 0x1);
log_write("[NSP] DEVICE SUCCESS %s %s\n", path.s, entry->name);
out_path = entry->mount;
entry->ref_count++;
*itr = std::move(entry);
if (!common::MountReadOnlyIndexDevice(
[&buffered, &collections](const common::MountConfig& config) {
return std::make_unique<Device>(std::move(buffered), collections, config);
},
sizeof(File), sizeof(Dir),
"NSP", out_path
)) {
log_write("[NSP] Failed to mount %s\n", path.s);
R_THROW(0x1);
}
R_SUCCEED();
}
void UmountNsp(const fs::FsPath& mount) {
SCOPED_MUTEX(&g_mutex);
auto itr = std::ranges::find_if(g_entries, [&mount](auto& e){
return e && e->mount == mount;
});
if (itr == g_entries.end()) {
return;
}
if ((*itr)->ref_count) {
(*itr)->ref_count--;
}
if (!(*itr)->ref_count) {
itr->reset();
}
}
} // namespace sphaira::devoptab

View File

@@ -12,18 +12,11 @@
#include <array>
#include <memory>
#include <algorithm>
#include <sys/iosupport.h>
namespace sphaira::devoptab {
namespace {
struct Device {
save_ctx_t* ctx;
hierarchical_save_file_table_ctx_t* file_table;
};
struct File {
Device* device;
save_fs_list_entry_t entry;
allocation_table_storage_ctx_t storage;
size_t off;
@@ -35,138 +28,140 @@ struct DirNext {
};
struct Dir {
Device* device;
save_fs_list_entry_t entry;
u32 next_directory;
u32 next_file;
};
int set_errno(struct _reent *r, int err) {
r->_errno = err;
return -1;
}
struct Device final : common::MountDevice {
Device(save_ctx_t* _ctx, const common::MountConfig& _config)
: MountDevice{_config}
, ctx{_ctx} {
file_table = &ctx->save_filesystem_core.file_table;
}
int devoptab_open(struct _reent *r, void *fileStruct, const char *_path, int flags, int mode) {
auto device = (Device*)r->deviceData;
~Device() {
save_close_savefile(&this->ctx);
}
private:
bool Mount() override { return true; }
int devoptab_open(void *fileStruct, const char *path, int flags, int mode) override;
int devoptab_close(void *fd) override;
ssize_t devoptab_read(void *fd, char *ptr, size_t len) override;
ssize_t devoptab_seek(void *fd, off_t pos, int dir) override;
int devoptab_fstat(void *fd, struct stat *st) override;
int devoptab_diropen(void* fd, const char *path) override;
int devoptab_dirreset(void* fd) override;
int devoptab_dirnext(void* fd, char *filename, struct stat *filestat) override;
int devoptab_dirclose(void* fd) override;
int devoptab_lstat(const char *path, struct stat *st) override;
private:
save_ctx_t* ctx;
hierarchical_save_file_table_ctx_t* file_table;
};
int Device::devoptab_open(void *fileStruct, const char *path, int flags, int mode) {
auto file = static_cast<File*>(fileStruct);
std::memset(file, 0, sizeof(*file));
char path[FS_MAX_PATH];
if (!common::fix_path(_path, path)) {
return set_errno(r, ENOENT);
if (!save_hierarchical_file_table_get_file_entry_by_path(this->file_table, path, &file->entry)) {
return -ENOENT;
}
if (!save_hierarchical_file_table_get_file_entry_by_path(device->file_table, path, &file->entry)) {
return set_errno(r, ENOENT);
if (!save_open_fat_storage(&this->ctx->save_filesystem_core, &file->storage, file->entry.value.save_file_info.start_block)) {
return -ENOENT;
}
if (!save_open_fat_storage(&device->ctx->save_filesystem_core, &file->storage, file->entry.value.save_file_info.start_block)) {
return set_errno(r, ENOENT);
}
file->device = device;
return r->_errno = 0;
return 0;
}
int devoptab_close(struct _reent *r, void *fd) {
int Device::devoptab_close(void *fd) {
auto file = static_cast<File*>(fd);
std::memset(file, 0, sizeof(*file));
return r->_errno = 0;
return 0;
}
ssize_t devoptab_read(struct _reent *r, void *fd, char *ptr, size_t len) {
ssize_t Device::devoptab_read(void *fd, char *ptr, size_t len) {
auto file = static_cast<File*>(fd);
len = std::min(len, file->entry.value.save_file_info.length - file->off);
if (!len) {
return 0;
}
// todo: maybe eof here?
const auto bytes_read = save_allocation_table_storage_read(&file->storage, ptr, file->off, len);
if (!bytes_read) {
return set_errno(r, ENOENT);
return -ENOENT;
}
file->off += bytes_read;
return bytes_read;
}
off_t devoptab_seek(struct _reent *r, void *fd, off_t pos, int dir) {
ssize_t Device::devoptab_seek(void *fd, off_t pos, int dir) {
auto file = static_cast<File*>(fd);
if (dir == SEEK_CUR) {
pos += file->off;
} else if (dir == SEEK_END) {
pos = file->storage._length;
pos = file->entry.value.save_file_info.length;
}
r->_errno = 0;
return file->off = std::clamp<u64>(pos, 0, file->storage._length);
return file->off = std::clamp<u64>(pos, 0, file->entry.value.save_file_info.length);
}
int devoptab_fstat(struct _reent *r, void *fd, struct stat *st) {
int Device::devoptab_fstat(void *fd, struct stat *st) {
auto file = static_cast<File*>(fd);
log_write("[\t\tDEV] fstat\n");
std::memset(st, 0, sizeof(*st));
st->st_nlink = 1;
st->st_size = file->storage._length;
st->st_size = file->entry.value.save_file_info.length;
st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
return r->_errno = 0;
return 0;
}
DIR_ITER* devoptab_diropen(struct _reent *r, DIR_ITER *dirState, const char *_path) {
auto device = (Device*)r->deviceData;
auto dir = static_cast<Dir*>(dirState->dirStruct);
std::memset(dir, 0, sizeof(*dir));
char path[FS_MAX_PATH];
if (!common::fix_path(_path, path)) {
set_errno(r, ENOENT);
return NULL;
}
int Device::devoptab_diropen(void* fd, const char *path) {
auto dir = static_cast<Dir*>(fd);
if (!std::strcmp(path, "/")) {
save_entry_key_t key{};
const auto idx = save_fs_list_get_index_from_key(&device->file_table->directory_table, &key, NULL);
const auto idx = save_fs_list_get_index_from_key(&this->file_table->directory_table, &key, NULL);
if (idx == 0xFFFFFFFF) {
set_errno(r, ENOENT);
return NULL;
return -ENOENT;
}
if (!save_fs_list_get_value(&device->file_table->directory_table, idx, &dir->entry)) {
set_errno(r, ENOENT);
return NULL;
if (!save_fs_list_get_value(&this->file_table->directory_table, idx, &dir->entry)) {
return -ENOENT;
}
} else if (!save_hierarchical_directory_table_get_file_entry_by_path(device->file_table, path, &dir->entry)) {
set_errno(r, ENOENT);
return NULL;
} else if (!save_hierarchical_directory_table_get_file_entry_by_path(this->file_table, path, &dir->entry)) {
return -ENOENT;
}
dir->device = device;
dir->next_file = dir->entry.value.save_find_position.next_file;
dir->next_directory = dir->entry.value.save_find_position.next_directory;
r->_errno = 0;
return dirState;
return 0;
}
int devoptab_dirreset(struct _reent *r, DIR_ITER *dirState) {
auto dir = static_cast<Dir*>(dirState->dirStruct);
int Device::devoptab_dirreset(void* fd) {
auto dir = static_cast<Dir*>(fd);
dir->next_file = dir->entry.value.save_find_position.next_file;
dir->next_directory = dir->entry.value.save_find_position.next_directory;
return r->_errno = 0;
return 0;
}
int devoptab_dirnext(struct _reent *r, DIR_ITER *dirState, char *filename, struct stat *filestat) {
auto dir = static_cast<Dir*>(dirState->dirStruct);
std::memset(filestat, 0, sizeof(*filestat));
int Device::devoptab_dirnext(void* fd, char *filename, struct stat *filestat) {
auto dir = static_cast<Dir*>(fd);
save_fs_list_entry_t entry{};
if (dir->next_directory) {
// todo: use save_allocation_table_storage_read for faster reads
if (!save_fs_list_get_value(&dir->device->file_table->directory_table, dir->next_directory, &entry)) {
return set_errno(r, ENOENT);
if (!save_fs_list_get_value(&this->file_table->directory_table, dir->next_directory, &entry)) {
return -ENOENT;
}
filestat->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
@@ -174,113 +169,55 @@ int devoptab_dirnext(struct _reent *r, DIR_ITER *dirState, char *filename, struc
}
else if (dir->next_file) {
// todo: use save_allocation_table_storage_read for faster reads
if (!save_fs_list_get_value(&dir->device->file_table->file_table, dir->next_file, &entry)) {
return set_errno(r, ENOENT);
if (!save_fs_list_get_value(&this->file_table->file_table, dir->next_file, &entry)) {
return -ENOENT;
}
filestat->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
// todo: confirm this.
filestat->st_size = entry.value.save_file_info.length;
// filestat->st_size = file->storage.block_size;
dir->next_file = entry.value.next_sibling;
}
else {
return set_errno(r, ENOENT);
return -ENOENT;
}
filestat->st_nlink = 1;
strcpy(filename, entry.name);
std::strcpy(filename, entry.name);
return r->_errno = 0;
return 0;
}
int devoptab_dirclose(struct _reent *r, DIR_ITER *dirState) {
auto dir = static_cast<Dir*>(dirState->dirStruct);
int Device::devoptab_dirclose(void* fd) {
auto dir = static_cast<Dir*>(fd);
std::memset(dir, 0, sizeof(*dir));
return r->_errno = 0;
return 0;
}
int devoptab_lstat(struct _reent *r, const char *_path, struct stat *st) {
auto device = (Device*)r->deviceData;
int Device::devoptab_lstat(const char *path, struct stat *st) {
st->st_nlink = 1;
char path[FS_MAX_PATH];
if (!common::fix_path(_path, path)) {
return set_errno(r, ENOENT);
}
std::memset(st, 0, sizeof(*st));
save_fs_list_entry_t entry{};
// NOTE: this is very slow.
if (save_hierarchical_file_table_get_file_entry_by_path(device->file_table, path, &entry)) {
if (save_hierarchical_file_table_get_file_entry_by_path(this->file_table, path, &entry)) {
st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
st->st_size = entry.value.save_file_info.length;
} else if (save_hierarchical_directory_table_get_file_entry_by_path(device->file_table, path, &entry)) {
} else if (save_hierarchical_directory_table_get_file_entry_by_path(this->file_table, path, &entry)) {
st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
} else {
return set_errno(r, ENOENT);
return -ENOENT;
}
st->st_nlink = 1;
return r->_errno = 0;
return 0;
}
constexpr devoptab_t DEVOPTAB = {
.structSize = sizeof(File),
.open_r = devoptab_open,
.close_r = devoptab_close,
.read_r = devoptab_read,
.seek_r = devoptab_seek,
.fstat_r = devoptab_fstat,
.stat_r = devoptab_lstat,
.dirStateSize = sizeof(Dir),
.diropen_r = devoptab_diropen,
.dirreset_r = devoptab_dirreset,
.dirnext_r = devoptab_dirnext,
.dirclose_r = devoptab_dirclose,
.lstat_r = devoptab_lstat,
};
struct Entry {
Device device{};
devoptab_t devoptab{};
u64 id{};
fs::FsPath mount{};
char name[32]{};
s32 ref_count{};
~Entry() {
RemoveDevice(mount);
save_close_savefile(&device.ctx);
}
};
Mutex g_mutex;
std::array<std::unique_ptr<Entry>, common::MAX_ENTRIES> g_entries;
} // namespace
Result MountSaveSystem(u64 id, fs::FsPath& out_path) {
SCOPED_MUTEX(&g_mutex);
static Mutex mutex{};
SCOPED_MUTEX(&mutex);
// check if we already have the save mounted.
for (auto& e : g_entries) {
if (e && e->id == id) {
e->ref_count++;
out_path = e->mount;
R_SUCCEED();
}
}
// otherwise, find next free entry.
auto itr = std::ranges::find_if(g_entries, [](auto& e){
return !e;
});
R_UNLESS(itr != g_entries.end(), 0x1);
char path[256];
fs::FsPath path{};
std::snprintf(path, sizeof(path), "SYSTEM:/save/%016lx", id);
auto ctx = save_open_savefile(path, 0);
@@ -288,46 +225,18 @@ Result MountSaveSystem(u64 id, fs::FsPath& out_path) {
R_THROW(0x1);
}
log_write("[SAVE] OPEN SUCCESS %s\n", path);
auto entry = std::make_unique<Entry>();
entry->id = id;
entry->device.ctx = ctx;
entry->device.file_table = &ctx->save_filesystem_core.file_table;
entry->devoptab = DEVOPTAB;
entry->devoptab.name = entry->name;
entry->devoptab.deviceData = &entry->device;
std::snprintf(entry->name, sizeof(entry->name), "%016lx", id);
std::snprintf(entry->mount, sizeof(entry->mount), "%016lx:/", id);
R_UNLESS(AddDevice(&entry->devoptab) >= 0, 0x1);
log_write("[SAVE] DEVICE SUCCESS %s %s\n", path, entry->name);
out_path = entry->mount;
entry->ref_count++;
*itr = std::move(entry);
if (!common::MountReadOnlyIndexDevice(
[&ctx](const common::MountConfig& config) {
return std::make_unique<Device>(ctx, config);
},
sizeof(File), sizeof(Dir),
"SAVE", out_path
)) {
log_write("[SAVE] Failed to mount %s\n", path.s);
R_THROW(0x1);
}
R_SUCCEED();
}
void UnmountSave(u64 id) {
SCOPED_MUTEX(&g_mutex);
auto itr = std::ranges::find_if(g_entries, [id](auto& e){
return e && e->id == id;
});
if (itr == g_entries.end()) {
return;
}
if ((*itr)->ref_count) {
(*itr)->ref_count--;
}
if (!(*itr)->ref_count) {
itr->reset();
}
}
} // namespace sphaira::devoptab

View File

@@ -0,0 +1,543 @@
// NOTE (09/09/2025): do not use as it is unusably slow, even on local network.
// the issue isn't the ssh protocol (although it is slow). haven't looked into libssh2 yet
// it could be how they handle blocking. CPU usage is 0%, so its not that.
// NOTE (09/09/2025): its just reads that as super slow, which is even more strange!
// writes are very fast (for sftp), maxing switch wifi. what is going on???
// NOTE (09/09/2025): the issue was that fread was buffering, causing double reads.
// it would read the first 4mb, then read another 1kb.
// disabling buffering fixed the issue, and i have disabled buffering by default.
// buffering is now enabled only when requested.
#include "utils/devoptab_common.hpp"
#include "utils/profile.hpp"
#include "defines.hpp"
#include "log.hpp"
#include <sys/stat.h>
#include <fcntl.h>
#include <cstring>
#include <string>
#include <cstring>
#include <libssh2.h>
#include <libssh2_sftp.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netdb.h>
namespace sphaira::devoptab {
namespace {
struct Device final : common::MountDevice {
using MountDevice::MountDevice;
~Device();
private:
bool Mount() override;
int devoptab_open(void *fileStruct, const char *path, int flags, int mode) override;
int devoptab_close(void *fd) override;
ssize_t devoptab_read(void *fd, char *ptr, size_t len) override;
ssize_t devoptab_write(void *fd, const char *ptr, size_t len) override;
ssize_t devoptab_seek(void *fd, off_t pos, int dir) override;
int devoptab_fstat(void *fd, struct stat *st) override;
int devoptab_unlink(const char *path) override;
int devoptab_rename(const char *oldName, const char *newName) override;
int devoptab_mkdir(const char *path, int mode) override;
int devoptab_rmdir(const char *path) override;
int devoptab_diropen(void* fd, const char *path) override;
int devoptab_dirreset(void* fd) override;
int devoptab_dirnext(void* fd, char *filename, struct stat *filestat) override;
int devoptab_dirclose(void* fd) override;
int devoptab_lstat(const char *path, struct stat *st) override;
int devoptab_ftruncate(void *fd, off_t len) override;
int devoptab_statvfs(const char *path, struct statvfs *buf) override;
int devoptab_fsync(void *fd) override;
private:
LIBSSH2_SESSION* m_session{};
LIBSSH2_SFTP* m_sftp_session{};
int m_socket{};
bool m_is_ssh2_init{}; // set if libssh2_init() was successful.
bool m_is_handshake_done{}; // set if handshake was successful.
bool m_is_auth_done{}; // set if auth was successful.
bool mounted{};
};
struct File {
LIBSSH2_SFTP_HANDLE* fd{};
};
struct Dir {
LIBSSH2_SFTP_HANDLE* fd{};
};
int convert_flags_to_sftp(int flags) {
int sftp_flags = 0;
if ((flags & O_ACCMODE) == O_RDONLY) {
sftp_flags |= LIBSSH2_FXF_READ;
} else if ((flags & O_ACCMODE) == O_WRONLY) {
sftp_flags |= LIBSSH2_FXF_WRITE;
} else if ((flags & O_ACCMODE) == O_RDWR) {
sftp_flags |= LIBSSH2_FXF_READ | LIBSSH2_FXF_WRITE;
}
if (flags & O_CREAT) {
sftp_flags |= LIBSSH2_FXF_CREAT;
}
if (flags & O_TRUNC) {
sftp_flags |= LIBSSH2_FXF_TRUNC;
}
if (flags & O_APPEND) {
sftp_flags |= LIBSSH2_FXF_APPEND;
}
if (flags & O_EXCL) {
sftp_flags |= LIBSSH2_FXF_EXCL;
}
return sftp_flags;
}
int convert_mode_to_sftp(int mode) {
int sftp_mode = 0;
// permission bits.
sftp_mode |= (mode & S_IRUSR) ? LIBSSH2_SFTP_S_IRUSR : 0;
sftp_mode |= (mode & S_IWUSR) ? LIBSSH2_SFTP_S_IWUSR : 0;
sftp_mode |= (mode & S_IXUSR) ? LIBSSH2_SFTP_S_IXUSR : 0;
sftp_mode |= (mode & S_IRGRP) ? LIBSSH2_SFTP_S_IRGRP : 0;
sftp_mode |= (mode & S_IWGRP) ? LIBSSH2_SFTP_S_IWGRP : 0;
sftp_mode |= (mode & S_IXGRP) ? LIBSSH2_SFTP_S_IXGRP : 0;
sftp_mode |= (mode & S_IROTH) ? LIBSSH2_SFTP_S_IROTH : 0;
sftp_mode |= (mode & S_IWOTH) ? LIBSSH2_SFTP_S_IWOTH : 0;
sftp_mode |= (mode & S_IXOTH) ? LIBSSH2_SFTP_S_IXOTH : 0;
// file type bits.
if (S_ISREG(mode)) {
sftp_mode |= LIBSSH2_SFTP_S_IFREG;
} else if (S_ISDIR(mode)) {
sftp_mode |= LIBSSH2_SFTP_S_IFDIR;
} else if (S_ISCHR(mode)) {
sftp_mode |= LIBSSH2_SFTP_S_IFCHR;
} else if (S_ISBLK(mode)) {
sftp_mode |= LIBSSH2_SFTP_S_IFBLK;
} else if (S_ISFIFO(mode)) {
sftp_mode |= LIBSSH2_SFTP_S_IFIFO;
} else if (S_ISLNK(mode)) {
sftp_mode |= LIBSSH2_SFTP_S_IFLNK;
} else if (S_ISSOCK(mode)) {
sftp_mode |= LIBSSH2_SFTP_S_IFSOCK;
}
return sftp_mode;
}
void fill_stat(struct stat* st, const LIBSSH2_SFTP_ATTRIBUTES* attrs) {
if (attrs->flags & LIBSSH2_SFTP_ATTR_PERMISSIONS) {
st->st_mode = attrs->permissions;
}
if (attrs->flags & LIBSSH2_SFTP_ATTR_SIZE) {
st->st_size = attrs->filesize;
}
if (attrs->flags & LIBSSH2_SFTP_ATTR_UIDGID) {
st->st_uid = attrs->uid;
st->st_gid = attrs->gid;
}
if (attrs->flags & LIBSSH2_SFTP_ATTR_ACMODTIME) {
st->st_atime = attrs->atime;
st->st_mtime = attrs->mtime;
st->st_ctime = attrs->mtime; // no ctime available, use mtime.
}
st->st_nlink = 1;
}
Device::~Device() {
if (m_sftp_session) {
libssh2_sftp_shutdown(m_sftp_session);
}
if (m_session) {
libssh2_session_disconnect(m_session, "Normal Shutdown");
libssh2_session_free(m_session);
}
if (m_socket > 0) {
shutdown(m_socket, SHUT_RDWR);
close(m_socket);
}
if (m_is_ssh2_init) {
libssh2_exit();
}
}
bool Device::Mount() {
if (mounted) {
return true;
}
log_write("[SFTP] Mounting %s version: %s\n", this->config.url.c_str(), LIBSSH2_VERSION);
if (!m_socket) {
// connect the socket.
addrinfo hints{};
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
addrinfo* res{};
const auto port = this->config.port > 0 ? this->config.port : 22;
const auto port_str = std::to_string(port);
auto ret = getaddrinfo(this->config.url.c_str(), port_str.c_str(), &hints, &res);
if (ret != 0) {
log_write("[SFTP] getaddrinfo() failed: %s\n", gai_strerror(ret));
return false;
}
ON_SCOPE_EXIT(freeaddrinfo(res));
for (auto addr = res; addr != nullptr; addr = addr->ai_next) {
m_socket = socket(addr->ai_family, addr->ai_socktype, addr->ai_protocol);
if (m_socket < 0) {
log_write("[SFTP] socket() failed: %s\n", std::strerror(errno));
continue;
}
ret = connect(m_socket, addr->ai_addr, addr->ai_addrlen);
if (ret < 0) {
log_write("[SFTP] connect() failed: %s\n", std::strerror(errno));
close(m_socket);
m_socket = -1;
continue;
}
break;
}
if (m_socket < 0) {
log_write("[SFTP] Failed to connect to %s:%ld\n", this->config.url.c_str(), port);
return false;
}
log_write("[SFTP] Connected to %s:%ld\n", this->config.url.c_str(), port);
}
if (!m_is_ssh2_init) {
auto ret = libssh2_init(0);
if (ret != 0) {
log_write("[SFTP] libssh2_init() failed: %d\n", ret);
return false;
}
m_is_ssh2_init = true;
}
if (!m_session) {
m_session = libssh2_session_init();
if (!m_session) {
log_write("[SFTP] libssh2_session_init() failed\n");
return false;
}
libssh2_session_set_blocking(m_session, 1);
libssh2_session_flag(m_session, LIBSSH2_FLAG_COMPRESS, 1);
if (this->config.timeout > 0) {
libssh2_session_set_timeout(m_session, this->config.timeout);
// dkp libssh2 is too old for this.
#if LIBSSH2_VERSION_NUM >= 0x010B00
libssh2_session_set_read_timeout(m_session, this->config.timeout);
#endif
}
}
if (this->config.user.empty() || this->config.pass.empty()) {
log_write("[SFTP] Missing username or password\n");
return false;
}
if (!m_is_handshake_done) {
const auto ret = libssh2_session_handshake(m_session, m_socket);
if (ret) {
log_write("[SFTP] libssh2_session_handshake() failed: %d\n", ret);
return false;
}
m_is_handshake_done = true;
}
if (!m_is_auth_done) {
const auto userauthlist = libssh2_userauth_list(m_session, this->config.user.c_str(), this->config.user.length());
if (!userauthlist) {
log_write("[SFTP] libssh2_userauth_list() failed\n");
return false;
}
// just handle user/pass auth for now, pub/priv key is a bit overkill.
if (std::strstr(userauthlist, "password")) {
auto ret = libssh2_userauth_password(m_session, this->config.user.c_str(), this->config.pass.c_str());
if (ret) {
log_write("[SFTP] Password auth failed: %d\n", ret);
return false;
}
} else {
log_write("[SFTP] No supported auth methods found\n");
return false;
}
m_is_auth_done = true;
}
if (!m_sftp_session) {
m_sftp_session = libssh2_sftp_init(m_session);
if (!m_sftp_session) {
log_write("[SFTP] libssh2_sftp_init() failed\n");
return false;
}
}
log_write("[SFTP] Mounted %s\n", this->config.url.c_str());
return mounted = true;
}
int Device::devoptab_open(void *fileStruct, const char *path, int flags, int mode) {
auto file = static_cast<File*>(fileStruct);
file->fd = libssh2_sftp_open(m_sftp_session, path, convert_flags_to_sftp(flags), convert_mode_to_sftp(mode));
if (!file->fd) {
log_write("[SFTP] libssh2_sftp_open() failed: %ld\n", libssh2_sftp_last_error(m_sftp_session));
return -EIO;
}
return 0;
}
int Device::devoptab_close(void *fd) {
auto file = static_cast<File*>(fd);
libssh2_sftp_close(file->fd);
return 0;
}
ssize_t Device::devoptab_read(void *fd, char *ptr, size_t len) {
auto file = static_cast<File*>(fd);
// enable if sftp read is slow again.
#if 0
char name[256]{};
std::snprintf(name, sizeof(name), "SFTP read %zu bytes", len);
SCOPED_TIMESTAMP(name);
#endif
const auto ret = libssh2_sftp_read(file->fd, ptr, len);
if (ret < 0) {
log_write("[SFTP] libssh2_sftp_read() failed: %ld\n", libssh2_sftp_last_error(m_sftp_session));
return -EIO;
}
return ret;
}
ssize_t Device::devoptab_write(void *fd, const char *ptr, size_t len) {
auto file = static_cast<File*>(fd);
const auto ret = libssh2_sftp_write(file->fd, ptr, len);
if (ret < 0) {
log_write("[SFTP] libssh2_sftp_write() failed: %ld\n", libssh2_sftp_last_error(m_sftp_session));
return -EIO;
}
return ret;
}
ssize_t Device::devoptab_seek(void *fd, off_t pos, int dir) {
auto file = static_cast<File*>(fd);
const auto current_pos = libssh2_sftp_tell64(file->fd);
if (dir == SEEK_CUR) {
pos += current_pos;
} else if (dir == SEEK_END) {
LIBSSH2_SFTP_ATTRIBUTES attrs{};
auto ret = libssh2_sftp_fstat(file->fd, &attrs);
if (ret || !(attrs.flags & LIBSSH2_SFTP_ATTR_SIZE)) {
log_write("[SFTP] libssh2_sftp_fstat() failed: %ld\n", libssh2_sftp_last_error(m_sftp_session));
} else {
pos = attrs.filesize;
}
}
// libssh2 already does this internally, but handle just in case this changes.
if (pos == current_pos) {
return pos;
}
log_write("[SFTP] Seeking to %ld dir: %d old: %llu\n", pos, dir, current_pos);
libssh2_sftp_seek64(file->fd, pos);
return libssh2_sftp_tell64(file->fd);
}
int Device::devoptab_fstat(void *fd, struct stat *st) {
auto file = static_cast<File*>(fd);
LIBSSH2_SFTP_ATTRIBUTES attrs{};
const auto ret = libssh2_sftp_fstat(file->fd, &attrs);
if (ret) {
log_write("[SFTP] libssh2_sftp_fstat() failed: %ld\n", libssh2_sftp_last_error(m_sftp_session));
return -EIO;
}
fill_stat(st, &attrs);
return 0;
}
int Device::devoptab_unlink(const char *path) {
const auto ret = libssh2_sftp_unlink(m_sftp_session, path);
if (ret) {
log_write("[SFTP] libssh2_sftp_unlink() failed: %ld\n", libssh2_sftp_last_error(m_sftp_session));
return -EIO;
}
return 0;
}
int Device::devoptab_rename(const char *oldName, const char *newName) {
const auto ret = libssh2_sftp_rename(m_sftp_session, oldName, newName);
if (ret) {
log_write("[SFTP] libssh2_sftp_rename() failed: %ld\n", libssh2_sftp_last_error(m_sftp_session));
return -EIO;
}
return 0;
}
int Device::devoptab_mkdir(const char *path, int mode) {
const auto ret = libssh2_sftp_mkdir(m_sftp_session, path, mode);
if (ret) {
log_write("[SFTP] libssh2_sftp_mkdir() failed: %ld\n", libssh2_sftp_last_error(m_sftp_session));
return -EIO;
}
return 0;
}
int Device::devoptab_rmdir(const char *path) {
const auto ret = libssh2_sftp_rmdir(m_sftp_session, path);
if (ret) {
log_write("[SFTP] libssh2_sftp_rmdir() failed: %ld\n", libssh2_sftp_last_error(m_sftp_session));
return -EIO;
}
return 0;
}
int Device::devoptab_diropen(void* fd, const char *path) {
auto dir = static_cast<Dir*>(fd);
dir->fd = libssh2_sftp_opendir(m_sftp_session, path);
if (!dir->fd) {
log_write("[SFTP] libssh2_sftp_opendir() failed: %ld\n", libssh2_sftp_last_error(m_sftp_session));
return -EIO;
}
return 0;
}
int Device::devoptab_dirreset(void* fd) {
auto dir = static_cast<Dir*>(fd);
libssh2_sftp_rewind(dir->fd);
return 0;
}
int Device::devoptab_dirnext(void* fd, char *filename, struct stat *filestat) {
auto dir = static_cast<Dir*>(fd);
LIBSSH2_SFTP_ATTRIBUTES attrs{};
const auto ret = libssh2_sftp_readdir(dir->fd, filename, NAME_MAX, &attrs);
if (ret <= 0) {
return -ENOENT;
}
fill_stat(filestat, &attrs);
return 0;
}
int Device::devoptab_dirclose(void* fd) {
auto dir = static_cast<Dir*>(fd);
libssh2_sftp_closedir(dir->fd);
return 0;
}
int Device::devoptab_lstat(const char *path, struct stat *st) {
LIBSSH2_SFTP_ATTRIBUTES attrs{};
const auto ret = libssh2_sftp_stat(m_sftp_session, path, &attrs);
if (ret) {
log_write("[SFTP] libssh2_sftp_stat() failed: %ld\n", libssh2_sftp_last_error(m_sftp_session));
return -EIO;
}
fill_stat(st, &attrs);
return 0;
}
#if 1
int Device::devoptab_ftruncate(void *fd, off_t len) {
// stubbed.
return 0;
}
#endif
int Device::devoptab_statvfs(const char *path, struct statvfs *buf) {
LIBSSH2_SFTP_STATVFS sftp_st{};
const auto ret = libssh2_sftp_statvfs(m_sftp_session, path, std::strlen(path), &sftp_st);
if (ret) {
log_write("[SFTP] libssh2_sftp_statvfs() failed: %ld\n", libssh2_sftp_last_error(m_sftp_session));
return -EIO;
}
buf->f_bsize = sftp_st.f_bsize;
buf->f_frsize = sftp_st.f_frsize;
buf->f_blocks = sftp_st.f_blocks;
buf->f_bfree = sftp_st.f_bfree;
buf->f_bavail = sftp_st.f_bavail;
buf->f_files = sftp_st.f_files;
buf->f_ffree = sftp_st.f_ffree;
buf->f_favail = sftp_st.f_favail;
buf->f_fsid = sftp_st.f_fsid;
buf->f_flag = sftp_st.f_flag;
buf->f_namemax = sftp_st.f_namemax;
return 0;
}
int Device::devoptab_fsync(void *fd) {
auto file = static_cast<File*>(fd);
const auto ret = libssh2_sftp_fsync(file->fd);
if (ret) {
log_write("[SFTP] libssh2_sftp_fsync() failed: %ld\n", libssh2_sftp_last_error(m_sftp_session));
return -EIO;
}
return 0;
}
} // namespace
Result MountSftpAll() {
return common::MountNetworkDevice([](const common::MountConfig& cfg) {
return std::make_unique<Device>(cfg);
},
sizeof(File), sizeof(Dir),
"SFTP"
);
}
} // namespace sphaira::devoptab

View File

@@ -0,0 +1,402 @@
#include "utils/devoptab_common.hpp"
#include "defines.hpp"
#include "log.hpp"
#include <sys/stat.h>
#include <fcntl.h>
#include <cstring>
#include <string>
#include <cstring>
#include <smb2/smb2.h>
#include <smb2/libsmb2.h>
#include <minIni.h>
namespace sphaira::devoptab {
namespace {
struct Device final : common::MountDevice {
using MountDevice::MountDevice;
~Device();
private:
bool fix_path(const char* str, char* out, bool strip_leading_slash = false) override {
return common::fix_path(str, out, true);
}
bool Mount() override;
int devoptab_open(void *fileStruct, const char *path, int flags, int mode) override;
int devoptab_close(void *fd) override;
ssize_t devoptab_read(void *fd, char *ptr, size_t len) override;
ssize_t devoptab_write(void *fd, const char *ptr, size_t len) override;
ssize_t devoptab_seek(void *fd, off_t pos, int dir) override;
int devoptab_fstat(void *fd, struct stat *st) override;
int devoptab_unlink(const char *path) override;
int devoptab_rename(const char *oldName, const char *newName) override;
int devoptab_mkdir(const char *path, int mode) override;
int devoptab_rmdir(const char *path) override;
int devoptab_diropen(void* fd, const char *path) override;
int devoptab_dirreset(void* fd) override;
int devoptab_dirnext(void* fd, char *filename, struct stat *filestat) override;
int devoptab_dirclose(void* fd) override;
int devoptab_lstat(const char *path, struct stat *st) override;
int devoptab_ftruncate(void *fd, off_t len) override;
int devoptab_statvfs(const char *path, struct statvfs *buf) override;
int devoptab_fsync(void *fd) override;
private:
smb2_context* smb2{};
bool mounted{};
};
struct File {
smb2fh* fd;
};
struct Dir {
smb2dir* dir;
};
void fill_stat(struct stat* st, const smb2_stat_64* smb2_st) {
if (smb2_st->smb2_type == SMB2_TYPE_FILE) {
st->st_mode = S_IFREG;
} else if (smb2_st->smb2_type == SMB2_TYPE_DIRECTORY) {
st->st_mode = S_IFDIR;
} else if (smb2_st->smb2_type == SMB2_TYPE_LINK) {
st->st_mode = S_IFLNK;
} else {
log_write("[SMB2] Unknown file type: %u\n", smb2_st->smb2_type);
st->st_mode = S_IFCHR; // will be skipped by stdio readdir wrapper.
}
st->st_ino = smb2_st->smb2_ino;
st->st_nlink = smb2_st->smb2_nlink;
st->st_size = smb2_st->smb2_size;
st->st_atime = smb2_st->smb2_atime;
st->st_mtime = smb2_st->smb2_mtime;
st->st_ctime = smb2_st->smb2_ctime;
}
Device::~Device() {
if (this->smb2) {
if (this->mounted) {
smb2_disconnect_share(this->smb2);
}
smb2_destroy_context(this->smb2);
}
}
bool Device::Mount() {
if (mounted) {
return true;
}
if (!this->smb2) {
this->smb2 = smb2_init_context();
if (!this->smb2) {
log_write("[SMB2] smb2_init_context() failed\n");
return false;
}
smb2_set_security_mode(this->smb2, SMB2_NEGOTIATE_SIGNING_ENABLED);
if (!this->config.user.empty()) {
smb2_set_user(this->smb2, this->config.user.c_str());
}
if (!this->config.pass.empty()) {
smb2_set_password(this->smb2, this->config.pass.c_str());
}
const auto domain = this->config.extra.find("domain");
if (domain != this->config.extra.end()) {
smb2_set_domain(this->smb2, domain->second.c_str());
}
const auto workstation = this->config.extra.find("workstation");
if (workstation != this->config.extra.end()) {
smb2_set_workstation(this->smb2, workstation->second.c_str());
}
if (config.timeout > 0) {
smb2_set_timeout(this->smb2, this->config.timeout);
}
}
// due to a bug in old sphira, i incorrectly prepended the url with smb:// rather than smb2://
auto url = this->config.url;
if (!url.ends_with('/')) {
url += '/';
}
auto smb2_url = smb2_parse_url(this->smb2, url.c_str());
if (!smb2_url) {
log_write("[SMB2] smb2_parse_url() failed: %s\n", smb2_get_error(this->smb2));
return false;
}
ON_SCOPE_EXIT(smb2_destroy_url(smb2_url));
const auto ret = smb2_connect_share(this->smb2, smb2_url->server, smb2_url->share, smb2_url->user);
if (ret) {
log_write("[SMB2] smb2_connect_share() failed: %s errno: %s\n", smb2_get_error(this->smb2), std::strerror(-ret));
return false;
}
this->mounted = true;
return true;
}
int Device::devoptab_open(void *fileStruct, const char *path, int flags, int mode) {
auto file = static_cast<File*>(fileStruct);
file->fd = smb2_open(this->smb2, path, flags);
if (!file->fd) {
log_write("[SMB2] smb2_open() failed: %s\n", smb2_get_error(this->smb2));
return -EIO;
}
return 0;
}
int Device::devoptab_close(void *fd) {
auto file = static_cast<File*>(fd);
smb2_close(this->smb2, file->fd);
return 0;
}
ssize_t Device::devoptab_read(void *fd, char *ptr, size_t len) {
auto file = static_cast<File*>(fd);
const auto max_read = smb2_get_max_read_size(this->smb2);
size_t bytes_read = 0;
while (bytes_read < len) {
const auto to_read = std::min<size_t>(len - bytes_read, max_read);
const auto ret = smb2_read(this->smb2, file->fd, (u8*)ptr, to_read);
if (ret < 0) {
log_write("[SMB2] smb2_read() failed: %s errno: %s\n", smb2_get_error(this->smb2), std::strerror(-ret));
return ret;
}
ptr += ret;
bytes_read += ret;
if (ret < to_read) {
break;
}
}
return bytes_read;
}
ssize_t Device::devoptab_write(void *fd, const char *ptr, size_t len) {
auto file = static_cast<File*>(fd);
const auto max_write = smb2_get_max_write_size(this->smb2);
size_t written = 0;
while (written < len) {
const auto to_write = std::min<size_t>(len - written, max_write);
const auto ret = smb2_write(this->smb2, file->fd, (const u8*)ptr, to_write);
if (ret < 0) {
log_write("[SMB2] smb2_write() failed: %s errno: %s\n", smb2_get_error(this->smb2), std::strerror(-ret));
return ret;
}
ptr += ret;
written += ret;
if (ret < to_write) {
break;
}
}
return written;
}
ssize_t Device::devoptab_seek(void *fd, off_t pos, int dir) {
auto file = static_cast<File*>(fd);
u64 current_offset = 0;
const auto ret = smb2_lseek(this->smb2, file->fd, pos, dir, &current_offset);
if (ret < 0) {
log_write("[SMB2] smb2_lseek() failed: %s errno: %s\n", smb2_get_error(this->smb2), std::strerror(-ret));
return ret;
}
return current_offset;
}
int Device::devoptab_fstat(void *fd, struct stat *st) {
auto file = static_cast<File*>(fd);
smb2_stat_64 smb2_st{};
const auto ret = smb2_fstat(this->smb2, file->fd, &smb2_st);
if (ret < 0) {
log_write("[SMB2] smb2_fstat() failed: %s errno: %s\n", smb2_get_error(this->smb2), std::strerror(-ret));
return ret;
}
fill_stat(st, &smb2_st);
return 0;
}
int Device::devoptab_unlink(const char *path) {
const auto ret = smb2_unlink(this->smb2, path);
if (ret) {
log_write("[SMB2] smb2_unlink() failed: %s errno: %s\n", smb2_get_error(this->smb2), std::strerror(-ret));
return ret;
}
return 0;
}
int Device::devoptab_rename(const char *oldName, const char *newName) {
const auto ret = smb2_rename(this->smb2, oldName, newName);
if (ret) {
log_write("[SMB2] smb2_rename() failed: %s errno: %s\n", smb2_get_error(this->smb2), std::strerror(-ret));
return ret;
}
return 0;
}
int Device::devoptab_mkdir(const char *path, int mode) {
const auto ret = smb2_mkdir(this->smb2, path);
if (ret) {
log_write("[SMB2] smb2_mkdir() failed: %s errno: %s\n", smb2_get_error(this->smb2), std::strerror(-ret));
return ret;
}
return 0;
}
int Device::devoptab_rmdir(const char *path) {
const auto ret = smb2_rmdir(this->smb2, path);
if (ret) {
log_write("[SMB2] smb2_rmdir() failed: %s errno: %s\n", smb2_get_error(this->smb2), std::strerror(-ret));
return ret;
}
return 0;
}
int Device::devoptab_diropen(void* fd, const char *path) {
auto dir = static_cast<Dir*>(fd);
dir->dir = smb2_opendir(this->smb2, path);
if (!dir->dir) {
log_write("[SMB2] smb2_opendir() failed: %s\n", smb2_get_error(this->smb2));
return -EIO;
}
return 0;
}
int Device::devoptab_dirreset(void* fd) {
auto dir = static_cast<Dir*>(fd);
smb2_rewinddir(this->smb2, dir->dir);
return 0;
}
int Device::devoptab_dirnext(void* fd, char *filename, struct stat *filestat) {
auto dir = static_cast<Dir*>(fd);
if (!dir->dir) {
return EINVAL;
}
const auto entry = smb2_readdir(this->smb2, dir->dir);
if (!entry) {
return -ENOENT;
}
std::strncpy(filename, entry->name, NAME_MAX);
filename[NAME_MAX - 1] = '\0';
fill_stat(filestat, &entry->st);
return 0;
}
int Device::devoptab_dirclose(void* fd) {
auto dir = static_cast<Dir*>(fd);
smb2_closedir(this->smb2, dir->dir);
return 0;
}
int Device::devoptab_lstat(const char *path, struct stat *st) {
smb2_stat_64 smb2_st{};
const auto ret = smb2_stat(this->smb2, path, &smb2_st);
if (ret) {
log_write("[SMB2] smb2_stat() failed: %s errno: %s\n", smb2_get_error(this->smb2), std::strerror(-ret));
return ret;
}
fill_stat(st, &smb2_st);
return 0;
}
int Device::devoptab_ftruncate(void *fd, off_t len) {
auto file = static_cast<File*>(fd);
const auto ret = smb2_ftruncate(this->smb2, file->fd, len);
if (ret) {
log_write("[SMB2] smb2_ftruncate() failed: %s errno: %s\n", smb2_get_error(this->smb2), std::strerror(-ret));
return ret;
}
return 0;
}
int Device::devoptab_statvfs(const char *path, struct statvfs *buf) {
struct smb2_statvfs smb2_st{};
const auto ret = smb2_statvfs(this->smb2, path, &smb2_st);
if (ret) {
log_write("[SMB2] smb2_statvfs() failed: %s errno: %s\n", smb2_get_error(this->smb2), std::strerror(-ret));
return ret;
}
buf->f_bsize = smb2_st.f_bsize;
buf->f_frsize = smb2_st.f_frsize;
buf->f_blocks = smb2_st.f_blocks;
buf->f_bfree = smb2_st.f_bfree;
buf->f_bavail = smb2_st.f_bavail;
buf->f_files = smb2_st.f_files;
buf->f_ffree = smb2_st.f_ffree;
buf->f_favail = smb2_st.f_favail;
buf->f_fsid = smb2_st.f_fsid;
buf->f_flag = smb2_st.f_flag;
buf->f_namemax = smb2_st.f_namemax;
return 0;
}
int Device::devoptab_fsync(void *fd) {
auto file = static_cast<File*>(fd);
const auto ret = smb2_fsync(this->smb2, file->fd);
if (ret) {
log_write("[SMB2] smb2_fsync() failed: %s errno: %s\n", smb2_get_error(this->smb2), std::strerror(-ret));
return ret;
}
return 0;
}
} // namespace
Result MountSmb2All() {
return common::MountNetworkDevice([](const common::MountConfig& cfg) {
return std::make_unique<Device>(cfg);
},
sizeof(File), sizeof(Dir),
"SMB"
);
}
} // namespace sphaira::devoptab

View File

@@ -0,0 +1,284 @@
#include "utils/devoptab_common.hpp"
#include "defines.hpp"
#include "log.hpp"
#include <sys/stat.h>
#include <fcntl.h>
#include <cstring>
#include <string>
#include <cstring>
namespace sphaira::devoptab {
namespace {
struct Device final : common::MountDevice {
Device(const common::MountConfig& _config)
: common::MountDevice{_config}
, m_root{config.url} {
}
private:
bool fix_path(const char* str, char* out, bool strip_leading_slash = false) override {
char temp[PATH_MAX]{};
if (!common::fix_path(str, temp, false)) {
return false;
}
std::snprintf(out, PATH_MAX, "%s/%s", m_root.c_str(), temp);
log_write("[VFS] fixed path: %s -> %s\n", str, out);
return true;
}
bool Mount() override;
int devoptab_open(void *fileStruct, const char *path, int flags, int mode) override;
int devoptab_close(void *fd) override;
ssize_t devoptab_read(void *fd, char *ptr, size_t len) override;
ssize_t devoptab_write(void *fd, const char *ptr, size_t len) override;
ssize_t devoptab_seek(void *fd, off_t pos, int dir) override;
int devoptab_fstat(void *fd, struct stat *st) override;
int devoptab_unlink(const char *path) override;
int devoptab_rename(const char *oldName, const char *newName) override;
int devoptab_mkdir(const char *path, int mode) override;
int devoptab_rmdir(const char *path) override;
int devoptab_diropen(void* fd, const char *path) override;
int devoptab_dirreset(void* fd) override;
int devoptab_dirnext(void* fd, char *filename, struct stat *filestat) override;
int devoptab_dirclose(void* fd) override;
int devoptab_lstat(const char *path, struct stat *st) override;
int devoptab_ftruncate(void *fd, off_t len) override;
int devoptab_statvfs(const char *path, struct statvfs *buf) override;
int devoptab_fsync(void *fd) override;
int devoptab_utimes(const char *path, const struct timeval times[2]) override;
private:
const std::string m_root{};
bool mounted{};
};
struct File {
int fd;
};
struct Dir {
DIR* dir;
};
bool Device::Mount() {
if (mounted) {
return true;
}
log_write("[VFS] Mounting %s\n", this->config.url.c_str());
if (m_root.empty()) {
log_write("[VFS] Empty root path\n");
return false;
}
log_write("[VFS] Mounted %s\n", this->config.url.c_str());
return mounted = true;
}
int return_errno(int err = EIO) {
return errno ? -errno : -err;
}
int Device::devoptab_open(void *fileStruct, const char *path, int flags, int mode) {
auto file = static_cast<File*>(fileStruct);
const auto ret = open(path, flags, mode);
if (ret < 0) {
return return_errno();
}
file->fd = ret;
return 0;
}
int Device::devoptab_close(void *fd) {
auto file = static_cast<File*>(fd);
close(file->fd);
return 0;
}
ssize_t Device::devoptab_read(void *fd, char *ptr, size_t len) {
auto file = static_cast<File*>(fd);
const auto ret = read(file->fd, ptr, len);
if (ret < 0) {
return return_errno();
}
return ret;
}
ssize_t Device::devoptab_write(void *fd, const char *ptr, size_t len) {
auto file = static_cast<File*>(fd);
const auto ret = write(file->fd, ptr, len);
if (ret < 0) {
return return_errno();
}
return ret;
}
ssize_t Device::devoptab_seek(void *fd, off_t pos, int dir) {
auto file = static_cast<File*>(fd);
return lseek(file->fd, pos, dir);
}
int Device::devoptab_fstat(void *fd, struct stat *st) {
auto file = static_cast<File*>(fd);
const auto ret = fstat(file->fd, st);
if (ret < 0) {
return return_errno();
}
return 0;
}
int Device::devoptab_unlink(const char *path) {
const auto ret = unlink(path);
if (ret < 0) {
return return_errno();
}
return 0;
}
int Device::devoptab_rename(const char *oldName, const char *newName) {
const auto ret = rename(oldName, newName);
if (ret < 0) {
return return_errno();
}
return 0;
}
int Device::devoptab_mkdir(const char *path, int mode) {
const auto ret = mkdir(path, mode);
if (ret < 0) {
return return_errno();
}
return 0;
}
int Device::devoptab_rmdir(const char *path) {
const auto ret = rmdir(path);
if (ret < 0) {
return return_errno();
}
return 0;
}
int Device::devoptab_diropen(void* fd, const char *path) {
auto dir = static_cast<Dir*>(fd);
auto ret = opendir(path);
if (!ret) {
return return_errno();
}
dir->dir = ret;
return 0;
}
int Device::devoptab_dirreset(void* fd) {
auto dir = static_cast<Dir*>(fd);
rewinddir(dir->dir);
return 0;
}
int Device::devoptab_dirnext(void* fd, char *filename, struct stat *filestat) {
auto dir = static_cast<Dir*>(fd);
const auto entry = readdir(dir->dir);
if (!entry) {
return return_errno(ENOENT);
}
filestat->st_ino = entry->d_ino;
filestat->st_mode = entry->d_type << 12; // DT_* to S_IF*
filestat->st_nlink = 1; // unknown
std::strncpy(filename, entry->d_name, NAME_MAX);
filename[NAME_MAX - 1] = '\0';
return 0;
}
int Device::devoptab_dirclose(void* fd) {
auto dir = static_cast<Dir*>(fd);
closedir(dir->dir);
return 0;
}
int Device::devoptab_lstat(const char *path, struct stat *st) {
const auto ret = lstat(path, st);
if (ret < 0) {
return return_errno();
}
return 0;
}
int Device::devoptab_ftruncate(void *fd, off_t len) {
auto file = static_cast<File*>(fd);
const auto ret = ftruncate(file->fd, len);
if (ret < 0) {
return return_errno();
}
return 0;
}
int Device::devoptab_statvfs(const char *path, struct statvfs *buf) {
const auto ret = statvfs(path, buf);
if (ret < 0) {
return return_errno();
}
return 0;
}
int Device::devoptab_fsync(void *fd) {
auto file = static_cast<File*>(fd);
const auto ret = fsync(file->fd);
if (ret < 0) {
return return_errno();
}
return 0;
}
int Device::devoptab_utimes(const char *path, const struct timeval times[2]) {
const auto ret = utimes(path, times);
if (ret < 0) {
return return_errno();
}
return 0;
}
} // namespace
Result MountVfsAll() {
return common::MountNetworkDevice([](const common::MountConfig& cfg) {
return std::make_unique<Device>(cfg);
},
sizeof(File), sizeof(Dir),
"VFS"
);
}
} // namespace sphaira::devoptab

View File

@@ -0,0 +1,654 @@
#include "utils/devoptab_common.hpp"
#include "utils/profile.hpp"
#include "log.hpp"
#include "defines.hpp"
#include <fcntl.h>
#include <curl/curl.h>
#include <string>
#include <vector>
#include <memory>
#include <cstring>
#include <optional>
#include <sys/stat.h>
// todo: try to reduce binary size by using a smaller xml parser.
#include <pugixml.hpp>
namespace sphaira::devoptab {
namespace {
constexpr const char* XPATH_RESPONSE = "//*[local-name()='response']";
constexpr const char* XPATH_HREF = ".//*[local-name()='href']";
constexpr const char* XPATH_PROPSTAT_PROP = ".//*[local-name()='propstat']/*[local-name()='prop']";
constexpr const char* XPATH_PROP = ".//*[local-name()='prop']";
constexpr const char* XPATH_RESOURCETYPE = ".//*[local-name()='resourcetype']";
constexpr const char* XPATH_COLLECTION = ".//*[local-name()='collection']";
struct DirEntry {
std::string name{};
bool is_dir{};
};
using DirEntries = std::vector<DirEntry>;
struct FileEntry {
std::string path{};
struct stat st{};
};
struct File {
FileEntry* entry;
common::PushPullThreadData* push_pull_thread_data;
size_t off;
size_t last_off;
bool write_mode;
};
struct Dir {
DirEntries* entries;
size_t index;
};
struct Device final : common::MountCurlDevice {
using MountCurlDevice::MountCurlDevice;
private:
int devoptab_open(void *fileStruct, const char *path, int flags, int mode) override;
int devoptab_close(void *fd) override;
ssize_t devoptab_read(void *fd, char *ptr, size_t len) override;
ssize_t devoptab_write(void *fd, const char *ptr, size_t len) override;
ssize_t devoptab_seek(void *fd, off_t pos, int dir) override;
int devoptab_fstat(void *fd, struct stat *st) override;
int devoptab_unlink(const char *path) override;
int devoptab_rename(const char *oldName, const char *newName) override;
int devoptab_mkdir(const char *path, int mode) override;
int devoptab_rmdir(const char *path) override;
int devoptab_diropen(void* fd, const char *path) override;
int devoptab_dirreset(void* fd) override;
int devoptab_dirnext(void* fd, char *filename, struct stat *filestat) override;
int devoptab_dirclose(void* fd) override;
int devoptab_lstat(const char *path, struct stat *st) override;
int devoptab_ftruncate(void *fd, off_t len) override;
int devoptab_fsync(void *fd) override;
std::pair<bool, long> webdav_custom_command(const std::string& path, const std::string& cmd, std::string_view postfields, std::span<const std::string> headers, bool is_dir, std::vector<char>* response_data = nullptr);
int webdav_dirlist(const std::string& path, DirEntries& out);
int webdav_stat(const std::string& path, struct stat* st, bool is_dir);
int webdav_remove_file_folder(const std::string& path, bool is_dir);
int webdav_unlink(const std::string& path);
int webdav_rename(const std::string& old_path, const std::string& new_path, bool is_dir);
int webdav_mkdir(const std::string& path);
int webdav_rmdir(const std::string& path);
};
size_t dummy_data_callback(char *ptr, size_t size, size_t nmemb, void *userdata) {
return size * nmemb;
}
std::pair<bool, long> Device::webdav_custom_command(const std::string& path, const std::string& cmd, std::string_view postfields, std::span<const std::string> headers, bool is_dir, std::vector<char>* response_data) {
const auto url = build_url(path, is_dir);
curl_slist* header_list{};
ON_SCOPE_EXIT(curl_slist_free_all(header_list));
for (const auto& header : headers) {
log_write("[WEBDAV] Header: %s\n", header.c_str());
header_list = curl_slist_append(header_list, header.c_str());
}
log_write("[WEBDAV] %s %s\n", cmd.c_str(), url.c_str());
curl_set_common_options(this->curl, url);
curl_easy_setopt(this->curl, CURLOPT_HTTPHEADER, header_list);
curl_easy_setopt(this->curl, CURLOPT_CUSTOMREQUEST, cmd.c_str());
if (!postfields.empty()) {
log_write("[WEBDAV] Post fields: %.*s\n", (int)postfields.length(), postfields.data());
curl_easy_setopt(this->curl, CURLOPT_POSTFIELDS, postfields.data());
curl_easy_setopt(this->curl, CURLOPT_POSTFIELDSIZE, (long)postfields.length());
}
if (response_data) {
response_data->clear();
curl_easy_setopt(this->curl, CURLOPT_WRITEFUNCTION, write_memory_callback);
curl_easy_setopt(this->curl, CURLOPT_WRITEDATA, (void *)response_data);
} else {
curl_easy_setopt(this->curl, CURLOPT_WRITEFUNCTION, dummy_data_callback);
}
const auto res = curl_easy_perform(this->curl);
if (res != CURLE_OK) {
log_write("[WEBDAV] curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
return {false, 0};
}
long response_code = 0;
curl_easy_getinfo(this->curl, CURLINFO_RESPONSE_CODE, &response_code);
return {true, response_code};
}
int Device::webdav_dirlist(const std::string& path, DirEntries& out) {
const std::string_view post_fields =
"<?xml version=\"1.0\" encoding=\"utf-8\" ?>"
"<d:propfind xmlns:d=\"DAV:\">"
"<d:prop>"
// "<d:getcontentlength/>"
"<d:resourcetype/>"
"</d:prop>"
"</d:propfind>";
const std::string custom_headers[] = {
"Content-Type: application/xml; charset=utf-8",
"Depth: 1"
};
std::vector<char> chunk;
const auto [success, response_code] = webdav_custom_command(path, "PROPFIND", post_fields, custom_headers, true, &chunk);
if (!success) {
return -EIO;
}
switch (response_code) {
case 207: // Multi-Status
break;
case 404: // Not Found
return -ENOENT;
case 403: // Forbidden
return -EACCES;
default:
log_write("[WEBDAV] Unexpected HTTP response code: %ld\n", response_code);
return -EIO;
}
SCOPED_TIMESTAMP("webdav_dirlist parse");
pugi::xml_document doc;
const auto result = doc.load_buffer_inplace(chunk.data(), chunk.size());
if (!result) {
log_write("[WEBDAV] Failed to parse XML: %s\n", result.description());
return -EIO;
}
log_write("\n[WEBDAV] XML parsed successfully\n");
auto requested_path = url_decode(path);
if (!requested_path.empty() && requested_path.back() == '/') {
requested_path.pop_back();
}
const auto responses = doc.select_nodes(XPATH_RESPONSE);
for (const auto& rnode : responses) {
const auto response = rnode.node();
if (!response) {
continue;
}
const auto href_x = response.select_node(XPATH_HREF);
if (!href_x) {
continue;
}
// todo: fix requested path still being displayed.
const auto href = url_decode(href_x.node().text().as_string());
if (href.empty() || href == requested_path || href == requested_path + '/') {
continue;
}
// propstat/prop/resourcetype
auto prop_x = response.select_node(XPATH_PROPSTAT_PROP);
if (!prop_x) {
// try direct prop if structure differs
prop_x = response.select_node(XPATH_PROP);
if (!prop_x) {
continue;
}
}
const auto prop = prop_x.node();
const auto rtype_x = prop.select_node(XPATH_RESOURCETYPE);
bool is_dir = false;
if (rtype_x && rtype_x.node().select_node(XPATH_COLLECTION)) {
is_dir = true;
}
auto name = href;
if (!name.empty() && name.back() == '/') {
name.pop_back();
}
const auto pos = name.find_last_of('/');
if (pos != std::string::npos) {
name = name.substr(pos + 1);
}
// skip root entry
if (name.empty() || name == ".") {
continue;
}
out.emplace_back(name, is_dir);
}
log_write("[WEBDAV] Parsed %zu entries from directory listing\n", out.size());
return 0;
}
// todo: use PROPFIND to get file size and time, although it is slower...
int Device::webdav_stat(const std::string& path, struct stat* st, bool is_dir) {
std::memset(st, 0, sizeof(*st));
const auto url = build_url(path, is_dir);
curl_set_common_options(this->curl, url);
curl_easy_setopt(this->curl, CURLOPT_NOBODY, 1L);
curl_easy_setopt(this->curl, CURLOPT_FILETIME, 1L);
const auto res = curl_easy_perform(this->curl);
if (res != CURLE_OK) {
log_write("[WEBDAV] curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
return -EIO;
}
long response_code = 0;
curl_easy_getinfo(this->curl, CURLINFO_RESPONSE_CODE, &response_code);
curl_off_t file_size = 0;
curl_easy_getinfo(this->curl, CURLINFO_CONTENT_LENGTH_DOWNLOAD_T, &file_size);
curl_off_t file_time = 0;
curl_easy_getinfo(this->curl, CURLINFO_FILETIME_T, &file_time);
const char* content_type{};
curl_easy_getinfo(this->curl, CURLINFO_CONTENT_TYPE, &content_type);
const char* effective_url{};
curl_easy_getinfo(this->curl, CURLINFO_EFFECTIVE_URL, &effective_url);
switch (response_code) {
case 200: // OK
case 206: // Partial Content
break;
case 404: // Not Found
return -ENOENT;
case 403: // Forbidden
return -EACCES;
default:
log_write("[WEBDAV] Unexpected HTTP response code: %ld\n", response_code);
return -EIO;
}
if (effective_url) {
if (std::string_view{effective_url}.ends_with('/')) {
is_dir = true;
}
}
if (content_type && !std::strcmp(content_type, "text/html")) {
is_dir = true;
}
if (is_dir) {
st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
} else {
st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
st->st_size = file_size > 0 ? file_size : 0;
}
st->st_mtime = file_time > 0 ? file_time : 0;
st->st_atime = st->st_mtime;
st->st_ctime = st->st_mtime;
st->st_nlink = 1;
return 0;
}
int Device::webdav_remove_file_folder(const std::string& path, bool is_dir) {
const auto [success, response_code] = webdav_custom_command(path, "DELETE", "", {}, is_dir);
if (!success) {
return -EIO;
}
switch (response_code) {
case 200: // OK
case 204: // No Content
return 0;
case 404: // Not Found
return -ENOENT;
case 403: // Forbidden
return -EACCES;
case 409: // Conflict
return -ENOTEMPTY; // Directory not empty
default:
return -EIO;
}
}
int Device::webdav_unlink(const std::string& path) {
return webdav_remove_file_folder(path, false);
}
int Device::webdav_rename(const std::string& old_path, const std::string& new_path, bool is_dir) {
log_write("[WEBDAV] Renaming %s to %s\n", old_path.c_str(), new_path.c_str());
const std::string custom_headers[] = {
"Destination: " + build_url(new_path, is_dir),
"Overwrite: T",
};
const auto [success, response_code] = webdav_custom_command(old_path, "MOVE", "", custom_headers, is_dir);
if (!success) {
return -EIO;
}
switch (response_code) {
case 201: // Created
case 204: // No Content
return 0;
case 404: // Not Found
return -ENOENT;
case 403: // Forbidden
return -EACCES;
case 412: // Precondition Failed
return -EEXIST; // Destination already exists and Overwrite is F
case 409: // Conflict
return -ENOENT; // Parent directory of destination does not exist
default:
return -EIO;
}
}
int Device::webdav_mkdir(const std::string& path) {
const auto [success, response_code] = webdav_custom_command(path, "MKCOL", "", {}, true);
if (!success) {
return -EIO;
}
switch (response_code) {
case 201: // Created
return 0;
case 405: // Method Not Allowed
return -EEXIST; // Collection already exists
case 409: // Conflict
return -ENOENT; // Parent collection does not exist
case 403: // Forbidden
return -EACCES;
default:
return -EIO;
}
}
int Device::webdav_rmdir(const std::string& path) {
return webdav_remove_file_folder(path, true);
}
int Device::devoptab_open(void *fileStruct, const char *path, int flags, int mode) {
auto file = static_cast<File*>(fileStruct);
struct stat st{};
// append mode is not supported.
if (flags & O_APPEND) {
return -E2BIG;
}
if ((flags & O_ACCMODE) == O_RDONLY) {
// ensure the file exists and get its size.
const auto ret = webdav_stat(path, &st, false);
if (ret < 0) {
return ret;
}
if (st.st_mode & S_IFDIR) {
log_write("[WEBDAV] Path is a directory, not a file: %s\n", path);
return -EISDIR;
}
}
log_write("[WEBDAV] Opening file: %s\n", path);
file->entry = new FileEntry{path, st};
file->write_mode = (flags & (O_WRONLY | O_RDWR));
return 0;
}
int Device::devoptab_close(void *fd) {
auto file = static_cast<File*>(fd);
log_write("[WEBDAV] Closing file: %s\n", file->entry->path.c_str());
delete file->push_pull_thread_data;
delete file->entry;
return 0;
}
ssize_t Device::devoptab_read(void *fd, char *ptr, size_t len) {
auto file = static_cast<File*>(fd);
len = std::min(len, file->entry->st.st_size - file->off);
if (file->write_mode) {
log_write("[WEBDAV] Attempt to read from a write-only file\n");
return -EBADF;
}
if (!len) {
return 0;
}
if (file->off != file->last_off) {
log_write("[WEBDAV] File offset changed from %zu to %zu, resetting download thread\n", file->last_off, file->off);
file->last_off = file->off;
delete file->push_pull_thread_data;
file->push_pull_thread_data = nullptr;
}
if (!file->push_pull_thread_data) {
log_write("[WEBDAV] Creating download thread data for file: %s\n", file->entry->path.c_str());
file->push_pull_thread_data = CreatePushData(this->transfer_curl, build_url(file->entry->path, false), file->off);
if (!file->push_pull_thread_data) {
log_write("[WEBDAV] Failed to create download thread data for file: %s\n", file->entry->path.c_str());
return -EIO;
}
}
const auto ret = file->push_pull_thread_data->PullData(ptr, len);
file->off += ret;
file->last_off = file->off;
return ret;
}
ssize_t Device::devoptab_write(void *fd, const char *ptr, size_t len) {
auto file = static_cast<File*>(fd);
if (!file->write_mode) {
log_write("[WEBDAV] Attempt to write to a read-only file\n");
return -EBADF;
}
if (!len) {
return 0;
}
if (!file->push_pull_thread_data) {
log_write("[WEBDAV] Creating upload thread data for file: %s\n", file->entry->path.c_str());
file->push_pull_thread_data = CreatePullData(this->transfer_curl, build_url(file->entry->path, false));
if (!file->push_pull_thread_data) {
log_write("[WEBDAV] Failed to create upload thread data for file: %s\n", file->entry->path.c_str());
return -EIO;
}
}
const auto ret = file->push_pull_thread_data->PushData(ptr, len);
file->off += ret;
file->entry->st.st_size = std::max<off_t>(file->entry->st.st_size, file->off);
return ret;
}
ssize_t Device::devoptab_seek(void *fd, off_t pos, int dir) {
auto file = static_cast<File*>(fd);
if (dir == SEEK_CUR) {
pos += file->off;
} else if (dir == SEEK_END) {
pos = file->entry->st.st_size;
}
// for now, random access writes are disabled.
if (file->write_mode && pos != file->off) {
log_write("[WEBDAV] Random access writes are not supported\n");
return file->off;
}
return file->off = std::clamp<u64>(pos, 0, file->entry->st.st_size);
}
int Device::devoptab_fstat(void *fd, struct stat *st) {
auto file = static_cast<File*>(fd);
std::memcpy(st, &file->entry->st, sizeof(*st));
return 0;
}
int Device::devoptab_unlink(const char *path) {
const auto ret = webdav_unlink(path);
if (ret < 0) {
log_write("[WEBDAV] webdav_unlink() failed: %s errno: %s\n", path, std::strerror(-ret));
return ret;
}
return 0;
}
int Device::devoptab_rename(const char *oldName, const char *newName) {
auto ret = webdav_rename(oldName, newName, false);
if (ret == -ENOENT) {
ret = webdav_rename(oldName, newName, true);
}
if (ret < 0) {
log_write("[WEBDAV] webdav_rename() failed: %s to %s errno: %s\n", oldName, newName, std::strerror(-ret));
return ret;
}
return 0;
}
int Device::devoptab_mkdir(const char *path, int mode) {
const auto ret = webdav_mkdir(path);
if (ret < 0) {
log_write("[WEBDAV] webdav_mkdir() failed: %s errno: %s\n", path, std::strerror(-ret));
return ret;
}
return 0;
}
int Device::devoptab_rmdir(const char *path) {
const auto ret = webdav_rmdir(path);
if (ret < 0) {
log_write("[WEBDAV] webdav_rmdir() failed: %s errno: %s\n", path, std::strerror(-ret));
return ret;
}
return 0;
}
int Device::devoptab_diropen(void* fd, const char *path) {
auto dir = static_cast<Dir*>(fd);
auto entries = new DirEntries();
const auto ret = webdav_dirlist(path, *entries);
if (ret < 0) {
log_write("[WEBDAV] webdav_dirlist() failed: %s errno: %s\n", path, std::strerror(-ret));
delete entries;
return ret;
}
dir->entries = entries;
return 0;
}
int Device::devoptab_dirreset(void* fd) {
auto dir = static_cast<Dir*>(fd);
dir->index = 0;
return 0;
}
int Device::devoptab_dirnext(void* fd, char *filename, struct stat *filestat) {
auto dir = static_cast<Dir*>(fd);
if (dir->index >= dir->entries->size()) {
return -ENOENT;
}
auto& entry = (*dir->entries)[dir->index];
if (entry.is_dir) {
filestat->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
} else {
filestat->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
}
filestat->st_nlink = 1;
std::strcpy(filename, entry.name.c_str());
dir->index++;
return 0;
}
int Device::devoptab_dirclose(void* fd) {
auto dir = static_cast<Dir*>(fd);
delete dir->entries;
return 0;
}
int Device::devoptab_lstat(const char *path, struct stat *st) {
auto ret = webdav_stat(path, st, false);
if (ret == -ENOENT) {
ret = webdav_stat(path, st, true);
}
if (ret < 0) {
log_write("[WEBDAV] webdav_stat() failed: %s errno: %s\n", path, std::strerror(-ret));
return ret;
}
return 0;
}
int Device::devoptab_ftruncate(void *fd, off_t len) {
auto file = static_cast<File*>(fd);
if (!file->write_mode) {
log_write("[WEBDAV] Attempt to truncate a read-only file\n");
return -EBADF;
}
file->entry->st.st_size = len;
return 0;
}
int Device::devoptab_fsync(void *fd) {
auto file = static_cast<File*>(fd);
if (!file->write_mode) {
log_write("[WEBDAV] Attempt to fsync a read-only file\n");
return -EBADF;
}
return 0;
}
} // namespace
Result MountWebdavAll() {
return common::MountNetworkDevice([](const common::MountConfig& config) {
return std::make_unique<Device>(config);
},
sizeof(File), sizeof(Dir),
"WEBDAV"
);
}
} // namespace sphaira::devoptab

View File

@@ -12,79 +12,85 @@
#include <array>
#include <memory>
#include <algorithm>
#include <sys/iosupport.h>
namespace sphaira::devoptab {
namespace {
struct Device {
std::unique_ptr<common::LruBufferedData> source;
yati::container::Xci::Partitions partitions;
};
struct File {
Device* device;
const yati::container::CollectionEntry* collection;
size_t off;
};
struct Dir {
Device* device;
const yati::container::Collections* collections;
u32 index;
};
int set_errno(struct _reent *r, int err) {
r->_errno = err;
return -1;
}
struct Device final : common::MountDevice {
Device(std::unique_ptr<common::LruBufferedData>&& _source, const yati::container::Xci::Partitions& _partitions, const common::MountConfig& _config)
: MountDevice{_config}
, source{std::forward<decltype(_source)>(_source)}
, partitions{_partitions} {
int devoptab_open(struct _reent *r, void *fileStruct, const char *_path, int flags, int mode) {
auto device = (Device*)r->deviceData;
auto file = static_cast<File*>(fileStruct);
std::memset(file, 0, sizeof(*file));
char path[FS_MAX_PATH];
if (!common::fix_path(_path, path)) {
return set_errno(r, ENOENT);
}
for (const auto& partition : device->partitions) {
private:
bool Mount() override { return true; }
int devoptab_open(void *fileStruct, const char *path, int flags, int mode) override;
int devoptab_close(void *fd) override;
ssize_t devoptab_read(void *fd, char *ptr, size_t len) override;
ssize_t devoptab_seek(void *fd, off_t pos, int dir) override;
int devoptab_fstat(void *fd, struct stat *st) override;
int devoptab_diropen(void* fd, const char *path) override;
int devoptab_dirreset(void* fd) override;
int devoptab_dirnext(void* fd, char *filename, struct stat *filestat) override;
int devoptab_dirclose(void* fd) override;
int devoptab_lstat(const char *path, struct stat *st) override;
private:
std::unique_ptr<common::LruBufferedData> source;
const yati::container::Xci::Partitions partitions;
};
int Device::devoptab_open(void *fileStruct, const char *path, int flags, int mode) {
auto file = static_cast<File*>(fileStruct);
for (const auto& partition : this->partitions) {
for (const auto& collection : partition.collections) {
if (path == "/" + partition.name + "/" + collection.name) {
file->device = device;
file->collection = &collection;
return r->_errno = 0;
return 0;
}
}
}
return set_errno(r, ENOENT);
log_write("[XCI] devoptab_open: failed to find path: %s\n", path);
return -ENOENT;
}
int devoptab_close(struct _reent *r, void *fd) {
int Device::devoptab_close(void *fd) {
auto file = static_cast<File*>(fd);
std::memset(file, 0, sizeof(*file));
return r->_errno = 0;
return 0;
}
ssize_t devoptab_read(struct _reent *r, void *fd, char *ptr, size_t len) {
ssize_t Device::devoptab_read(void *fd, char *ptr, size_t len) {
auto file = static_cast<File*>(fd);
const auto& collection = file->collection;
len = std::min(len, collection->size - file->off);
u64 bytes_read;
if (R_FAILED(file->device->source->Read(ptr, collection->offset + file->off, len, &bytes_read))) {
return set_errno(r, ENOENT);
if (R_FAILED(this->source->Read(ptr, collection->offset + file->off, len, &bytes_read))) {
return -EIO;
}
file->off += bytes_read;
return bytes_read;
}
off_t devoptab_seek(struct _reent *r, void *fd, off_t pos, int dir) {
ssize_t Device::devoptab_seek(void *fd, off_t pos, int dir) {
auto file = static_cast<File*>(fd);
const auto& collection = file->collection;
@@ -94,73 +100,58 @@ off_t devoptab_seek(struct _reent *r, void *fd, off_t pos, int dir) {
pos = collection->size;
}
r->_errno = 0;
return file->off = std::clamp<u64>(pos, 0, collection->size);
}
int devoptab_fstat(struct _reent *r, void *fd, struct stat *st) {
int Device::devoptab_fstat(void *fd, struct stat *st) {
auto file = static_cast<File*>(fd);
const auto& collection = file->collection;
std::memset(st, 0, sizeof(*st));
st->st_nlink = 1;
st->st_size = collection->size;
st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
return r->_errno = 0;
return 0;
}
DIR_ITER* devoptab_diropen(struct _reent *r, DIR_ITER *dirState, const char *_path) {
auto device = (Device*)r->deviceData;
auto dir = static_cast<Dir*>(dirState->dirStruct);
std::memset(dir, 0, sizeof(*dir));
char path[FS_MAX_PATH];
if (!common::fix_path(_path, path)) {
set_errno(r, ENOENT);
return NULL;
}
int Device::devoptab_diropen(void* fd, const char *path) {
auto dir = static_cast<Dir*>(fd);
if (!std::strcmp(path, "/")) {
dir->device = device;
r->_errno = 0;
return dirState;
return 0;
} else {
for (const auto& partition : device->partitions) {
for (const auto& partition : this->partitions) {
if (path == "/" + partition.name) {
dir->collections = &partition.collections;
r->_errno = 0;
return dirState;
return 0;
}
}
}
set_errno(r, ENOENT);
return NULL;
return -ENOENT;
}
int devoptab_dirreset(struct _reent *r, DIR_ITER *dirState) {
auto dir = static_cast<Dir*>(dirState->dirStruct);
int Device::devoptab_dirreset(void* fd) {
auto dir = static_cast<Dir*>(fd);
dir->index = 0;
return r->_errno = 0;
return 0;
}
int devoptab_dirnext(struct _reent *r, DIR_ITER *dirState, char *filename, struct stat *filestat) {
auto dir = static_cast<Dir*>(dirState->dirStruct);
std::memset(filestat, 0, sizeof(*filestat));
int Device::devoptab_dirnext(void* fd, char *filename, struct stat *filestat) {
auto dir = static_cast<Dir*>(fd);
if (!dir->collections) {
if (dir->index >= dir->device->partitions.size()) {
return set_errno(r, ENOENT);
if (dir->index >= this->partitions.size()) {
return -ENOENT;
}
filestat->st_nlink = 1;
filestat->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
std::strcpy(filename, dir->device->partitions[dir->index].name.c_str());
std::strcpy(filename, this->partitions[dir->index].name.c_str());
} else {
if (dir->index >= dir->collections->size()) {
return set_errno(r, ENOENT);
return -ENOENT;
}
const auto& collection = (*dir->collections)[dir->index];
@@ -171,133 +162,57 @@ int devoptab_dirnext(struct _reent *r, DIR_ITER *dirState, char *filename, struc
}
dir->index++;
return r->_errno = 0;
return 0;
}
int devoptab_dirclose(struct _reent *r, DIR_ITER *dirState) {
auto dir = static_cast<Dir*>(dirState->dirStruct);
int Device::devoptab_dirclose(void* fd) {
auto dir = static_cast<Dir*>(fd);
std::memset(dir, 0, sizeof(*dir));
return r->_errno = 0;
return 0;
}
int devoptab_lstat(struct _reent *r, const char *_path, struct stat *st) {
auto device = (Device*)r->deviceData;
char path[FS_MAX_PATH];
if (!common::fix_path(_path, path)) {
return set_errno(r, ENOENT);
}
std::memset(st, 0, sizeof(*st));
int Device::devoptab_lstat(const char *path, struct stat *st) {
st->st_nlink = 1;
if (!std::strcmp(path, "/")) {
st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
} else {
for (const auto& partition : device->partitions) {
for (const auto& partition : this->partitions) {
if (path == "/" + partition.name) {
st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
return r->_errno = 0;
return 0;
}
for (const auto& collection : partition.collections) {
if (path == "/" + partition.name + "/" + collection.name) {
st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
st->st_size = collection.size;
return r->_errno = 0;
return 0;
}
}
}
}
return set_errno(r, ENOENT);
}
constexpr devoptab_t DEVOPTAB = {
.structSize = sizeof(File),
.open_r = devoptab_open,
.close_r = devoptab_close,
.read_r = devoptab_read,
.seek_r = devoptab_seek,
.fstat_r = devoptab_fstat,
.stat_r = devoptab_lstat,
.dirStateSize = sizeof(Dir),
.diropen_r = devoptab_diropen,
.dirreset_r = devoptab_dirreset,
.dirnext_r = devoptab_dirnext,
.dirclose_r = devoptab_dirclose,
.lstat_r = devoptab_lstat,
};
struct Entry {
Device device{};
devoptab_t devoptab{};
fs::FsPath path{};
fs::FsPath mount{};
char name[32]{};
s32 ref_count{};
~Entry() {
RemoveDevice(mount);
}
};
Mutex g_mutex;
std::array<std::unique_ptr<Entry>, common::MAX_ENTRIES> g_entries;
bool IsAlreadyMounted(const fs::FsPath& path, fs::FsPath& out_path) {
// check if we already have the save mounted.
for (auto& e : g_entries) {
if (e && e->path == path) {
e->ref_count++;
out_path = e->mount;
return true;
}
}
return false;
return -ENOENT;
}
Result MountXciInternal(const std::shared_ptr<yati::source::Base>& source, s64 size, const fs::FsPath& path, fs::FsPath& out_path) {
// check if we already have the save mounted.
for (auto& e : g_entries) {
if (e && e->path == path) {
e->ref_count++;
out_path = e->mount;
R_SUCCEED();
}
}
// otherwise, find next free entry.
auto itr = std::ranges::find_if(g_entries, [](auto& e){
return !e;
});
R_UNLESS(itr != g_entries.end(), 0x1);
const auto index = std::distance(g_entries.begin(), itr);
auto buffered = std::make_unique<common::LruBufferedData>(source, size);
yati::container::Xci xci{buffered.get()};
yati::container::Xci::Root root;
R_TRY(xci.GetRoot(root));
auto entry = std::make_unique<Entry>();
entry->path = path;
entry->devoptab = DEVOPTAB;
entry->devoptab.name = entry->name;
entry->devoptab.deviceData = &entry->device;
entry->device.source = std::move(buffered);
entry->device.partitions = root.partitions;
std::snprintf(entry->name, sizeof(entry->name), "xci_%zu", index);
std::snprintf(entry->mount, sizeof(entry->mount), "xci_%zu:/", index);
R_UNLESS(AddDevice(&entry->devoptab) >= 0, 0x1);
log_write("[XCI] DEVICE SUCCESS %s %s\n", path.s, entry->name);
out_path = entry->mount;
entry->ref_count++;
*itr = std::move(entry);
if (!common::MountReadOnlyIndexDevice(
[&buffered, &root](const common::MountConfig& config) {
return std::make_unique<Device>(std::move(buffered), root.partitions, config);
},
sizeof(File), sizeof(Dir),
"XCI", out_path
)) {
log_write("[XCI] Failed to mount %s\n", path.s);
R_THROW(0x1);
}
R_SUCCEED();
}
@@ -305,12 +220,6 @@ Result MountXciInternal(const std::shared_ptr<yati::source::Base>& source, s64 s
} // namespace
Result MountXci(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path) {
SCOPED_MUTEX(&g_mutex);
if (IsAlreadyMounted(path, out_path)) {
R_SUCCEED();
}
s64 size;
auto source = std::make_shared<yati::source::File>(fs, path);
R_TRY(source->GetSize(&size));
@@ -319,33 +228,7 @@ 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) {
SCOPED_MUTEX(&g_mutex);
if (IsAlreadyMounted(path, out_path)) {
R_SUCCEED();
}
return MountXciInternal(source, size, path, out_path);
}
void UmountXci(const fs::FsPath& mount) {
SCOPED_MUTEX(&g_mutex);
auto itr = std::ranges::find_if(g_entries, [&mount](auto& e){
return e && e->mount == mount;
});
if (itr == g_entries.end()) {
return;
}
if ((*itr)->ref_count) {
(*itr)->ref_count--;
}
if (!(*itr)->ref_count) {
itr->reset();
}
}
} // namespace sphaira::devoptab

View File

@@ -10,7 +10,6 @@
#include <array>
#include <memory>
#include <algorithm>
#include <sys/iosupport.h>
#include <zlib.h>
namespace sphaira::devoptab {
@@ -113,11 +112,6 @@ struct DirectoryEntry {
using FileTableEntries = std::vector<FileEntry>;
struct Device {
std::unique_ptr<common::LruBufferedData> source;
DirectoryEntry root;
};
struct Zfile {
z_stream z; // zlib stream.
Bytef* buffer; // buffer that compressed data is read into.
@@ -126,7 +120,6 @@ struct Zfile {
};
struct File {
Device* device;
const FileEntry* entry;
Zfile zfile; // only used if the file is compressed.
size_t data_off; // offset of the file data.
@@ -134,7 +127,6 @@ struct File {
};
struct Dir {
Device* device;
const DirectoryEntry* entry;
u32 index;
};
@@ -194,44 +186,58 @@ void set_stat_file(const FileEntry* entry, struct stat *st) {
st->st_ctime = st->st_atime;
}
int set_errno(struct _reent *r, int err) {
r->_errno = err;
return -1;
}
struct Device final : common::MountDevice {
Device(std::unique_ptr<common::LruBufferedData>&& _source, const DirectoryEntry& _root, const common::MountConfig& _config)
: MountDevice{_config}
, source{std::forward<decltype(_source)>(_source)}
, root{_root} {
int devoptab_open(struct _reent *r, void *fileStruct, const char *_path, int flags, int mode) {
auto device = (Device*)r->deviceData;
auto file = static_cast<File*>(fileStruct);
std::memset(file, 0, sizeof(*file));
char path[FS_MAX_PATH]{};
if (!common::fix_path(_path, path)) {
return set_errno(r, ENOENT);
}
const auto entry = find_file_entry(device->root, path);
private:
bool Mount() override { return true; }
int devoptab_open(void *fileStruct, const char *path, int flags, int mode) override;
int devoptab_close(void *fd) override;
ssize_t devoptab_read(void *fd, char *ptr, size_t len) override;
ssize_t devoptab_seek(void *fd, off_t pos, int dir) override;
int devoptab_fstat(void *fd, struct stat *st) override;
int devoptab_diropen(void* fd, const char *path) override;
int devoptab_dirreset(void* fd) override;
int devoptab_dirnext(void* fd, char *filename, struct stat *filestat) override;
int devoptab_dirclose(void* fd) override;
int devoptab_lstat(const char *path, struct stat *st) override;
private:
std::unique_ptr<common::LruBufferedData> source;
const DirectoryEntry root;
};
int Device::devoptab_open(void *fileStruct, const char *path, int flags, int mode) {
auto file = static_cast<File*>(fileStruct);
const auto entry = find_file_entry(this->root, path);
if (!entry) {
return set_errno(r, ENOENT);
return -ENOENT;
}
if ((entry->flags & mmz_Flag_Encrypted) || (entry->flags & mmz_Flag_StrongEncrypted)) {
log_write("[ZIP] encrypted zip not supported\n");
return set_errno(r, ENOENT);
return -ENOENT;
}
if (entry->compression_type != mmz_Compression_None && entry->compression_type != mmz_Compression_Deflate) {
log_write("[ZIP] unsuported compression type: %u\n", entry->compression_type);
return set_errno(r, ENOENT);
return -ENOENT;
}
mmz_LocalHeader local_hdr{};
auto offset = entry->local_file_header_off;
if (R_FAILED(device->source->Read2(&local_hdr, offset, sizeof(local_hdr)))) {
return set_errno(r, ENOENT);
if (R_FAILED(this->source->Read2(&local_hdr, offset, sizeof(local_hdr)))) {
return -ENOENT;
}
if (local_hdr.sig != LOCAL_HEADER_SIG) {
return set_errno(r, ENOENT);
return -ENOENT;
}
offset += sizeof(local_hdr) + local_hdr.filename_len + local_hdr.extrafield_len;
@@ -239,12 +245,12 @@ int devoptab_open(struct _reent *r, void *fileStruct, const char *_path, int fla
// todo: does a decs take prio over file header?
if (local_hdr.flags & mmz_Flag_DataDescriptor) {
mmz_DataDescriptor data_desc{};
if (R_FAILED(device->source->Read2(&data_desc, offset, sizeof(data_desc)))) {
return set_errno(r, ENOENT);
if (R_FAILED(this->source->Read2(&data_desc, offset, sizeof(data_desc)))) {
return -ENOENT;
}
if (data_desc.sig != DATA_DESCRIPTOR_SIG) {
return set_errno(r, ENOENT);
return -ENOENT;
}
offset += sizeof(data_desc);
@@ -253,41 +259,39 @@ int devoptab_open(struct _reent *r, void *fileStruct, const char *_path, int fla
if (entry->compression_type == mmz_Compression_Deflate) {
auto& zfile = file->zfile;
zfile.buffer_size = 1024 * 64;
zfile.buffer = (Bytef*)calloc(1, zfile.buffer_size);
zfile.buffer = (Bytef*)std::calloc(1, zfile.buffer_size);
if (!zfile.buffer) {
return set_errno(r, ENOENT);
return -ENOENT;
}
// skip zlib header.
if (Z_OK != inflateInit2(&zfile.z, -MAX_WBITS)) {
free(zfile.buffer);
std::free(zfile.buffer);
zfile.buffer = nullptr;
return set_errno(r, ENOENT);
return -ENOENT;
}
}
file->device = device;
file->entry = entry;
file->data_off = offset;
return r->_errno = 0;
return 0;
}
int devoptab_close(struct _reent *r, void *fd) {
int Device::devoptab_close(void *fd) {
auto file = static_cast<File*>(fd);
if (file->entry->compression_type == mmz_Compression_Deflate) {
if (file->entry->compression_type == mmz_Compression_Deflate) {
inflateEnd(&file->zfile.z);
if (file->zfile.buffer) {
free(file->zfile.buffer);
std::free(file->zfile.buffer);
}
}
std::memset(file, 0, sizeof(*file));
return r->_errno = 0;
return 0;
}
ssize_t devoptab_read(struct _reent *r, void *fd, char *ptr, size_t len) {
ssize_t Device::devoptab_read(void *fd, char *ptr, size_t len) {
auto file = static_cast<File*>(fd);
len = std::min(len, file->entry->uncompressed_size - file->off);
@@ -296,8 +300,8 @@ ssize_t devoptab_read(struct _reent *r, void *fd, char *ptr, size_t len) {
}
if (file->entry->compression_type == mmz_Compression_None) {
if (R_FAILED(file->device->source->Read2(ptr, file->data_off + file->off, len))) {
return set_errno(r, ENOENT);
if (R_FAILED(this->source->Read2(ptr, file->data_off + file->off, len))) {
return -ENOENT;
}
} else if (file->entry->compression_type == mmz_Compression_Deflate) {
auto& zfile = file->zfile;
@@ -309,8 +313,8 @@ ssize_t devoptab_read(struct _reent *r, void *fd, char *ptr, size_t len) {
// check if we need to fetch more data.
if (!zfile.z.next_in || !zfile.z.avail_in) {
const auto clen = std::min(zfile.buffer_size, file->entry->compressed_size - zfile.compressed_off);
if (R_FAILED(file->device->source->Read2(zfile.buffer, file->data_off + zfile.compressed_off, clen))) {
return set_errno(r, ENOENT);
if (R_FAILED(this->source->Read2(zfile.buffer, file->data_off + zfile.compressed_off, clen))) {
return -ENOENT;
}
zfile.compressed_off += clen;
@@ -324,7 +328,7 @@ ssize_t devoptab_read(struct _reent *r, void *fd, char *ptr, size_t len) {
len -= zfile.z.avail_out;
} else {
log_write("[ZLIB] failed to inflate: %d %s\n", rc, zfile.z.msg);
return set_errno(r, ENOENT);
return -ENOENT;
}
}
}
@@ -334,7 +338,7 @@ ssize_t devoptab_read(struct _reent *r, void *fd, char *ptr, size_t len) {
return len;
}
off_t devoptab_seek(struct _reent *r, void *fd, off_t pos, int dir) {
ssize_t Device::devoptab_seek(void *fd, off_t pos, int dir) {
auto file = static_cast<File*>(fd);
// seek like normal.
@@ -365,58 +369,44 @@ off_t devoptab_seek(struct _reent *r, void *fd, off_t pos, int dir) {
}
}
r->_errno = 0;
return file->off = std::clamp<u64>(pos, 0, file->entry->uncompressed_size);
}
int devoptab_fstat(struct _reent *r, void *fd, struct stat *st) {
int Device::devoptab_fstat(void *fd, struct stat *st) {
auto file = static_cast<File*>(fd);
std::memset(st, 0, sizeof(*st));
set_stat_file(file->entry, st);
return r->_errno = 0;
return 0;
}
DIR_ITER* devoptab_diropen(struct _reent *r, DIR_ITER *dirState, const char *_path) {
auto device = (Device*)r->deviceData;
auto dir = static_cast<Dir*>(dirState->dirStruct);
std::memset(dir, 0, sizeof(*dir));
int Device::devoptab_diropen(void* fd, const char *path) {
auto dir = static_cast<Dir*>(fd);
char path[FS_MAX_PATH];
if (!common::fix_path(_path, path)) {
set_errno(r, ENOENT);
return NULL;
}
const auto entry = find_dir_entry(device->root, path);
const auto entry = find_dir_entry(this->root, path);
if (!entry) {
set_errno(r, ENOENT);
return NULL;
return -ENOENT;
}
dir->device = device;
dir->entry = entry;
r->_errno = 0;
return dirState;
return 0;
}
int devoptab_dirreset(struct _reent *r, DIR_ITER *dirState) {
auto dir = static_cast<Dir*>(dirState->dirStruct);
int Device::devoptab_dirreset(void* fd) {
auto dir = static_cast<Dir*>(fd);
dir->index = 0;
return r->_errno = 0;
return 0;
}
int devoptab_dirnext(struct _reent *r, DIR_ITER *dirState, char *filename, struct stat *filestat) {
auto dir = static_cast<Dir*>(dirState->dirStruct);
std::memset(filestat, 0, sizeof(*filestat));
int Device::devoptab_dirnext(void* fd, char *filename, struct stat *filestat) {
auto dir = static_cast<Dir*>(fd);
u32 index = dir->index;
if (index >= dir->entry->dir_child.size()) {
index -= dir->entry->dir_child.size();
if (index >= dir->entry->file_child.size()) {
return set_errno(r, ENOENT);
return -ENOENT;
} else {
const auto& entry = dir->entry->file_child[index];
const auto rel_path = entry.path.substr(entry.path.find_last_of('/') + 1);
@@ -434,59 +424,31 @@ int devoptab_dirnext(struct _reent *r, DIR_ITER *dirState, char *filename, struc
}
dir->index++;
return r->_errno = 0;
return 0;
}
int devoptab_dirclose(struct _reent *r, DIR_ITER *dirState) {
auto dir = static_cast<Dir*>(dirState->dirStruct);
int Device::devoptab_dirclose(void* fd) {
auto dir = static_cast<Dir*>(fd);
std::memset(dir, 0, sizeof(*dir));
return r->_errno = 0;
return 0;
}
int devoptab_lstat(struct _reent *r, const char *_path, struct stat *st) {
auto device = (Device*)r->deviceData;
if (!device) {
return set_errno(r, ENOENT);
}
char path[FS_MAX_PATH];
if (!common::fix_path(_path, path)) {
return set_errno(r, ENOENT);
}
std::memset(st, 0, sizeof(*st));
int Device::devoptab_lstat(const char *path, struct stat *st) {
st->st_nlink = 1;
if (find_dir_entry(device->root, path)) {
if (find_dir_entry(this->root, path)) {
st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
} else if (auto entry = find_file_entry(device->root, path)) {
} else if (auto entry = find_file_entry(this->root, path)) {
set_stat_file(entry, st);
} else {
log_write("[ZIP] didn't find in lstat\n");
return set_errno(r, ENOENT);
return -ENOENT;
}
return r->_errno = 0;
return 0;
}
constexpr devoptab_t DEVOPTAB = {
.structSize = sizeof(File),
.open_r = devoptab_open,
.close_r = devoptab_close,
.read_r = devoptab_read,
.seek_r = devoptab_seek,
.fstat_r = devoptab_fstat,
.stat_r = devoptab_lstat,
.dirStateSize = sizeof(Dir),
.diropen_r = devoptab_diropen,
.dirreset_r = devoptab_dirreset,
.dirnext_r = devoptab_dirnext,
.dirclose_r = devoptab_dirclose,
.lstat_r = devoptab_lstat,
};
auto BuildPath(const std::string& path) -> std::string {
if (path.starts_with('/')) {
return path;
@@ -600,48 +562,13 @@ Result ParseZip(common::LruBufferedData* source, s64 size, FileTableEntries& out
R_SUCCEED();
}
struct Entry {
Device device{};
devoptab_t devoptab{};
fs::FsPath path{};
fs::FsPath mount{};
char name[32]{};
s32 ref_count{};
~Entry() {
RemoveDevice(mount);
}
};
Mutex g_mutex;
std::array<std::unique_ptr<Entry>, common::MAX_ENTRIES> g_entries;
} // namespace
Result MountZip(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path) {
SCOPED_MUTEX(&g_mutex);
// check if we already have the save mounted.
for (auto& e : g_entries) {
if (e && e->path == path) {
e->ref_count++;
out_path = e->mount;
R_SUCCEED();
}
}
// otherwise, find next free entry.
auto itr = std::ranges::find_if(g_entries, [](auto& e){
return !e;
});
R_UNLESS(itr != g_entries.end(), 0x1);
const auto index = std::distance(g_entries.begin(), itr);
auto source = std::make_shared<yati::source::File>(fs, path);
s64 size;
R_TRY(source->GetSize(&size));
auto buffered = std::make_unique<common::LruBufferedData>(source, size);
FileTableEntries table_entries;
@@ -651,44 +578,18 @@ Result MountZip(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path) {
DirectoryEntry root;
Parse(table_entries, root);
auto entry = std::make_unique<Entry>();
entry->path = path;
entry->devoptab = DEVOPTAB;
entry->devoptab.name = entry->name;
entry->devoptab.deviceData = &entry->device;
entry->device.source = std::move(buffered);
entry->device.root = root;
std::snprintf(entry->name, sizeof(entry->name), "zip_%zu", index);
std::snprintf(entry->mount, sizeof(entry->mount), "zip_%zu:/", index);
R_UNLESS(AddDevice(&entry->devoptab) >= 0, 0x1);
log_write("[ZIP] DEVICE SUCCESS %s %s\n", path.s, entry->name);
out_path = entry->mount;
entry->ref_count++;
*itr = std::move(entry);
if (!common::MountReadOnlyIndexDevice(
[&buffered, &root](const common::MountConfig& config) {
return std::make_unique<Device>(std::move(buffered), root, config);
},
sizeof(File), sizeof(Dir),
"ZIP", out_path
)) {
log_write("[ZIP] Failed to mount %s\n", path.s);
R_THROW(0x1);
}
R_SUCCEED();
}
void UmountZip(const fs::FsPath& mount) {
SCOPED_MUTEX(&g_mutex);
auto itr = std::ranges::find_if(g_entries, [&mount](auto& e){
return e && e->mount == mount;
});
if (itr == g_entries.end()) {
return;
}
if ((*itr)->ref_count) {
(*itr)->ref_count--;
}
if (!(*itr)->ref_count) {
itr->reset();
}
}
} // namespace sphaira::devoptab

View File

@@ -14,6 +14,25 @@ HashStr hexIdToStrInternal(auto id) {
return str;
}
std::string formatSizeInetrnal(double size, double base) {
static const char* const suffixes[] = { "B", "KB", "MB", "GB", "TB", "PB", "EB" };
size_t suffix_index = 0;
while (size >= base && suffix_index < std::size(suffixes) - 1) {
size /= base;
suffix_index++;
}
char buffer[32];
if (suffix_index == 0) {
std::snprintf(buffer, sizeof(buffer), "%.0f %s", size, suffixes[suffix_index]);
} else {
std::snprintf(buffer, sizeof(buffer), "%.2f %s", size, suffixes[suffix_index]);
}
return buffer;
}
} // namespace
HashStr hexIdToStr(FsRightsId id) {
@@ -28,4 +47,12 @@ HashStr hexIdToStr(NcmContentId id) {
return hexIdToStrInternal(id);
}
std::string formatSizeStorage(u64 size) {
return formatSizeInetrnal(size, 1024.0);
}
std::string formatSizeNetwork(u64 size) {
return formatSizeInetrnal(size, 1000.0);
}
} // namespace sphaira::utils

View File

@@ -11,8 +11,6 @@ auto WebShow(const std::string& url) -> Result {
WebCommonReply reply{};
WebExitReason reason{};
AccountUid account_uid{};
char last_url[FS_MAX_PATH]{};
size_t last_url_len{};
// WebBackgroundKind_Unknown1 = shows background
// WebBackgroundKind_Unknown2 = shows background faded
@@ -54,8 +52,6 @@ auto WebShow(const std::string& url) -> Result {
if (R_FAILED(webConfigShow(&config, &reply))) { log_write("failed: webConfigShow\n"); }
if (R_FAILED(webReplyGetExitReason(&reply, &reason))) { log_write("failed: webReplyGetExitReason\n"); }
if (R_FAILED(webReplyGetLastUrl(&reply, last_url, sizeof(last_url), &last_url_len))) { log_write("failed: webReplyGetLastUrl\n"); }
log_write("last url: %s\n", last_url);
R_SUCCEED();
}

View File

@@ -34,9 +34,9 @@ auto GetContentTypeStr(u8 content_type) -> const char* {
case NcmContentType_Program: return "Program";
case NcmContentType_Data: return "Data";
case NcmContentType_Control: return "Control";
case NcmContentType_HtmlDocument: return "HtmlDocument";
case NcmContentType_LegalInformation: return "LegalInformation";
case NcmContentType_DeltaFragment: return "DeltaFragment";
case NcmContentType_HtmlDocument: return "Html";
case NcmContentType_LegalInformation: return "Legal";
case NcmContentType_DeltaFragment: return "Delta";
}
return "Unknown";

View File

@@ -16,6 +16,8 @@
#include "utils/thread.hpp"
#include "ui/progress_box.hpp"
#include "ui/menus/game_menu.hpp"
#include "app.hpp"
#include "i18n.hpp"
#include "log.hpp"
@@ -873,6 +875,9 @@ Yati::~Yati() {
}
App::SetAutoSleepDisabled(false);
// force update the game menu, as we may have installed a game.
ui::menu::game::SignalChange();
}
Result Yati::Setup(const ConfigOverride& override) {

View File

@@ -1,114 +0,0 @@
import unittest
import crc32c
import os
from usb_common import CMD_EXPORT, CMD_QUIT, RESULT_OK, RESULT_ERROR
class FakeUsb:
def __init__(self, files=None):
# files: list of tuples (filename: str, data: bytes)
self.files = files or [("testfile.bin", b"testdata")]
self._cmd_index = 0
self._file_index = 0
self._data_index = 0
self.results = []
self._reading_filename = True
self._reading_data = False
self._current_data = b""
self._current_data_offset = 0
self._current_data_sent = 0
self._current_file = None
self._send_data_header_calls = 0
def wait_for_connect(self):
pass
def get_send_header(self):
# Simulate command sequence: export for each file, then quit
if self._cmd_index < len(self.files):
filename, data = self.files[self._cmd_index]
self._current_file = (filename, data)
self._cmd_index += 1
self._reading_filename = True
self._reading_data = False
self._current_data = data
self._current_data_offset = 0
self._current_data_sent = 0
self._send_data_header_calls = 0
return [CMD_EXPORT, len(filename.encode("utf-8")), 0]
else:
return [CMD_QUIT, 0, 0]
def read(self, size):
# Simulate reading file name or data
if self._reading_filename:
filename = self._current_file[0].encode("utf-8")
self._reading_filename = False
self._reading_data = True
return filename[:size]
elif self._reading_data:
# Return file data for export
data = self._current_data[self._current_data_sent:self._current_data_sent+size]
self._current_data_sent += len(data)
return data
else:
return b""
def get_send_data_header(self):
# Simulate sending data in one chunk, then finish
if self._send_data_header_calls == 0:
self._send_data_header_calls += 1
data = self._current_data
crc = crc32c.crc32c(data)
return [0, len(data), crc]
else:
return [0, 0, 0] # End of transfer
def send_result(self, result):
self.results.append(result)
# test case for usb_export.py
class TestUsbExport(unittest.TestCase):
def setUp(self):
self.root = "test_output"
os.makedirs(self.root, exist_ok=True)
# 100 files named test1.bin, test2.bin, ..., test10.bin, each with different sizes
self.files = [
(f"test{i+1}.bin", bytes([65 + i]) * (i * 100 + 1)) for i in range(100)
]
self.fake_usb = FakeUsb(files=self.files)
def tearDown(self):
# Clean up created files/folders
for f in os.listdir(self.root):
os.remove(os.path.join(self.root, f))
os.rmdir(self.root)
def test_export_multiple_files(self):
from usb_export import get_file_name, create_file_folder, wait_for_input
# Simulate the main loop for all files
for filename, data in self.files:
cmd, name_len, _ = self.fake_usb.get_send_header()
self.assertEqual(cmd, CMD_EXPORT)
file_name = get_file_name(self.fake_usb, name_len)
self.assertEqual(file_name, filename)
full_path = create_file_folder(self.root, file_name)
self.fake_usb.send_result(RESULT_OK)
wait_for_input(self.fake_usb, full_path)
# Check file was created and contents match
with open(full_path, "rb") as f:
filedata = f.read()
self.assertEqual(filedata, data)
# After all files, should get CMD_QUIT
cmd, _, _ = self.fake_usb.get_send_header()
self.assertEqual(cmd, CMD_QUIT)
self.assertIn(RESULT_OK, self.fake_usb.results)
if __name__ == "__main__":
unittest.main()

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