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 name: build
on: [push, pull_request] on:
push:
branches-ignore:
- dev
pull_request:
branches-ignore:
- dev
jobs: jobs:
build: build:

View File

@@ -3,7 +3,7 @@ name: USB Export Python Tests
on: on:
push: push:
paths: &python_usb_export_paths paths: &python_usb_export_paths
- 'tools/test_usb_export.py' - 'tools/tests/test_usb_export.py'
- 'tools/usb_export.py' - 'tools/usb_export.py'
- 'tools/usb_common.py' - 'tools/usb_common.py'
- 'tools/requirements.txt' - 'tools/requirements.txt'
@@ -30,4 +30,4 @@ jobs:
- name: Run tests - name: Run tests
run: | 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: on:
push: push:
paths: &python_usb_install_paths paths: &python_usb_install_paths
- 'tools/test_usb_install.py' - 'tools/tests/test_usb_install.py'
- 'tools/usb_install.py' - 'tools/usb_install.py'
- 'tools/usb_common.py' - 'tools/usb_common.py'
- 'tools/requirements.txt' - 'tools/requirements.txt'
@@ -30,4 +30,4 @@ jobs:
- name: Run tests - name: Run tests
run: | 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/ usb_test/
__pycache__ __pycache__
usb_*.spec usb_*.spec
CMakeUserPresets.json
build_patreon.sh

View File

@@ -21,6 +21,35 @@
"LTO": true "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", "name": "Dev",
"displayName": "Dev", "displayName": "Dev",
@@ -38,6 +67,11 @@
"configurePreset": "Release", "configurePreset": "Release",
"jobs": 16 "jobs": 16
}, },
{
"name": "Lite",
"configurePreset": "Lite",
"jobs": 16
},
{ {
"name": "Dev", "name": "Dev",
"configurePreset": "Dev", "configurePreset": "Dev",

View File

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

View File

@@ -1,6 +1,34 @@
cmake_minimum_required(VERSION 3.13) 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 project(sphaira
VERSION ${sphaira_VERSION} VERSION ${sphaira_VERSION}
@@ -30,7 +58,11 @@ execute_process(
OUTPUT_STRIP_TRAILING_WHITESPACE 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 add_executable(sphaira
source/ui/menus/appstore.cpp source/ui/menus/appstore.cpp
@@ -46,8 +78,6 @@ add_executable(sphaira
source/ui/menus/themezer.cpp source/ui/menus/themezer.cpp
source/ui/menus/ghdl.cpp source/ui/menus/ghdl.cpp
source/ui/menus/usb_menu.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/gc_menu.cpp
source/ui/menus/game_menu.cpp source/ui/menus/game_menu.cpp
source/ui/menus/game_meta_menu.cpp source/ui/menus/game_meta_menu.cpp
@@ -85,17 +115,12 @@ add_executable(sphaira
source/web.cpp source/web.cpp
source/hasher.cpp source/hasher.cpp
source/i18n.cpp source/i18n.cpp
source/ftpsrv_helper.cpp
source/haze_helper.cpp
source/threaded_file_transfer.cpp source/threaded_file_transfer.cpp
source/title_info.cpp source/title_info.cpp
source/minizip_helper.cpp source/minizip_helper.cpp
source/fatfs.cpp
source/usbdvd.cpp
source/utils/utils.cpp source/utils/utils.cpp
source/utils/audio.cpp source/utils/audio.cpp
source/utils/nsz_dumper.cpp
source/utils/devoptab_common.cpp source/utils/devoptab_common.cpp
source/utils/devoptab_romfs.cpp source/utils/devoptab_romfs.cpp
source/utils/devoptab_save.cpp source/utils/devoptab_save.cpp
@@ -105,6 +130,11 @@ add_executable(sphaira
source/utils/devoptab_xci.cpp source/utils/devoptab_xci.cpp
source/utils/devoptab_zip.cpp source/utils/devoptab_zip.cpp
source/utils/devoptab_bfsar.cpp source/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/base.cpp
source/usb/usbds.cpp source/usb/usbds.cpp
@@ -133,7 +163,7 @@ add_executable(sphaira
target_compile_definitions(sphaira PRIVATE target_compile_definitions(sphaira PRIVATE
-DAPP_VERSION="${sphaira_VERSION}" -DAPP_VERSION="${sphaira_VERSION}"
-DAPP_VERSION_HASH="${sphaira_VERSION_HASH}" -DAPP_DISPLAY_VERSION="${sphaira_DISPLAY_VERSION}"
-DCURL_NO_OLDIES=1 -DCURL_NO_OLDIES=1
-DDEV_BUILD=$<BOOL:${DEV_BUILD}> -DDEV_BUILD=$<BOOL:${DEV_BUILD}>
-DZSTD_STATIC_LINKING_ONLY=1 -DZSTD_STATIC_LINKING_ONLY=1
@@ -182,17 +212,6 @@ target_compile_options(sphaira PRIVATE
include(FetchContent) include(FetchContent)
set(FETCHCONTENT_QUIET FALSE) 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 FetchContent_Declare(libpulsar
GIT_REPOSITORY https://github.com/ITotalJustice/switch-libpulsar.git GIT_REPOSITORY https://github.com/ITotalJustice/switch-libpulsar.git
GIT_TAG ac7bc97 GIT_TAG ac7bc97
@@ -224,11 +243,6 @@ FetchContent_Declare(zstd
SOURCE_SUBDIR build/cmake SOURCE_SUBDIR build/cmake
) )
FetchContent_Declare(libusbhsfs
GIT_REPOSITORY https://github.com/ITotalJustice/libusbhsfs.git
GIT_TAG 625269b
)
FetchContent_Declare(libnxtc FetchContent_Declare(libnxtc
GIT_REPOSITORY https://github.com/ITotalJustice/libnxtc.git GIT_REPOSITORY https://github.com/ITotalJustice/libnxtc.git
GIT_TAG 88ce3d8 GIT_TAG 88ce3d8
@@ -245,35 +259,279 @@ FetchContent_Declare(dr_libs
SOURCE_SUBDIR NONE SOURCE_SUBDIR NONE
) )
FetchContent_Declare(id3v2lib if (ENABLE_NVJPG)
GIT_REPOSITORY https://github.com/larsbs/id3v2lib.git FetchContent_Declare(nvjpg
GIT_TAG 141ffb8 GIT_REPOSITORY https://github.com/ITotalJustice/oss-nvjpg.git
) GIT_TAG 45680e7
)
FetchContent_Declare(libusbdvd FetchContent_MakeAvailable(nvjpg)
GIT_REPOSITORY https://github.com/proconsule/libusbdvd.git
GIT_TAG 3cb0613
)
set(USE_NEW_ZSTD ON) add_library(nvjpg
# has issues with some homebrew and game icons (oxenfree, overwatch2). ${nvjpg_SOURCE_DIR}/lib/decoder.cpp
set(USE_NVJPG OFF) ${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_STATIC ON)
set(ZSTD_BUILD_SHARED OFF) set(ZSTD_BUILD_SHARED OFF)
set(ZSTD_BUILD_COMPRESSION ON) set(ZSTD_BUILD_COMPRESSION ${ENABLE_NSZ})
set(ZSTD_MULTITHREAD_SUPPORT ON) set(ZSTD_MULTITHREAD_SUPPORT ${ENABLE_NSZ})
set(ZSTD_BUILD_DECOMPRESSION ON) set(ZSTD_BUILD_DECOMPRESSION ON)
set(ZSTD_BUILD_DICTBUILDER OFF) set(ZSTD_BUILD_DICTBUILDER OFF)
set(ZSTD_LEGACY_SUPPORT OFF) set(ZSTD_LEGACY_SUPPORT OFF)
set(ZSTD_BUILD_PROGRAMS OFF) set(ZSTD_BUILD_PROGRAMS OFF)
set(ZSTD_BUILD_TESTS OFF) set(ZSTD_BUILD_TESTS OFF)
# minini
set(MININI_LIB_NAME minIni) set(MININI_LIB_NAME minIni)
set(MININI_USE_STDIO ON) set(MININI_USE_STDIO ON)
set(MININI_USE_NX OFF) set(MININI_USE_NX OFF)
set(MININI_USE_FLOAT ON) set(MININI_USE_FLOAT ON)
# nanovg
if (CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo") if (CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")
set(NANOVG_DEBUG ON) set(NANOVG_DEBUG ON)
endif() endif()
@@ -287,6 +545,7 @@ set(NANOVG_NO_HDR ON)
set(NANOVG_NO_PIC ON) set(NANOVG_NO_PIC ON)
set(NANOVG_NO_PNM ON) set(NANOVG_NO_PNM ON)
# yyjson
set(YYJSON_INSTALL OFF) set(YYJSON_INSTALL OFF)
set(YYJSON_DISABLE_READER OFF) set(YYJSON_DISABLE_READER OFF)
set(YYJSON_DISABLE_WRITER 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_UTF8_VALIDATION ON)
set(YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS OFF) 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( FetchContent_MakeAvailable(
ftpsrv
libhaze
libpulsar libpulsar
nanovg nanovg
stb stb
minIni minIni
yyjson yyjson
zstd zstd
libusbhsfs
libnxtc libnxtc
nvjpg
dr_libs 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) add_library(stb INTERFACE)
target_include_directories(stb INTERFACE ${stb_SOURCE_DIR}) target_include_directories(stb INTERFACE ${stb_SOURCE_DIR})
@@ -366,38 +579,6 @@ add_library(libnxtc
) )
target_include_directories(libnxtc PUBLIC ${libnxtc_SOURCE_DIR}/include) target_include_directories(libnxtc PUBLIC ${libnxtc_SOURCE_DIR}/include)
add_library(libusbdvd
${libusbdvd_SOURCE_DIR}/source/usbdvd.cpp
${libusbdvd_SOURCE_DIR}/source/usbdvd_scsi.cpp
${libusbdvd_SOURCE_DIR}/source/usbdvd_utils.cpp
${libusbdvd_SOURCE_DIR}/source/fs/usbdvd_datadisc.cpp
${libusbdvd_SOURCE_DIR}/source/fs/audiocdfs/audiocdfs.cpp
${libusbdvd_SOURCE_DIR}/source/fs/audiocdfs/cdaudio_devoptab.cpp
${libusbdvd_SOURCE_DIR}/source/fs/iso9660/usbdvd_iso9660.cpp
${libusbdvd_SOURCE_DIR}/source/fs/iso9660/iso9660_devoptab.cpp
${libusbdvd_SOURCE_DIR}/source/fs/udf/usbdvd_udf.cpp
${libusbdvd_SOURCE_DIR}/source/fs/udf/udf_devoptab.cpp
${libusbdvd_SOURCE_DIR}/source/os/switch/switch_usb.cpp
)
target_include_directories(libusbdvd PRIVATE ${libusbdvd_SOURCE_DIR}/source/)
target_include_directories(libusbdvd PRIVATE ${libusbdvd_SOURCE_DIR}/source/os/switch)
target_include_directories(libusbdvd PRIVATE ${libusbdvd_SOURCE_DIR}/source/fs)
target_include_directories(libusbdvd PRIVATE ${libusbdvd_SOURCE_DIR}/source/fs/audiocdfs)
target_include_directories(libusbdvd PRIVATE ${libusbdvd_SOURCE_DIR}/source/fs/iso9660)
target_include_directories(libusbdvd PRIVATE ${libusbdvd_SOURCE_DIR}/source/fs/udf)
target_include_directories(libusbdvd PUBLIC ${libusbdvd_SOURCE_DIR}/include)
if (USE_NVJPG)
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_package(ZLIB REQUIRED)
find_library(minizip_lib minizip REQUIRED) find_library(minizip_lib minizip REQUIRED)
find_path(minizip_inc minizip REQUIRED) find_path(minizip_inc minizip REQUIRED)
@@ -406,11 +587,6 @@ find_package(CURL REQUIRED)
find_path(mbedtls_inc mbedtls REQUIRED) find_path(mbedtls_inc mbedtls REQUIRED)
find_library(mbedcrypto_lib mbedcrypto 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 add_library(fatfs
source/ff16/diskio.c source/ff16/diskio.c
source/ff16/ff.c source/ff16/ff.c
@@ -426,19 +602,15 @@ set_target_properties(sphaira PROPERTIES
) )
target_link_libraries(sphaira PRIVATE target_link_libraries(sphaira PRIVATE
ftpsrv_helper
libhaze
libpulsar libpulsar
minIni minIni
nanovg nanovg
stb stb
yyjson yyjson
# libusbhsfs
libnxtc libnxtc
fatfs fatfs
dr_libs dr_libs
id3v2lib libzstd_static
libusbdvd
${minizip_lib} ${minizip_lib}
ZLIB::ZLIB ZLIB::ZLIB
@@ -446,20 +618,6 @@ target_link_libraries(sphaira PRIVATE
${mbedcrypto_lib} ${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 target_include_directories(sphaira PRIVATE
include include
${minizip_inc} ${minizip_inc}

View File

@@ -95,7 +95,6 @@ public:
static auto GetInstallEmummcEnable() -> bool; static auto GetInstallEmummcEnable() -> bool;
static auto GetInstallSdEnable() -> bool; static auto GetInstallSdEnable() -> bool;
static auto GetThemeMusicEnable() -> bool; static auto GetThemeMusicEnable() -> bool;
static auto Get12HourTimeEnable() -> bool;
static auto GetLanguage() -> long; static auto GetLanguage() -> long;
static auto GetTextScrollSpeed() -> long; static auto GetTextScrollSpeed() -> long;
@@ -127,10 +126,13 @@ public:
static void DisplayThemeOptions(bool left_side = true); static void DisplayThemeOptions(bool left_side = true);
// todo: // todo:
static void DisplayNetworkOptions(bool left_side = true); 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 DisplayAdvancedOptions(bool left_side = true);
static void DisplayInstallOptions(bool left_side = true); static void DisplayInstallOptions(bool left_side = true);
static void DisplayDumpOptions(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 // helper for sidebar options to toggle install on/off
static void ShowEnableInstallPromptOption(option::OptionBool& option, bool& enable); static void ShowEnableInstallPromptOption(option::OptionBool& option, bool& enable);
@@ -155,6 +157,9 @@ public:
static Result SetDefaultBackgroundMusic(fs::Fs* fs, const fs::FsPath& path); static Result SetDefaultBackgroundMusic(fs::Fs* fs, const fs::FsPath& path);
static void SetBackgroundMusicPause(bool pause); 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. // helper that converts 1.2.3 to a u32 used for comparisons.
static auto GetVersionFromString(const char* str) -> u32; static auto GetVersionFromString(const char* str) -> u32;
static auto IsVersionNewer(const char* current, const char* new_version) -> u32; static auto IsVersionNewer(const char* current, const char* new_version) -> u32;
@@ -267,6 +272,7 @@ public:
PadState m_pad{}; PadState m_pad{};
TouchInfo m_touch_info{}; TouchInfo m_touch_info{};
Controller m_controller{}; Controller m_controller{};
KeyboardState m_keyboard{};
std::vector<ThemeMeta> m_theme_meta_entries; std::vector<ThemeMeta> m_theme_meta_entries;
Vec2 m_scale{1, 1}; 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_default_music{INI_SECTION, "default_music", "/config/sphaira/themes/default_music.bfstm"};
option::OptionString m_theme_path{INI_SECTION, "theme", DEFAULT_THEME_PATH}; option::OptionString m_theme_path{INI_SECTION, "theme", DEFAULT_THEME_PATH};
option::OptionBool m_theme_music{INI_SECTION, "theme_music", true}; option::OptionBool m_theme_music{INI_SECTION, "theme_music", true};
option::OptionBool m_12hour_time{INI_SECTION, "12hour_time", false}; option::OptionBool m_show_ip_addr{INI_SECTION, "show_ip_addr", true};
option::OptionLong m_language{INI_SECTION, "language", 0}; // auto 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_left_menu{INI_SECTION, "left_side_menu", "FileBrowser"};
option::OptionString m_right_menu{INI_SECTION, "right_side_menu", "Appstore"}; option::OptionString m_right_menu{INI_SECTION, "right_side_menu", "Appstore"};
option::OptionBool m_progress_boost_mode{INI_SECTION, "progress_boost_mode", true}; 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 // todo: move this into it's own menu
option::OptionLong m_text_scroll_speed{"accessibility", "text_scroll_speed", 1}; // normal option::OptionLong m_text_scroll_speed{"accessibility", "text_scroll_speed", 1}; // normal
// 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{}; std::shared_ptr<fs::FsNativeSd> m_fs{};
audio::SongID m_background_music{}; audio::SongID m_background_music{};

View File

@@ -511,7 +511,22 @@ enum class SphairaResult : Result {
FsNewPathEmpty, FsNewPathEmpty,
FsLoadingCancelled, FsLoadingCancelled,
FsBrokenRoot, FsBrokenRoot,
FsUnknownStdioError, FsUnknownStdioError,
FsStdioFailedToSeek,
FsStdioFailedToRead,
FsStdioFailedToWrite,
FsStdioFailedToOpenFile,
FsStdioFailedToCreate,
FsStdioFailedToTruncate,
FsStdioFailedToFlush,
FsStdioFailedToCreateDirectory,
FsStdioFailedToDeleteFile,
FsStdioFailedToDeleteDirectory,
FsStdioFailedToOpenDirectory,
FsStdioFailedToRename,
FsStdioFailedToStat,
FsReadOnly, FsReadOnly,
FsNotActive, FsNotActive,
FsFailedStdioStat, FsFailedStdioStat,
@@ -680,6 +695,19 @@ enum : Result {
MAKE_SPHAIRA_RESULT_ENUM(FsLoadingCancelled), MAKE_SPHAIRA_RESULT_ENUM(FsLoadingCancelled),
MAKE_SPHAIRA_RESULT_ENUM(FsBrokenRoot), MAKE_SPHAIRA_RESULT_ENUM(FsBrokenRoot),
MAKE_SPHAIRA_RESULT_ENUM(FsUnknownStdioError), 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(FsReadOnly),
MAKE_SPHAIRA_RESULT_ENUM(FsNotActive), MAKE_SPHAIRA_RESULT_ENUM(FsNotActive),
MAKE_SPHAIRA_RESULT_ENUM(FsFailedStdioStat), MAKE_SPHAIRA_RESULT_ENUM(FsFailedStdioStat),
@@ -823,11 +851,83 @@ enum : Result {
#define CONCATENATE(s1, s2) CONCATENATE_IMPL(s1, s2) #define CONCATENATE(s1, s2) CONCATENATE_IMPL(s1, s2)
#define ANONYMOUS_VARIABLE(pref) CONCATENATE(pref, __COUNTER__) #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_FAIL(_f) std::experimental::scope_exit ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE_){[&] { if (R_FAILED(rc)) { _f; } }};
// #define ON_SCOPE_SUCCESS(_f) std::experimental::scope_exit ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE_){[&] { if (R_SUCCEEDED(rc)) { _f; } }}; // #define ON_SCOPE_SUCCESS(_f) std::experimental::scope_exit ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE_){[&] { if (R_SUCCEEDED(rc)) { _f; } }};
// mutex helpers.
#define SCOPED_MUTEX(mutex) \
mutexLock(mutex); \
ON_SCOPE_EXIT(mutexUnlock(mutex))

View File

@@ -23,8 +23,6 @@ enum DumpLocationType {
DumpLocationType_DevNull, DumpLocationType_DevNull,
// dump to stdio, ideal for custom mount points using devoptab, such as hdd. // dump to stdio, ideal for custom mount points using devoptab, such as hdd.
DumpLocationType_Stdio, DumpLocationType_Stdio,
// dump to custom locations found in locations.ini.
DumpLocationType_Network,
}; };
enum DumpLocationFlag { enum DumpLocationFlag {
@@ -33,8 +31,7 @@ enum DumpLocationFlag {
DumpLocationFlag_UsbS2S = 1 << DumpLocationType_UsbS2S, DumpLocationFlag_UsbS2S = 1 << DumpLocationType_UsbS2S,
DumpLocationFlag_DevNull = 1 << DumpLocationType_DevNull, DumpLocationFlag_DevNull = 1 << DumpLocationType_DevNull,
DumpLocationFlag_Stdio = 1 << DumpLocationType_Stdio, DumpLocationFlag_Stdio = 1 << DumpLocationType_Stdio,
DumpLocationFlag_Network = 1 << DumpLocationType_Network, DumpLocationFlag_All = DumpLocationFlag_SdCard | DumpLocationFlag_Usb | DumpLocationFlag_UsbS2S | DumpLocationFlag_DevNull | DumpLocationFlag_Stdio,
DumpLocationFlag_All = DumpLocationFlag_SdCard | DumpLocationFlag_Usb | DumpLocationFlag_UsbS2S | DumpLocationFlag_DevNull | DumpLocationFlag_Stdio | DumpLocationFlag_Network,
}; };
struct DumpEntry { struct DumpEntry {
@@ -44,7 +41,6 @@ struct DumpEntry {
struct DumpLocation { struct DumpLocation {
DumpEntry entry{}; DumpEntry entry{};
location::Entries network{};
location::StdioEntries stdio{}; 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 <dirent.h>
#include <cstring> #include <cstring>
#include <vector> #include <vector>
#include <span>
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <sys/syslimits.h>
#include "defines.hpp" #include "defines.hpp"
namespace fs { 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 { struct FsPath {
FsPath() = default; FsPath() = default;
@@ -138,20 +152,24 @@ struct FsPath {
return *this; 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 { 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 { 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 { 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 { 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) { static consteval bool Test(const auto& str) {
@@ -164,7 +182,7 @@ struct FsPath {
return path[0] == str[0]; return path[0] == str[0];
} }
char s[FS_MAX_PATH]{}; char s[PATH_MAX]{};
}; };
inline FsPath operator+(const char* v, const FsPath& fp) { 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. // 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 { 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} { } constexpr FsPathReal(const FsPath& str) : FsPathReal{str.s} { }
explicit constexpr FsPathReal(const char* str) { explicit constexpr FsPathReal(const char* str) {
@@ -211,7 +233,7 @@ struct FsPathReal {
constexpr operator const char*() const { return s; } constexpr operator const char*() const { return s; }
constexpr operator std::string_view() const { return s; } constexpr operator std::string_view() const { return s; }
char s[FS_MAX_PATH]; char s[PATH_MAX];
}; };
// fwd // fwd
@@ -229,7 +251,6 @@ struct File {
fs::Fs* m_fs{}; fs::Fs* m_fs{};
FsFile m_native{}; FsFile m_native{};
std::FILE* m_stdio{}; std::FILE* m_stdio{};
s64 m_stdio_off{};
u32 m_mode{}; u32 m_mode{};
}; };
@@ -299,7 +320,7 @@ Result IsDirEmpty(fs::Fs* m_fs, const fs::FsPath& path, bool* out);
// helpers. // helpers.
Result read_entire_file(Fs* fs, const FsPath& path, std::vector<u8>& out); 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); Result copy_entire_file(Fs* fs, const FsPath& dst, const FsPath& src, bool ignore_read_only = true);
struct Fs { struct Fs {
@@ -346,7 +367,7 @@ struct Fs {
Result read_entire_file(const FsPath& path, std::vector<u8>& out) { Result read_entire_file(const FsPath& path, std::vector<u8>& out) {
return fs::read_entire_file(this, path, 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); return fs::write_entire_file(this, path, in, m_ignore_read_only);
} }
Result copy_entire_file(const FsPath& dst, const FsPath& src) { Result copy_entire_file(const FsPath& dst, const FsPath& src) {

View File

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

View File

@@ -2,7 +2,7 @@
#include <functional> #include <functional>
namespace sphaira::haze { namespace sphaira::libhaze {
bool Init(); bool Init();
bool IsInit(); 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 InitInstallMode(const OnInstallStart& on_start, const OnInstallWrite& on_write, const OnInstallClose& on_close);
void DisableInstallMode(); void DisableInstallMode();
} // namespace sphaira::haze } // namespace sphaira::libhaze

View File

@@ -3,23 +3,13 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include <switch.h> #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 { namespace sphaira::location {
struct Entry { using FsEntryFlag = ui::menu::filebrowser::FsEntryFlag;
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);
// helper for hdd devices. // helper for hdd devices.
// this doesn't really belong in this header, however // this doesn't really belong in this header, however
@@ -29,14 +19,19 @@ struct StdioEntry {
std::string mount{}; std::string mount{};
// ums0: (USB Flash Disk) // ums0: (USB Flash Disk)
std::string name{}; std::string name{};
// set if read-only. // FsEntryFlag
bool write_protect; 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>; using StdioEntries = std::vector<StdioEntry>;
// set write=true to filter out write protected devices. // set write=true to filter out write protected devices.
auto GetStdio(bool write) -> StdioEntries; auto GetStdio(bool write) -> StdioEntries;
auto GetFat() -> StdioEntries;
} // namespace sphaira::location } // namespace sphaira::location

View File

@@ -2,10 +2,11 @@
#include <switch.h> #include <switch.h>
#include <string> #include <string>
#include <sys/syslimits.h>
namespace sphaira::swkbd { 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 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* guide = nullptr, const char* initial = nullptr, s64 len_min = -1, s64 len_max = FS_MAX_PATH); 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 } // namespace sphaira::swkbd

View File

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

View File

@@ -7,6 +7,7 @@
#include "fs.hpp" #include "fs.hpp"
#include "option.hpp" #include "option.hpp"
#include "hasher.hpp" #include "hasher.hpp"
#include "nro.hpp"
#include <span> #include <span>
namespace sphaira::ui::menu::filebrowser { namespace sphaira::ui::menu::filebrowser {
@@ -16,18 +17,16 @@ enum FsOption : u32 {
// can split screen. // can split screen.
FsOption_CanSplit = BIT(0), FsOption_CanSplit = BIT(0),
// can upload files.
FsOption_CanUpload = BIT(1),
// can selected multiple files. // can selected multiple files.
FsOption_CanSelect = BIT(2), FsOption_CanSelect = BIT(1),
// shows the option to install. // shows the option to install.
FsOption_CanInstall = BIT(3), FsOption_CanInstall = BIT(2),
// loads file assoc. // loads file assoc.
FsOption_LoadAssoc = BIT(4), FsOption_LoadAssoc = BIT(3),
// do not prompt on exit even if not tabbed. // 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_All = FsOption_DoNotPrompt | FsOption_Normal,
FsOption_Picker = FsOption_NONE, FsOption_Picker = FsOption_NONE,
}; };
@@ -39,7 +38,12 @@ enum FsEntryFlag {
// supports file assoc. // supports file assoc.
FsEntryFlag_Assoc = 1 << 1, FsEntryFlag_Assoc = 1 << 1,
// this is an sd card, files can be launched from here. // 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 { enum class FsType {
@@ -90,6 +94,22 @@ struct FsEntry {
return flags & FsEntryFlag_IsSd; 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 { auto IsSame(const FsEntry& e) const {
return root == e.root && type == e.type; return root == e.root && type == e.type;
} }
@@ -460,6 +480,15 @@ protected:
std::vector<std::string> m_filter{}; 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_sort{INI_SECTION, "sort", SortType::SortType_Alphabetical};
option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Descending}; option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Descending};
option::OptionBool m_show_hidden{INI_SECTION, "show_hidden", false}; option::OptionBool m_show_hidden{INI_SECTION, "show_hidden", false};

View File

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

View File

@@ -34,7 +34,7 @@ auto GetNroEntries() -> std::span<const NroEntry>;
void SignalChange(); void SignalChange();
struct Menu final : grid::Menu { struct Menu final : grid::Menu {
Menu(); Menu(u32 flags);
~Menu(); ~Menu();
auto GetShortTitle() const -> const char* override { return "Apps"; }; 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 // this holds 2 menus and allows for switching between them
struct MainMenu final : Widget { struct MainMenu final : Widget {

View File

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

View File

@@ -12,6 +12,14 @@
namespace sphaira::ui::menu::save { 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 { struct Entry final : FsSaveDataInfo {
NacpLanguageEntry lang{}; NacpLanguageEntry lang{};
int image{}; int image{};
@@ -82,13 +90,13 @@ private:
void DisplayOptions(); void DisplayOptions();
void BackupSaves(std::vector<std::reference_wrapper<Entry>>& entries); void BackupSaves(std::vector<std::reference_wrapper<Entry>>& entries, u32 flags);
void RestoreSave(); void RestoreSave();
auto BuildSavePath(const Entry& e, bool is_auto) const -> fs::FsPath; auto BuildSavePath(const Entry& e, u32 flags) const -> fs::FsPath;
Result RestoreSaveInternal(ProgressBox* pbox, const Entry& e, const fs::FsPath& path) const; Result RestoreSaveInternal(ProgressBox* pbox, const Entry& e, const fs::FsPath& path);
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, Entry& e, u32 flags);
Result BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& location, std::span<const std::reference_wrapper<Entry>> entries, bool compressed, bool is_auto = false) const; Result BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& location, std::span<const std::reference_wrapper<Entry>> entries, u32 flags);
Result MountSaveFs(); 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 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 updateHighlightAnimation();
void getHighlightAnimation(float* gradientX, float* gradientY, float* color); void getHighlightAnimation(float* gradientX, float* gradientY, float* color);

View File

@@ -11,6 +11,7 @@ namespace sphaira::ui {
struct ProgressBox; struct ProgressBox;
using ProgressBoxCallback = std::function<Result(ProgressBox*)>; using ProgressBoxCallback = std::function<Result(ProgressBox*)>;
using ProgressBoxDoneCallback = std::function<void(Result rc)>; using ProgressBoxDoneCallback = std::function<void(Result rc)>;
// using CancelCallback = std::function<void()>;
struct ProgressBox final : Widget { struct ProgressBox final : Widget {
ProgressBox( ProgressBox(
@@ -39,6 +40,9 @@ struct ProgressBox final : Widget {
auto ShouldExit() -> bool; auto ShouldExit() -> bool;
auto ShouldExitResult() -> Result; auto ShouldExitResult() -> Result;
void AddCancelEvent(UEvent* event);
void RemoveCancelEvent(const UEvent* event);
// helper functions // 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_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; 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{}; Thread m_thread{};
ThreadData m_thread_data{}; ThreadData m_thread_data{};
ProgressBoxDoneCallback m_done{}; ProgressBoxDoneCallback m_done{};
std::vector<UEvent*> m_cancel_events{};
// shared data start. // shared data start.
std::string m_action{}; std::string m_action{};

View File

@@ -7,6 +7,7 @@
#include <memory> #include <memory>
#include <concepts> #include <concepts>
#include <utility> #include <utility>
#include <sys/syslimits.h>
namespace sphaira::ui { namespace sphaira::ui {
@@ -43,6 +44,14 @@ public:
m_depends_click = depends_click; m_depends_click = depends_click;
} }
void SetDirty(bool dirty = true) {
m_dirty = dirty;
}
auto IsDirty() const -> bool {
return m_dirty;
}
protected: protected:
auto IsEnabled() const -> bool { auto IsEnabled() const -> bool {
if (m_depends_callback) { if (m_depends_callback) {
@@ -68,6 +77,7 @@ private:
DependsClickCallback m_depends_click{}; DependsClickCallback m_depends_click{};
ScrollingText m_scolling_title{}; ScrollingText m_scolling_title{};
ScrollingText m_scolling_value{}; ScrollingText m_scolling_value{};
bool m_dirty{};
}; };
template<typename T> template<typename T>
@@ -174,12 +184,27 @@ private:
class SidebarEntryTextInput final : public SidebarEntryTextBase { class SidebarEntryTextInput final : public SidebarEntryTextBase {
public: 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: private:
const std::string m_header;
const std::string m_guide; const std::string m_guide;
const s64 m_len_min; const s64 m_len_min;
const s64 m_len_max; const s64 m_len_max;
const Callback m_callback;
}; };
class SidebarEntryFilePicker final : public SidebarEntryTextBase { class SidebarEntryFilePicker final : public SidebarEntryTextBase {
@@ -199,12 +224,12 @@ class Sidebar : public Widget {
public: public:
enum class Side { LEFT, RIGHT }; enum class Side { LEFT, RIGHT };
using Items = std::vector<std::unique_ptr<SidebarEntryBase>>; using Items = std::vector<std::unique_ptr<SidebarEntryBase>>;
using OnExitWhenChangedCallback = std::function<void()>;
public: public:
explicit Sidebar(const std::string& title, Side side, Items&& items); explicit Sidebar(const std::string& title, Side side, float width = 450.f);
explicit Sidebar(const std::string& title, Side side); explicit Sidebar(const std::string& title, const std::string& sub, Side side, float width = 450.f);
explicit Sidebar(const std::string& title, const std::string& sub, Side side, Items&& items); ~Sidebar();
explicit Sidebar(const std::string& title, const std::string& sub, Side side);
auto Update(Controller* controller, TouchInfo* touch) -> void override; auto Update(Controller* controller, TouchInfo* touch) -> void override;
auto Draw(NVGcontext* vg, Theme* theme) -> 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)...)); 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: private:
void SetIndex(s64 index); void SetIndex(s64 index);
void SetupButtons(); void SetupButtons();
@@ -226,19 +257,28 @@ private:
const std::string m_title; const std::string m_title;
const std::string m_sub; const std::string m_sub;
const Side m_side; const Side m_side;
Items m_items; Items m_items{};
s64 m_index{}; s64 m_index{};
std::unique_ptr<List> m_list; std::unique_ptr<List> m_list{};
Vec4 m_top_bar{}; Vec4 m_top_bar{};
Vec4 m_bottom_bar{}; Vec4 m_bottom_bar{};
Vec2 m_title_pos{}; Vec2 m_title_pos{};
Vec4 m_base_pos{}; Vec4 m_base_pos{};
OnExitWhenChangedCallback m_on_exit_when_changed{};
static constexpr float m_title_size{28.f}; static constexpr float m_title_size{28.f};
// static constexpr Vec2 box_size{380.f, 70.f}; // static constexpr Vec2 box_size{380.f, 70.f};
static constexpr Vec2 m_box_size{400.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 } // namespace sphaira::ui

View File

@@ -366,6 +366,81 @@ struct Action final {
std::string m_hint{}; 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 { struct Controller {
u64 m_kdown{}; u64 m_kdown{};
u64 m_kheld{}; u64 m_kheld{};
@@ -399,15 +474,29 @@ struct Controller {
void UpdateButtonHeld(u64 buttons, double delta) { void UpdateButtonHeld(u64 buttons, double delta) {
if (m_kdown & buttons) { if (m_kdown & buttons) {
m_step = 50; m_step_max = m_MAX_STEP;
m_step = m_INC_STEP;
m_counter = 0; m_counter = 0;
m_step_max_counter = 0;
} else if (m_kheld & buttons) { } else if (m_kheld & buttons) {
m_counter += m_step * delta; 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) { if (m_counter >= m_MAX) {
m_kdown |= m_kheld & buttons; m_kdown |= m_kheld & buttons;
m_counter = 0; 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: private:
static constexpr double m_MAX = 1000; static constexpr double m_MAX = 1000;
static constexpr double m_MAX_STEP = 250; 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; double m_counter = 0;
int m_step_max_counter = 0;
}; };
} // namespace sphaira } // namespace sphaira

View File

@@ -24,6 +24,14 @@ struct Usb {
// Result OpenFile(u32 index, s64& file_size); // Result OpenFile(u32 index, s64& file_size);
Result CloseFile(); Result CloseFile();
auto GetOpenResult() const {
return m_open_result;
}
auto GetCancelEvent() {
return m_usb->GetCancelEvent();
}
private: private:
Result SendAndVerify(const void* data, u32 size, u64 timeout, api::ResultPacket* out = nullptr); Result SendAndVerify(const void* data, u32 size, u64 timeout, api::ResultPacket* out = nullptr);
Result SendAndVerify(const void* data, u32 size, 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 OpenFile(u32 index, s64& file_size);
Result CloseFile(); Result CloseFile();
auto GetOpenResult() const {
return m_open_result;
}
auto GetCancelEvent() {
return m_usb->GetCancelEvent();
}
private: private:
Result SendAndVerify(const void* data, u32 size, u64 timeout, api::ResultPacket* out = nullptr); Result SendAndVerify(const void* data, u32 size, u64 timeout, api::ResultPacket* out = nullptr);
Result SendAndVerify(const void* data, u32 size, 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(); Result file_transfer_loop();
auto GetOpenResult() const {
return m_open_result;
}
auto GetCancelEvent() {
return m_usb->GetCancelEvent();
}
private: private:
Result SendResult(u32 result, u32 arg3 = 0, u32 arg4 = 0); Result SendResult(u32 result, u32 arg3 = 0, u32 arg4 = 0);

View File

@@ -2,34 +2,42 @@
#include "fs.hpp" #include "fs.hpp"
#include "yati/source/base.hpp" #include "yati/source/base.hpp"
#include "location.hpp"
#include <switch.h> #include <switch.h>
#include <memory> #include <memory>
namespace sphaira::devoptab { namespace sphaira::devoptab {
// mounts to "lower_case_hex_id:/"
Result MountSaveSystem(u64 id, fs::FsPath& out_path); 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); 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); 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 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); 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 MountNca(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path);
Result MountNcaNcm(NcmContentStorage* cs, const NcmContentId* id, 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); 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); 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 } // namespace sphaira::devoptab

View File

@@ -2,8 +2,13 @@
#include "yati/source/file.hpp" #include "yati/source/file.hpp"
#include "utils/lru.hpp" #include "utils/lru.hpp"
#include "location.hpp"
#include <memory> #include <memory>
#include <optional>
#include <span> #include <span>
#include <functional>
#include <unordered_map>
#include <curl/curl.h>
namespace sphaira::devoptab::common { namespace sphaira::devoptab::common {
@@ -81,6 +86,142 @@ private:
std::vector<BufferedFileData> buffered_large{}; // 1MiB 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 } // namespace sphaira::devoptab::common

View File

@@ -22,4 +22,11 @@ constexpr inline T AlignDown(T value, T align) {
return value &~ (align - 1); 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 } // namespace sphaira::utils

View File

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

View File

@@ -22,11 +22,11 @@
#include "haze_helper.hpp" #include "haze_helper.hpp"
#include "web.hpp" #include "web.hpp"
#include "swkbd.hpp" #include "swkbd.hpp"
#include "fatfs.hpp"
#include "usbdvd.hpp" #include "usbdvd.hpp"
#include "utils/profile.hpp" #include "utils/profile.hpp"
#include "utils/thread.hpp" #include "utils/thread.hpp"
#include "utils/devoptab.hpp"
#include <nanovg_dk.h> #include <nanovg_dk.h>
#include <minIni.h> #include <minIni.h>
@@ -37,7 +37,10 @@
#include <ctime> #include <ctime>
#include <span> #include <span>
#include <dirent.h> #include <dirent.h>
#include <usbhsfs.h>
#ifdef ENABLE_LIBUSBHSFS
#include <usbhsfs.h>
#endif // ENABLE_LIBUSBHSFS
extern "C" { extern "C" {
u32 __nx_applet_exit_mode = 0; u32 __nx_applet_exit_mode = 0;
@@ -71,6 +74,37 @@ struct NszOption {
const char* name; 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[] = { constexpr NszOption NSZ_COMPRESS_LEVEL_OPTIONS[] = {
{ .value = 0, .name = "Level 0 (no compression)" }, { .value = 0, .name = "Level 0 (no compression)" },
{ .value = 1, .name = "Level 1" }, { .value = 1, .name = "Level 1" },
@@ -542,7 +576,14 @@ void App::Loop() {
} }
auto App::Push(std::unique_ptr<ui::Widget>&& widget) -> void { 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. // check if the widget wants to pop before adding.
// this can happen if something failed in the constructor and the widget wants to exit. // 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(); return g_app->m_text_scroll_speed.Get();
} }
auto App::Get12HourTimeEnable() -> bool {
return g_app->m_12hour_time.Get();
}
auto App::GetNszCompressLevel() -> u8 { auto App::GetNszCompressLevel() -> u8 {
return NSZ_COMPRESS_LEVEL_OPTIONS[App::GetApp()->m_nsz_compress_level.Get()].value; 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) { void App::SetHddEnable(bool enable) {
if (App::GetHddEnable() != enable) { if (App::GetHddEnable() != enable) {
g_app->m_hdd_enabled.Set(enable); g_app->m_hdd_enabled.Set(enable);
#ifdef ENABLE_LIBUSBHSFS
if (enable) { if (enable) {
if (App::GetWriteProtect()) { if (App::GetWriteProtect()) {
usbHsFsSetFileSystemMountFlags(UsbHsFsMountFlags_ReadOnly); usbHsFsSetFileSystemMountFlags(UsbHsFsMountFlags_ReadOnly);
@@ -749,6 +787,7 @@ void App::SetHddEnable(bool enable) {
} else { } else {
usbHsFsExit(); usbHsFsExit();
} }
#endif // ENABLE_LIBUSBHSFS
} }
} }
@@ -756,11 +795,13 @@ void App::SetWriteProtect(bool enable) {
if (App::GetWriteProtect() != enable) { if (App::GetWriteProtect() != enable) {
g_app->m_hdd_write_protect.Set(enable); g_app->m_hdd_write_protect.Set(enable);
#ifdef ENABLE_LIBUSBHSFS
if (enable) { if (enable) {
usbHsFsSetFileSystemMountFlags(UsbHsFsMountFlags_ReadOnly); usbHsFsSetFileSystemMountFlags(UsbHsFsMountFlags_ReadOnly);
} else { } else {
usbHsFsSetFileSystemMountFlags(0); 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) { void App::SetMtpEnable(bool enable) {
if (App::GetMtpEnable() != enable) { if (App::GetMtpEnable() != enable) {
g_app->m_mtp_enabled.Set(enable); g_app->m_mtp_enabled.Set(enable);
#ifdef ENABLE_LIBHAZE
if (enable) { if (enable) {
haze::Init(); libhaze::Init();
} else { } else {
haze::Exit(); libhaze::Exit();
} }
#endif // ENABLE_LIBHAZE
} }
} }
void App::SetFtpEnable(bool enable) { void App::SetFtpEnable(bool enable) {
if (App::GetFtpEnable() != enable) { if (App::GetFtpEnable() != enable) {
g_app->m_ftp_enabled.Set(enable); g_app->m_ftp_enabled.Set(enable);
#ifdef ENABLE_FTPSRV
if (enable) { if (enable) {
ftpsrv::Init(); ftpsrv::Init();
} else { } else {
ftpsrv::Exit(); ftpsrv::Exit();
} }
#endif // ENABLE_FTPSRV
} }
} }
@@ -1006,6 +1049,16 @@ void App::Poll() {
hidGetTouchScreenStates(&state, 1); hidGetTouchScreenStates(&state, 1);
m_touch_info.is_clicked = false; 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 // todo: replace old touch code with gestures from below
#if 0 #if 0
static HidGestureState prev_gestures[17]{}; static HidGestureState prev_gestures[17]{};
@@ -1051,6 +1104,7 @@ void App::Poll() {
memcpy(prev_gestures, gestures, sizeof(gestures)); memcpy(prev_gestures, gestures, sizeof(gestures));
#endif #endif
// todo: support mouse scroll / touch.
if (state.count == 1 && !m_touch_info.is_touching) { if (state.count == 1 && !m_touch_info.is_touching) {
m_touch_info.initial = m_touch_info.cur = state.touches[0]; m_touch_info.initial = m_touch_info.cur = state.touches[0];
m_touch_info.is_touching = true; 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 // todo: better implement this to match hos
if (!m_touch_info.is_touching && !m_touch_info.is_clicked) { if (!m_touch_info.is_touching && !m_touch_info.is_clicked) {
// controller.
padUpdate(&m_pad); padUpdate(&m_pad);
m_controller.m_kdown = padGetButtonsDown(&m_pad); kdown |= padGetButtonsDown(&m_pad);
m_controller.m_kheld = padGetButtons(&m_pad); kheld |= padGetButtons(&m_pad);
m_controller.m_kup = padGetButtonsUp(&m_pad); kup |= padGetButtonsUp(&m_pad);
m_controller.UpdateButtonHeld(static_cast<u64>(Button::ANY_DIRECTION), m_delta_time);
// 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() { 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) { App::App(const char* argv0) {
// boost mode is enabled in userAppInit(). // boost mode is enabled in userAppInit().
ON_SCOPE_EXIT(App::SetBoostMode(false)); ON_SCOPE_EXIT(App::SetBoostMode(false));
@@ -1407,7 +1490,7 @@ App::App(const char* argv0) {
// init fs for app use. // init fs for app use.
m_fs = std::make_shared<fs::FsNativeSd>(true); 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); auto app = static_cast<App*>(UserData);
if (!std::strcmp(Section, INI_SECTION)) { 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_default_music.LoadFrom(Key, Value)) {}
else if (app->m_theme_path.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_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_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_left_menu.LoadFrom(Key, Value)) {}
else if (app->m_right_menu.LoadFrom(Key, Value)) {} else if (app->m_right_menu.LoadFrom(Key, Value)) {}
else if (app->m_install_sysmmc.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_ldm.LoadFrom(Key, Value)) {}
else if (app->m_nsz_compress_block.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 (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; return 1;
@@ -1472,7 +1587,7 @@ App::App(const char* argv0) {
if (App::GetLogEnable()) { if (App::GetLogEnable()) {
log_file_init(); 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 // 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/themes");
m_fs->CreateDirectory("/config/sphaira/github"); m_fs->CreateDirectory("/config/sphaira/github");
m_fs->CreateDirectory("/config/sphaira/i18n"); 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()) { if (App::GetMtpEnable()) {
SCOPED_TIMESTAMP("mtp init"); SCOPED_TIMESTAMP("mtp init");
haze::Init(); libhaze::Init();
} }
#endif // ENABLE_LIBHAZE
#ifdef ENABLE_FTPSRV
if (App::GetFtpEnable()) { if (App::GetFtpEnable()) {
SCOPED_TIMESTAMP("ftp init"); SCOPED_TIMESTAMP("ftp init");
ftpsrv::Init(); ftpsrv::Init();
} }
#endif // ENABLE_FTPSRV
if (App::GetNxlinkEnable()) { if (App::GetNxlinkEnable()) {
SCOPED_TIMESTAMP("nxlink init"); SCOPED_TIMESTAMP("nxlink init");
nxlinkInitialize(nxlink_callback); nxlinkInitialize(nxlink_callback);
} }
#ifdef ENABLE_LIBUSBHSFS
if (App::GetHddEnable()) { if (App::GetHddEnable()) {
SCOPED_TIMESTAMP("hdd init"); SCOPED_TIMESTAMP("hdd init");
if (App::GetWriteProtect()) { if (App::GetWriteProtect()) {
@@ -1570,26 +1693,85 @@ App::App(const char* argv0) {
usbHsFsInitialize(1); usbHsFsInitialize(1);
} }
#endif // ENABLE_LIBUSBHSFS
{ #ifdef ENABLE_LIBUSBDVD
SCOPED_TIMESTAMP("fat init");
if (R_FAILED(fatfs::MountAll())) {
log_write("[FAT] failed to mount bis\n");
}
}
{ {
SCOPED_TIMESTAMP("usbdvd init"); SCOPED_TIMESTAMP("usbdvd init");
if (R_FAILED(usbdvd::MountAll())) { if (R_FAILED(usbdvd::MountAll())) {
log_write("[USBDVD] failed to mount\n"); log_write("[USBDVD] failed to mount\n");
} }
} }
#endif // ENABLE_LIBUSBDVD
{ {
SCOPED_TIMESTAMP("curl init"); SCOPED_TIMESTAMP("curl init");
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"); SCOPED_TIMESTAMP("timestamp init");
// ini_putl(GetExePath(), "timestamp", m_start_timestamp, App::PLAYLOG_PATH); // ini_putl(GetExePath(), "timestamp", m_start_timestamp, App::PLAYLOG_PATH);
@@ -1599,9 +1781,14 @@ App::App(const char* argv0) {
SCOPED_TIMESTAMP("HID init"); SCOPED_TIMESTAMP("HID init");
hidInitializeTouchScreen(); hidInitializeTouchScreen();
hidInitializeGesture(); hidInitializeGesture();
hidInitializeKeyboard();
hidInitializeMouse();
padConfigureInput(8, HidNpadStyleSet_NpadStandard); padConfigureInput(8, HidNpadStyleSet_NpadStandard);
// padInitializeDefault(&m_pad); // padInitializeDefault(&m_pad);
padInitializeAny(&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. " "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); "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){ options->Add<ui::SidebarEntryBool>("Show IP address"_i18n, App::GetApp()->m_show_ip_addr,
App::Set12HourTimeEnable(enable); "Shows the IP address in all menus, including the WiFi strength.\n\n"
}, "Changes the clock to 12 hour"_i18n); "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 file picker for music here.
// todo: add array to audio which has the list of supported extensions. // 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) { void App::DisplayMenuOptions(bool left_side) {
auto options = std::make_unique<ui::Sidebar>("Misc Options"_i18n, left_side ? ui::Sidebar::Side::LEFT : ui::Sidebar::Side::RIGHT); 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))); ON_SCOPE_EXIT(App::Push(std::move(options)));
for (auto& e : ui::menu::main::GetMiscMenuEntries()) { for (auto& e : ui::menu::main::GetMenuMenuEntries()) {
if (e.name == g_app->m_left_menu.Get()) { if (e.name == g_app->m_center_menu.Get()) {
continue;
} else if (e.name == g_app->m_left_menu.Get()) {
continue; continue;
} else if (e.name == g_app->m_right_menu.Get()) { } else if (e.name == g_app->m_right_menu.Get()) {
continue; continue;
@@ -1859,7 +2049,7 @@ void App::DisplayAdvancedOptions(bool left_side) {
std::vector<std::string> menu_names; std::vector<std::string> menu_names;
ui::SidebarEntryArray::Items menu_items; ui::SidebarEntryArray::Items menu_items;
for (auto& e : ui::menu::main::GetMiscMenuEntries()) { for (auto& e : ui::menu::main::GetMenuMenuEntries()) {
if (!e.IsShortcut()) { if (!e.IsShortcut()) {
continue; 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" \ }, "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); "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, 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. " "Enables boost mode during transfers which can improve transfer speed. "
"This sets the CPU to 1785mhz and lowers the GPU 76mhz"_i18n); "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::SetTextScrollSpeed(index_out);
}, App::GetTextScrollSpeed(), "Change how fast the scrolling text updates"_i18n); }, 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){ options->Add<ui::SidebarEntryArray>("Set left-side menu"_i18n, menu_items, [menu_names](s64& index_out){
const auto e = menu_names[index_out]; const auto e = menu_names[index_out];
if (g_app->m_left_menu.Get() != e) { if (g_app->m_left_menu.Get() != e) {
// swap menus around. // 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_right_menu.Set(g_app->m_left_menu.Get());
} }
g_app->m_left_menu.Set(e); g_app->m_left_menu.Set(e);
@@ -1906,7 +2124,9 @@ void App::DisplayAdvancedOptions(bool left_side) {
const auto e = menu_names[index_out]; const auto e = menu_names[index_out];
if (g_app->m_right_menu.Get() != e) { if (g_app->m_right_menu.Get() != e) {
// swap menus around. // 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_left_menu.Set(g_app->m_right_menu.Get());
} }
g_app->m_right_menu.Set(e); 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](){ options->Add<ui::SidebarEntryCallback>("Export options"_i18n, [left_side](){
App::DisplayDumpOptions(left_side); App::DisplayDumpOptions(left_side);
}, "Change the export options."_i18n); }, "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) { 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); 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() { void App::ShowEnableInstallPrompt() {
// warn the user the dangers of installing. // warn the user the dangers of installing.
App::Push<ui::OptionBox>( App::Push<ui::OptionBox>(
@@ -2168,36 +2600,55 @@ App::~App() {
// async exit as these threads sleep every 100ms. // async exit as these threads sleep every 100ms.
{ {
SCOPED_TIMESTAMP("async signal"); SCOPED_TIMESTAMP("async signal");
nxlinkSignalExit(); #ifdef ENABLE_FTPSRV
ftpsrv::ExitSignal(); ftpsrv::ExitSignal();
#endif // ENABLE_FTPSRV
nxlinkSignalExit();
audio::ExitSignal(); audio::ExitSignal();
curl::ExitSignal(); curl::ExitSignal();
} }
utils::Async async_exit([this](){ // 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.
SCOPED_TIMESTAMP("usbdvd_exit"); // clear in reverse order as the widgets are a stack.
usbdvd::UnmountAll(); {
SCOPED_TIMESTAMP("widget exit");
while (!m_widgets.empty()) {
m_widgets.pop_back();
} }
}
utils::Async async_exit([this](){
{ {
SCOPED_TIMESTAMP("i18n_exit"); SCOPED_TIMESTAMP("i18n_exit");
i18n::exit(); i18n::exit();
} }
#ifdef ENABLE_LIBUSBDVD
{
SCOPED_TIMESTAMP("usbdvd_exit");
usbdvd::UnmountAll();
}
#endif // ENABLE_LIBUSBDVD
#ifdef ENABLE_LIBHAZE
{ {
SCOPED_TIMESTAMP("mtp exit"); SCOPED_TIMESTAMP("mtp exit");
haze::Exit(); libhaze::Exit();
} }
#endif // ENABLE_LIBHAZE
#ifdef ENABLE_LIBUSBHSFS
{ {
SCOPED_TIMESTAMP("hdd exit"); SCOPED_TIMESTAMP("hdd exit");
usbHsFsExit(); usbHsFsExit();
} }
#endif // ENABLE_LIBUSBHSFS
// this has to come before curl exit as it uses curl global.
{ {
SCOPED_TIMESTAMP("fatfs exit"); SCOPED_TIMESTAMP("devoptab exit");
fatfs::UnmountAll(); devoptab::UmountAllNeworkDevices();
} }
// do these last as they were signalled to exit. // do these last as they were signalled to exit.
@@ -2207,10 +2658,12 @@ App::~App() {
audio::Exit(); audio::Exit();
} }
#ifdef ENABLE_FTPSRV
{ {
SCOPED_TIMESTAMP("ftp exit"); SCOPED_TIMESTAMP("ftp exit");
ftpsrv::Exit(); ftpsrv::Exit();
} }
#endif // ENABLE_FTPSRV
{ {
SCOPED_TIMESTAMP("nxlink exit"); 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. // do not async close theme as it frees textures.
{ {
SCOPED_TIMESTAMP("theme exit"); SCOPED_TIMESTAMP("theme exit");
@@ -2249,6 +2683,11 @@ App::~App() {
CloseTheme(); CloseTheme();
} }
{
SCOPED_TIMESTAMP("destroy frame buffer resources");
this->destroyFramebufferResources();
}
{ {
SCOPED_TIMESTAMP("nvg exit"); SCOPED_TIMESTAMP("nvg exit");
nvgDeleteImage(vg, m_default_image); nvgDeleteImage(vg, m_default_image);

View File

@@ -33,8 +33,6 @@ namespace {
constexpr auto API_AGENT = "TotalJustice"; constexpr auto API_AGENT = "TotalJustice";
constexpr u64 CHUNK_SIZE = 1024*1024; constexpr u64 CHUNK_SIZE = 1024*1024;
constexpr auto MAX_THREADS = 4; constexpr auto MAX_THREADS = 4;
constexpr int THREAD_PRIO = 0x2F;
constexpr int THREAD_CORE = 2;
std::atomic_bool g_running{}; std::atomic_bool g_running{};
CURLSH* g_curl_share{}; CURLSH* g_curl_share{};
@@ -62,11 +60,6 @@ struct SeekCustomData {
s64 size{}; 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 { auto generate_key_from_path(const fs::FsPath& path) -> std::string {
const auto key = crc32Calculate(path.s, path.size()); const auto key = crc32Calculate(path.s, path.size());
return std::to_string(key); return std::to_string(key);
@@ -595,22 +588,16 @@ auto EscapeString(CURL* curl, const std::string& str) -> std::string {
return result; return result;
} }
auto EncodeUrl(std::string url) -> std::string { auto EncodeUrl(const std::string& url) -> std::string {
log_write("[CURL] encoding url\n"); 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(); auto clu = curl_url();
R_UNLESS(clu, url); R_UNLESS(clu, url);
ON_SCOPE_EXIT(curl_url_cleanup(clu)); ON_SCOPE_EXIT(curl_url_cleanup(clu));
log_write("[CURL] setting url\n"); log_write("[CURL] setting url\n");
CURLUcode clu_code; 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); R_UNLESS(clu_code == CURLUE_OK, url);
log_write("[CURL] set url success\n"); log_write("[CURL] set url success\n");
@@ -834,13 +821,6 @@ auto UploadInternal(CURL* curl, const Api& e) -> ApiResult {
return {}; 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& info = e.GetUploadInfo();
const auto url = e.GetUrl() + "/" + info.m_name; const auto url = e.GetUrl() + "/" + info.m_name;
const auto encoded_url = EncodeUrl(url); 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}; 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) { void my_lock(CURL *handle, curl_lock_data data, curl_lock_access laccess, void *useptr) {
mutexLock(&g_mutex_share[data]); mutexLock(&g_mutex_share[data]);
} }

View File

@@ -7,7 +7,6 @@
#include "i18n.hpp" #include "i18n.hpp"
#include "location.hpp" #include "location.hpp"
#include "threaded_file_transfer.hpp" #include "threaded_file_transfer.hpp"
#include "haze_helper.hpp"
#include "ui/sidebar.hpp" #include "ui/sidebar.hpp"
#include "ui/error_box.hpp" #include "ui/error_box.hpp"
@@ -133,9 +132,9 @@ struct WriteNullSource final : WriteSource {
struct WriteUsbSource final : WriteSource { struct WriteUsbSource final : WriteSource {
WriteUsbSource(u64 transfer_timeout) { WriteUsbSource(u64 transfer_timeout) {
// disable mtp if enabled. // disable mtp if enabled.
m_was_mtp_enabled = haze::IsInit(); m_was_mtp_enabled = App::GetMtpEnable();
if (m_was_mtp_enabled) { if (m_was_mtp_enabled) {
haze::Exit(); App::SetMtpEnable(false);
} }
m_usb = std::make_unique<usb::dump::Usb>(transfer_timeout); m_usb = std::make_unique<usb::dump::Usb>(transfer_timeout);
@@ -145,7 +144,7 @@ struct WriteUsbSource final : WriteSource {
m_usb.reset(); m_usb.reset();
if (m_was_mtp_enabled) { if (m_was_mtp_enabled) {
haze::Init(); App::SetMtpEnable(true);
} }
} }
@@ -165,6 +164,14 @@ struct WriteUsbSource final : WriteSource {
R_SUCCEED(); R_SUCCEED();
} }
auto GetOpenResult() const {
return m_usb->GetOpenResult();
}
auto GetCancelEvent() {
return m_usb->GetCancelEvent();
}
private: private:
std::unique_ptr<usb::dump::Usb> m_usb{}; std::unique_ptr<usb::dump::Usb> m_usb{};
bool m_was_mtp_enabled{}; bool m_was_mtp_enabled{};
@@ -178,8 +185,8 @@ constexpr DumpLocationEntry DUMP_LOCATIONS[]{
}; };
struct UsbTest final : usb::upload::Usb, yati::source::Stream { struct UsbTest final : usb::upload::Usb, yati::source::Stream {
UsbTest(ui::ProgressBox* pbox, BaseSource* source, std::span<const fs::FsPath> paths) UsbTest(ui::ProgressBox* pbox, BaseSource* source, std::span<const fs::FsPath> paths, u64 timeout)
: Usb{UINT64_MAX} : Usb{timeout}
, m_pbox{pbox} , m_pbox{pbox}
, m_source{source} , m_source{source}
, m_paths{paths} { , m_paths{paths} {
@@ -248,6 +255,10 @@ struct UsbTest final : usb::upload::Usb, yati::source::Stream {
return m_pull_offset; return m_pull_offset;
} }
auto GetOpenResult() const {
return Usb::GetOpenResult();
}
private: private:
ui::ProgressBox* m_pbox{}; ui::ProgressBox* m_pbox{};
BaseSource* m_source{}; 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) { 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) { for (const auto& path : paths) {
const auto file_size = source->GetSize(path); const auto file_size = source->GetSize(path);
@@ -273,7 +291,7 @@ Result DumpToUsb(ui::ProgressBox* pbox, BaseSource* source, std::span<const fs::
while (true) { while (true) {
R_TRY(pbox->ShouldExitResult()); 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)) { if (R_SUCCEEDED(rc)) {
break; break;
} }
@@ -318,7 +336,7 @@ Result DumpToFile(ui::ProgressBox* pbox, fs::Fs* fs, const fs::FsPath& root, Bas
{ {
fs::File file; fs::File file;
R_TRY(fs->OpenFile(temp_path, FsOpenMode_Write, &file)); R_TRY(fs->OpenFile(temp_path, FsOpenMode_Write|FsOpenMode_Append, &file));
auto write_source = std::make_unique<WriteFileSource>(&file); auto write_source = std::make_unique<WriteFileSource>(&file);
if (custom_transfer) { 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) { Result DumpToStdio(ui::ProgressBox* pbox, const location::StdioEntry& loc, BaseSource* source, std::span<const fs::FsPath> paths, const CustomTransfer& custom_transfer) {
fs::FsStdio fs{}; fs::FsStdio fs{};
return DumpToFile(pbox, &fs, loc.mount, source, paths, 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) { 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); file_list.emplace_back(path);
} }
auto usb = std::make_unique<UsbTest>(pbox, source, paths); // create usb test instance and verify that it opened.
constexpr u64 timeout = 3e+9; 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()) { while (!pbox->ShouldExit()) {
if (R_SUCCEEDED(usb->IsUsbConnected(timeout))) { if (R_SUCCEEDED(usb->IsUsbConnected(timeout))) {
@@ -461,54 +486,6 @@ Result DumpToDevNull(ui::ProgressBox* pbox, BaseSource* source, std::span<const
R_SUCCEED(); 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 } // namespace
void DumpGetLocation(const std::string& title, u32 location_flags, const OnLocation& on_loc, const CustomTransfer& custom_transfer) { 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; ui::PopupList::Items items;
std::vector<DumpEntry> dump_entries; std::vector<DumpEntry> dump_entries;
out.network = location::Load(); const auto stdio_entries = location::GetStdio(true);
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);
if (location_flags & (1 << DumpLocationType_Stdio)) { if (location_flags & (1 << DumpLocationType_Stdio)) {
for (s32 i = 0; i < std::size(out.stdio); i++) { for (auto& e : stdio_entries) {
dump_entries.emplace_back(DumpLocationType_Stdio, i); if (e.dump_hidden) {
items.emplace_back(out.stdio[i].name); 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) { 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) { if (location.entry.type == DumpLocationType_Stdio) {
R_TRY(DumpToNetwork(pbox, location.network[location.entry.index], source.get(), paths));
} else if (location.entry.type == DumpLocationType_Stdio) {
R_TRY(DumpToStdio(pbox, location.stdio[location.entry.index], source.get(), paths, custom_transfer)); R_TRY(DumpToStdio(pbox, location.stdio[location.entry.index], source.get(), paths, custom_transfer));
} else if (location.entry.type == DumpLocationType_SdCard) { } else if (location.entry.type == DumpLocationType_SdCard) {
R_TRY(DumpToFileNative(pbox, source.get(), paths, custom_transfer)); 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(); 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); 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) { 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); 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, "/"); 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(); R_SUCCEED();
} }
@@ -231,7 +235,7 @@ Result CreateDirectoryRecursivelyWithPath(FsFileSystem* fs, const FsPath& _path,
// strip file name form path. // strip file name form path.
const auto last_slash = std::strrchr(_path, '/'); const auto last_slash = std::strrchr(_path, '/');
if (!last_slash) { if (!last_slash || last_slash == _path.s) {
R_SUCCEED(); R_SUCCEED();
} }
@@ -317,12 +321,12 @@ Result CreateFile(const FsPathReal& path, u64 size, u32 option, bool ignore_read
} }
R_TRY(fsdevGetLastResult()); R_TRY(fsdevGetLastResult());
return Result_FsUnknownStdioError; return Result_FsStdioFailedToCreate;
} }
ON_SCOPE_EXIT(close(fd)); ON_SCOPE_EXIT(close(fd));
if (size) { if (size) {
R_UNLESS(!ftruncate(fd, size), Result_FsUnknownStdioError); R_UNLESS(!ftruncate(fd, size), Result_FsStdioFailedToTruncate);
} }
R_SUCCEED(); R_SUCCEED();
@@ -337,7 +341,7 @@ Result CreateDirectory(const FsPathReal& path, bool ignore_read_only) {
} }
R_TRY(fsdevGetLastResult()); R_TRY(fsdevGetLastResult());
return Result_FsUnknownStdioError; return Result_FsStdioFailedToCreateDirectory;
} }
R_SUCCEED(); R_SUCCEED();
} }
@@ -359,7 +363,7 @@ Result DeleteFile(const FsPathReal& path, bool ignore_read_only) {
if (unlink(path)) { if (unlink(path)) {
R_TRY(fsdevGetLastResult()); R_TRY(fsdevGetLastResult());
return Result_FsUnknownStdioError; return Result_FsStdioFailedToDeleteFile;
} }
R_SUCCEED(); R_SUCCEED();
} }
@@ -369,7 +373,7 @@ Result DeleteDirectory(const FsPathReal& path, bool ignore_read_only) {
if (rmdir(path)) { if (rmdir(path)) {
R_TRY(fsdevGetLastResult()); R_TRY(fsdevGetLastResult());
return Result_FsUnknownStdioError; return Result_FsStdioFailedToDeleteDirectory;
} }
R_SUCCEED(); R_SUCCEED();
} }
@@ -401,7 +405,7 @@ Result RenameFile(const FsPathReal& src, const FsPathReal& dst, bool ignore_read
if (rename(src, dst)) { if (rename(src, dst)) {
R_TRY(fsdevGetLastResult()); R_TRY(fsdevGetLastResult());
return Result_FsUnknownStdioError; return Result_FsStdioFailedToRename;
} }
R_SUCCEED(); R_SUCCEED();
} }
@@ -417,7 +421,7 @@ Result GetEntryType(const FsPathReal& path, FsDirEntryType* out) {
struct stat st; struct stat st;
if (stat(path, &st)) { if (stat(path, &st)) {
R_TRY(fsdevGetLastResult()); R_TRY(fsdevGetLastResult());
return Result_FsUnknownStdioError; return Result_FsStdioFailedToStat;
} }
*out = S_ISREG(st.st_mode) ? FsDirEntryType_File : FsDirEntryType_Dir; *out = S_ISREG(st.st_mode) ? FsDirEntryType_File : FsDirEntryType_Dir;
R_SUCCEED(); R_SUCCEED();
@@ -427,7 +431,7 @@ Result GetFileTimeStampRaw(const FsPathReal& path, FsTimeStampRaw *out) {
struct stat st; struct stat st;
if (stat(path, &st)) { if (stat(path, &st)) {
R_TRY(fsdevGetLastResult()); R_TRY(fsdevGetLastResult());
return Result_FsUnknownStdioError; return Result_FsStdioFailedToStat;
} }
out->is_valid = true; 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) { 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_fs = fs;
f->m_mode = mode; 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+"); 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(); R_SUCCEED();
@@ -500,9 +515,11 @@ Result File::Read( s64 off, void* buf, u64 read_size, u32 option, u64* bytes_rea
} else { } else {
R_UNLESS(m_stdio, Result_FsUnknownStdioError); R_UNLESS(m_stdio, Result_FsUnknownStdioError);
if (m_stdio_off != off) { if (off != std::ftell(m_stdio)) {
m_stdio_off = off; const auto ret = std::fseek(m_stdio, off, SEEK_SET);
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); *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 we read less bytes than expected, check if there was an error (ignoring eof).
if (*bytes_read < read_size) { if (*bytes_read < read_size) {
if (!std::feof(m_stdio) && std::ferror(m_stdio)) { 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(); R_SUCCEED();
@@ -528,17 +544,15 @@ Result File::Write(s64 off, const void* buf, u64 write_size, u32 option) {
} else { } else {
R_UNLESS(m_stdio, Result_FsUnknownStdioError); R_UNLESS(m_stdio, Result_FsUnknownStdioError);
if (m_stdio_off != off) { if (off != std::ftell(m_stdio)) {
log_write("[FS] diff seek\n"); const auto ret = std::fseek(m_stdio, off, SEEK_SET);
m_stdio_off = off; R_UNLESS(ret == 0, Result_FsStdioFailedToSeek);
std::fseek(m_stdio, off, SEEK_SET); R_UNLESS(off == std::ftell(m_stdio), Result_FsStdioFailedToSeek);
} }
const auto result = std::fwrite(buf, 1, write_size, m_stdio); const auto result = std::fwrite(buf, 1, write_size, m_stdio);
// log_write("[FS] fwrite res: %zu vs %zu\n", result, write_size); // log_write("[FS] fwrite res: %zu vs %zu\n", result, write_size);
R_UNLESS(result == write_size, Result_FsUnknownStdioError); R_UNLESS(result == write_size, Result_FsStdioFailedToWrite);
m_stdio_off += write_size;
} }
R_SUCCEED(); R_SUCCEED();
@@ -552,8 +566,8 @@ Result File::SetSize(s64 sz) {
} else { } else {
R_UNLESS(m_stdio, Result_FsUnknownStdioError); R_UNLESS(m_stdio, Result_FsUnknownStdioError);
const auto fd = fileno(m_stdio); const auto fd = fileno(m_stdio);
R_UNLESS(fd > 0, Result_FsUnknownStdioError); R_UNLESS(fd > 0, Result_FsStdioFailedToTruncate);
R_UNLESS(!ftruncate(fd, sz), Result_FsUnknownStdioError); R_UNLESS(!ftruncate(fd, sz), Result_FsStdioFailedToTruncate);
} }
R_SUCCEED(); R_SUCCEED();
@@ -568,7 +582,7 @@ Result File::GetSize(s64* out) {
R_UNLESS(m_stdio, Result_FsUnknownStdioError); R_UNLESS(m_stdio, Result_FsUnknownStdioError);
struct stat st; 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; *out = st.st_size;
} }
@@ -590,6 +604,7 @@ void File::Close() {
} }
} else { } else {
if (m_stdio) { if (m_stdio) {
log_write("[FS] closing stdio file\n");
std::fclose(m_stdio); std::fclose(m_stdio);
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)); R_TRY(fsFsOpenDirectory(&fs->m_fs, path, mode, &d->m_native));
} else { } else {
d->m_stdio = opendir(path); d->m_stdio = opendir(path);
R_UNLESS(d->m_stdio, Result_FsUnknownStdioError); R_UNLESS(d->m_stdio, Result_FsStdioFailedToOpenDirectory);
} }
R_SUCCEED(); R_SUCCEED();
@@ -673,6 +688,20 @@ Result Dir::GetEntryCount(s64* out) {
if (!std::strcmp(d->d_name, ".") || !std::strcmp(d->d_name, "..")) { if (!std::strcmp(d->d_name, ".") || !std::strcmp(d->d_name, "..")) {
continue; 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)++; (*out)++;
} }

View File

@@ -6,11 +6,12 @@
#include "utils/thread.hpp" #include "utils/thread.hpp"
#include <algorithm> #include <algorithm>
#include <minIni.h>
#include <ftpsrv.h> #include <ftpsrv.h>
#include <ftpsrv_vfs.h> #include <ftpsrv_vfs.h>
#include <nx/vfs_nx.h> #include <nx/vfs_nx.h>
#include <nx/utils.h> #include <nx/utils.h>
#include <unistd.h>
#include <fcntl.h>
namespace sphaira::ftpsrv { namespace sphaira::ftpsrv {
namespace { namespace {
@@ -28,19 +29,21 @@ struct InstallSharedData {
bool enabled; bool enabled;
}; };
const char* INI_PATH = "/config/ftpsrv/config.ini"; FtpSrvConfig g_ftpsrv_config{};
FtpSrvConfig g_ftpsrv_config = {0}; int g_ftpsrv_mount_flags{};
std::atomic_bool g_should_exit = false; std::vector<VfsNxCustomPath> g_custom_vfs{};
bool g_is_running{false}; std::atomic_bool g_should_exit{};
Thread g_thread; bool g_is_running{};
Thread g_thread{};
Mutex g_mutex{}; Mutex g_mutex{};
void ftp_log_callback(enum FTP_API_LOG_TYPE type, const char* msg) { 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) { void ftp_progress_callback(void) {
sphaira::App::NotifyFlashLed(); App::NotifyFlashLed();
} }
InstallSharedData g_shared_data{}; InstallSharedData g_shared_data{};
@@ -274,65 +277,203 @@ FtpVfs g_vfs_install = {
.rename = vfs_install_rename, .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) { void loop(void* arg) {
log_write("[FTP] loop entered\n"); log_write("[FTP] loop entered\n");
// load config.
{ {
SCOPED_MUTEX(&g_mutex); 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; 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(); fsdev_wrapMountSdmc();
vfs_nx_init(g_custom_vfs.data(), std::size(g_custom_vfs), g_ftpsrv_mount_flags, false);
const VfsNxCustomPath custom = {
.name = "install",
.user = NULL,
.func = &g_vfs_install,
};
vfs_nx_init(&custom, mount_devices, save_writable, mount_bis, false);
} }
ON_SCOPE_EXIT( ON_SCOPE_EXIT(
@@ -363,16 +504,93 @@ bool Init() {
return false; return false;
} }
// if (R_FAILED(fsdev_wrapMountSdmc())) { g_ftpsrv_mount_flags = 0;
// log_write("[FTP] cannot mount sdmc\n"); g_ftpsrv_config = {};
// return false; g_custom_vfs.clear();
// }
// todo: replace everything with ini_browse for faster loading. auto app = App::GetApp();
// or load everything in the init thread. 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; 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); log_write("[FTP] failed to create nxlink thread: 0x%X\n", rc);
return false; return false;
} }
@@ -399,7 +617,9 @@ void Exit() {
threadWaitForExit(&g_thread); threadWaitForExit(&g_thread);
threadClose(&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"); log_write("[FTP] exitied\n");
} }

View File

@@ -4,9 +4,6 @@
#include <mbedtls/md5.h> #include <mbedtls/md5.h>
#include <utility> #include <utility>
#include <zlib.h>
#include <zstd.h>
namespace sphaira::hash { namespace sphaira::hash {
namespace { namespace {
@@ -81,131 +78,6 @@ private:
size_t m_in_size{}; size_t m_in_size{};
}; };
// this currently crashes when freeing the pool :/
#define USE_THREAD_POOL 0
struct HashZstd final : HashSource {
HashZstd() {
const auto num_threads = 3;
const auto level = ZSTD_CLEVEL_DEFAULT;
m_ctx = ZSTD_createCCtx();
if (!m_ctx) {
log_write("[ZSTD] failed to create ctx\n");
}
#if USE_THREAD_POOL
m_pool = ZSTD_createThreadPool(num_threads);
if (!m_pool) {
log_write("[ZSTD] failed to create pool\n");
}
if (ZSTD_isError(ZSTD_CCtx_refThreadPool(m_ctx, m_pool))) {
log_write("[ZSTD] failed ZSTD_CCtx_refThreadPool(m_pool)\n");
}
#endif
if (ZSTD_isError(ZSTD_CCtx_setParameter(m_ctx, ZSTD_c_compressionLevel, level))) {
log_write("[ZSTD] failed ZSTD_CCtx_setParameter(ZSTD_c_compressionLevel)\n");
}
if (ZSTD_isError(ZSTD_CCtx_setParameter(m_ctx, ZSTD_c_nbWorkers, num_threads))) {
log_write("[ZSTD] failed ZSTD_CCtx_setParameter(ZSTD_c_nbWorkers)\n");
}
m_out_buf.resize(ZSTD_CStreamOutSize());
}
~HashZstd() {
ZSTD_freeCCtx(m_ctx);
#if USE_THREAD_POOL
// crashes here during ZSTD_pthread_join()
// ZSTD_freeThreadPool(m_pool);
#endif
}
void Update(const void* buf, s64 size, s64 file_size) override {
ZSTD_inBuffer input = { buf, (u64)size, 0 };
const auto last_chunk = m_in_size + size >= file_size;
const auto mode = last_chunk ? ZSTD_e_end : ZSTD_e_continue;
while (input.pos < input.size) {
ZSTD_outBuffer output = { m_out_buf.data(), m_out_buf.size(), 0 };
const size_t remaining = ZSTD_compressStream2(m_ctx, &output , &input, mode);
if (ZSTD_isError(remaining)) {
log_write("[ZSTD] error: %zu\n", remaining);
break;
}
m_out_size += output.pos;
};
m_in_size += size;
}
void Get(std::string& out) override {
log_write("getting size: %zu vs %zu\n", m_out_size, m_in_size);
char str[64];
const u32 percentage = ((double)m_out_size / (double)m_in_size) * 100.0;
std::snprintf(str, sizeof(str), "%u%%", percentage);
out = str;
log_write("got size: %zu vs %zu\n", m_out_size, m_in_size);
}
private:
ZSTD_CCtx* m_ctx{};
ZSTD_threadPool* m_pool{};
std::vector<u8> m_out_buf{};
size_t m_in_size{};
size_t m_out_size{};
};
struct HashDeflate final : HashSource {
HashDeflate() {
deflateInit(&m_ctx, Z_DEFAULT_COMPRESSION);
m_out_buf.resize(deflateBound(&m_ctx, 1024*1024*16)); // max chunk size.
}
~HashDeflate() {
deflateEnd(&m_ctx);
}
void Update(const void* buf, s64 size, s64 file_size) override {
m_ctx.avail_in = size;
m_ctx.next_in = const_cast<Bytef*>((const Bytef*)buf);
const auto last_chunk = m_in_size + size >= file_size;
const auto mode = last_chunk ? Z_FINISH : Z_NO_FLUSH;
while (m_ctx.avail_in != 0) {
m_ctx.next_out = m_out_buf.data();
m_ctx.avail_out = m_out_buf.size();
const auto rc = deflate(&m_ctx, mode);
if (Z_OK != rc) {
if (Z_STREAM_END != rc) {
log_write("[ZLIB] deflate error: %d\n", rc);
}
break;
}
}
m_in_size += size;
}
void Get(std::string& out) override {
char str[64];
const u32 percentage = ((double)m_ctx.total_out / (double)m_in_size) * 100.0;
std::snprintf(str, sizeof(str), "%u%%", percentage);
out = str;
}
private:
z_stream m_ctx{};
std::vector<u8> m_out_buf{};
size_t m_in_size{};
};
struct HashCrc32 final : HashSource { struct HashCrc32 final : HashSource {
void Update(const void* buf, s64 size, s64 file_size) override { void Update(const void* buf, s64 size, s64 file_size) override {
m_seed = crc32CalculateWithSeed(m_seed, buf, size); m_seed = crc32CalculateWithSeed(m_seed, buf, size);
@@ -328,8 +200,6 @@ auto GetTypeStr(Type type) -> const char* {
case Type::Sha1: return "SHA1"; case Type::Sha1: return "SHA1";
case Type::Sha256: return "SHA256"; case Type::Sha256: return "SHA256";
case Type::Null: return "/dev/null (Speed Test)"; case Type::Null: return "/dev/null (Speed Test)";
case Type::Deflate: return "Deflate (Speed Test)";
case Type::Zstd: return "ZSTD (Speed Test)";
} }
return ""; 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::Sha1: return Hash(pbox, std::make_unique<HashSha1>(), source, out);
case Type::Sha256: return Hash(pbox, std::make_unique<HashSha256>(), source, out); case Type::Sha256: return Hash(pbox, std::make_unique<HashSha256>(), source, out);
case Type::Null: return Hash(pbox, std::make_unique<HashNull>(), source, out); case Type::Null: return Hash(pbox, std::make_unique<HashNull>(), source, out);
case Type::Deflate: return Hash(pbox, std::make_unique<HashDeflate>(), source, out);
case Type::Zstd: return Hash(pbox, std::make_unique<HashZstd>(), source, out);
} }
std::unreachable(); std::unreachable();
} }

View File

@@ -9,7 +9,7 @@
#include <algorithm> #include <algorithm>
#include <haze.h> #include <haze.h>
namespace sphaira::haze { namespace sphaira::libhaze {
namespace { namespace {
struct InstallSharedData { 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} { 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; fs::FsPath buf;
const auto len = std::strlen(GetName()); const auto len = std::strlen(GetName());
if (len && !strncasecmp(path + 1, GetName(), len)) { if (len && !strncasecmp(path, GetName(), len)) {
std::snprintf(buf, sizeof(buf), "/%s", path + 1 + len); std::snprintf(buf, sizeof(buf), "%s/%s", base, path + len);
} else { } 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; return buf;
} }
@@ -88,6 +89,10 @@ protected:
}; };
struct FsProxy final : FsProxyBase { 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) FsProxy(std::unique_ptr<fs::Fs>&& fs, const char* name, const char* display_name)
: FsProxyBase{name, display_name} : FsProxyBase{name, display_name}
, m_fs{std::forward<decltype(fs)>(fs)} { , 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 // TODO: impl this for stdio
Result GetTotalSpace(const char *path, s64 *out) override { Result GetTotalSpace(const char *path, s64 *out) override {
if (m_fs->IsNative()) { if (m_fs->IsNative()) {
auto fs = (fs::FsNative*)m_fs.get(); auto fs = (fs::FsNative*)m_fs.get();
return fsFsGetTotalSpace(&fs->m_fs, FixPath(path), out); return fsFsGetTotalSpace(&fs->m_fs, FixPath(path), out);
} }
// todo: use statvfs.
// then fallback to 256gb if not available.
*out = 1024ULL * 1024ULL * 1024ULL * 256ULL; *out = 1024ULL * 1024ULL * 1024ULL * 256ULL;
R_SUCCEED(); R_SUCCEED();
} }
Result GetFreeSpace(const char *path, s64 *out) override { Result GetFreeSpace(const char *path, s64 *out) override {
if (m_fs->IsNative()) { if (m_fs->IsNative()) {
auto fs = (fs::FsNative*)m_fs.get(); auto fs = (fs::FsNative*)m_fs.get();
return fsFsGetFreeSpace(&fs->m_fs, FixPath(path), out); return fsFsGetFreeSpace(&fs->m_fs, FixPath(path), out);
} }
// todo: use statvfs.
// then fallback to 256gb if not available.
*out = 1024ULL * 1024ULL * 1024ULL * 256ULL; *out = 1024ULL * 1024ULL * 1024ULL * 256ULL;
R_SUCCEED(); R_SUCCEED();
} }
Result GetEntryType(const char *path, FsDirEntryType *out_entry_type) override {
const auto rc = m_fs->GetEntryType(FixPath(path), out_entry_type); Result GetEntryType(const char *path, haze::FileAttrType *out_entry_type) override {
log_write("[HAZE] GetEntryType(%s) 0x%X\n", path, rc); FsDirEntryType type;
return rc; 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); 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 { Result DeleteFile(const char* path) override {
log_write("[HAZE] DeleteFile(%s)\n", path); log_write("[HAZE] DeleteFile(%s)\n", path);
return m_fs->DeleteFile(FixPath(path)); return m_fs->DeleteFile(FixPath(path));
} }
Result RenameFile(const char *old_path, const char *new_path) override { Result RenameFile(const char *old_path, const char *new_path) override {
log_write("[HAZE] RenameFile(%s -> %s)\n", old_path, new_path); log_write("[HAZE] RenameFile(%s -> %s)\n", old_path, new_path);
return m_fs->RenameFile(FixPath(old_path), FixPath(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)) { Result OpenFile(const char *path, haze::FileOpenMode mode, haze::File *out_file) override {
std::memcpy(&out_file->s, &fptr, sizeof(fptr)); log_write("[HAZE] OpenFile(%s)\n", path);
} else {
delete fptr; 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"); Result GetFileSize(haze::File *file, s64 *out_size) override {
fs::File* f; auto f = static_cast<File*>(file->impl);
std::memcpy(&f, &file->s, sizeof(f));
return f->GetSize(out_size); return f->GetSize(out_size);
} }
Result SetFileSize(FsFile *file, s64 size) override {
log_write("[HAZE] SetFileSize(%zd)\n", size); Result SetFileSize(haze::File *file, s64 size) override {
fs::File* f; auto f = static_cast<File*>(file->impl);
std::memcpy(&f, &file->s, sizeof(f));
return f->SetSize(size); 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); Result ReadFile(haze::File *file, s64 off, void *buf, u64 read_size, u64 *out_bytes_read) override {
fs::File* f; auto f = static_cast<File*>(file->impl);
std::memcpy(&f, &file->s, sizeof(f)); return f->Read(off, buf, read_size, FsReadOption_None, out_bytes_read);
return f->Read(off, buf, read_size, option, 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); Result WriteFile(haze::File *file, s64 off, const void *buf, u64 write_size) override {
fs::File* f; auto f = static_cast<File*>(file->impl);
std::memcpy(&f, &file->s, sizeof(f)); return f->Write(off, buf, write_size, FsWriteOption_None);
return f->Write(off, buf, write_size, option);
} }
void CloseFile(FsFile *file) override {
log_write("[HAZE] CloseFile()\n"); void CloseFile(haze::File *file) override {
fs::File* f; auto f = static_cast<File*>(file->impl);
std::memcpy(&f, &file->s, sizeof(f));
if (f) { if (f) {
delete f; delete f;
file->impl = nullptr;
} }
std::memset(file, 0, sizeof(*file));
} }
Result CreateDirectory(const char* path) override { Result CreateDirectory(const char* path) override {
log_write("[HAZE] DeleteFile(%s)\n", path);
return m_fs->CreateDirectory(FixPath(path)); return m_fs->CreateDirectory(FixPath(path));
} }
Result DeleteDirectoryRecursively(const char* path) override { Result DeleteDirectoryRecursively(const char* path) override {
log_write("[HAZE] DeleteDirectoryRecursively(%s)\n", path);
return m_fs->DeleteDirectoryRecursively(FixPath(path)); return m_fs->DeleteDirectoryRecursively(FixPath(path));
} }
Result RenameDirectory(const char *old_path, const char *new_path) override { 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)); 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)) { Result OpenDirectory(const char *path, haze::Dir *out_dir) override {
std::memcpy(&out_dir->s, &fptr, sizeof(fptr)); auto dir = new Dir();
} else { const auto rc = m_fs->OpenDirectory(FixPath(path), FsDirOpenMode_ReadDirs | FsDirOpenMode_ReadFiles | FsDirOpenMode_NoFileSize, dir);
delete fptr; 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); out_dir->impl = dir;
return rc; R_SUCCEED();
} }
Result ReadDirectory(FsDir *d, s64 *out_total_entries, size_t max_entries, FsDirectoryEntry *buf) override {
fs::Dir* f; Result ReadDirectory(haze::Dir *d, s64 *out_total_entries, size_t max_entries, haze::DirEntry *buf) override {
std::memcpy(&f, &d->s, sizeof(f)); auto dir = static_cast<Dir*>(d->impl);
const auto rc = f->Read(out_total_entries, max_entries, buf);
log_write("[HAZE] ReadDirectory(%zd) 0x%X\n", *out_total_entries, rc); std::vector<FsDirectoryEntry> entries(max_entries);
return rc; R_TRY(dir->Read(out_total_entries, entries.size(), entries.data()));
}
Result GetDirectoryEntryCount(FsDir *d, s64 *out_count) override { for (s64 i = 0; i < *out_total_entries; i++) {
fs::Dir* f; std::strcpy(buf[i].name, entries[i].name);
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;
} }
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: private:
@@ -240,9 +291,22 @@ private:
// fake fs that allows for files to create r/w on the root. // fake fs that allows for files to create r/w on the root.
// folders are not yet supported. // folders are not yet supported.
struct FsProxyVfs : FsProxyBase { struct FsProxyVfs : FsProxyBase {
struct File {
u64 index{};
haze::FileOpenMode mode{};
};
struct Dir {
u64 pos{};
};
using FsProxyBase::FsProxyBase; using FsProxyBase::FsProxyBase;
virtual ~FsProxyVfs() = default; virtual ~FsProxyVfs() = default;
auto FixPath(const char* path) const {
return FsProxyBase::FixPath("", path);
}
auto GetFileName(const char* s) -> const char* { auto GetFileName(const char* s) -> const char* {
const auto file_name = std::strrchr(s, '/'); const auto file_name = std::strrchr(s, '/');
if (!file_name || file_name[1] == '\0') { if (!file_name || file_name[1] == '\0') {
@@ -251,9 +315,9 @@ struct FsProxyVfs : FsProxyBase {
return file_name + 1; 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) == "/") { if (FixPath(path) == "/") {
*out_entry_type = FsDirEntryType_Dir; *out_entry_type = haze::FileAttrType_DIR;
R_SUCCEED(); R_SUCCEED();
} else { } else {
const auto file_name = GetFileName(path); const auto file_name = GetFileName(path);
@@ -264,11 +328,12 @@ struct FsProxyVfs : FsProxyBase {
}); });
R_UNLESS(it != m_entries.end(), FsError_PathNotFound); R_UNLESS(it != m_entries.end(), FsError_PathNotFound);
*out_entry_type = FsDirEntryType_File; *out_entry_type = haze::FileAttrType_FILE;
R_SUCCEED(); 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); const auto file_name = GetFileName(path);
R_UNLESS(file_name, FsError_PathNotFound); R_UNLESS(file_name, FsError_PathNotFound);
@@ -285,6 +350,7 @@ struct FsProxyVfs : FsProxyBase {
m_entries.emplace_back(entry); m_entries.emplace_back(entry);
R_SUCCEED(); R_SUCCEED();
} }
virtual Result DeleteFile(const char* path) { virtual Result DeleteFile(const char* path) {
const auto file_name = GetFileName(path); const auto file_name = GetFileName(path);
R_UNLESS(file_name, FsError_PathNotFound); R_UNLESS(file_name, FsError_PathNotFound);
@@ -297,6 +363,7 @@ struct FsProxyVfs : FsProxyBase {
m_entries.erase(it); m_entries.erase(it);
R_SUCCEED(); R_SUCCEED();
} }
virtual Result RenameFile(const char *old_path, const char *new_path) { virtual Result RenameFile(const char *old_path, const char *new_path) {
const auto file_name = GetFileName(old_path); const auto file_name = GetFileName(old_path);
R_UNLESS(file_name, FsError_PathNotFound); R_UNLESS(file_name, FsError_PathNotFound);
@@ -317,7 +384,8 @@ struct FsProxyVfs : FsProxyBase {
std::strcpy(it->name, file_name_new); std::strcpy(it->name, file_name_new);
R_SUCCEED(); 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); const auto file_name = GetFileName(path);
R_UNLESS(file_name, FsError_PathNotFound); R_UNLESS(file_name, FsError_PathNotFound);
@@ -326,65 +394,89 @@ struct FsProxyVfs : FsProxyBase {
}); });
R_UNLESS(it != m_entries.end(), FsError_PathNotFound); R_UNLESS(it != m_entries.end(), FsError_PathNotFound);
out_file->s.object_id = std::distance(m_entries.begin(), it); auto f = new File();
out_file->s.own_handle = mode; f->index = std::distance(m_entries.begin(), it);
f->mode = mode;
out_file->impl = f;
R_SUCCEED(); R_SUCCEED();
} }
virtual Result GetFileSize(FsFile *file, s64 *out_size) {
auto& e = m_entries[file->s.object_id]; virtual Result GetFileSize(haze::File *file, s64 *out_size) {
*out_size = e.file_size; auto f = static_cast<File*>(file->impl);
*out_size = m_entries[f->index].file_size;
R_SUCCEED(); R_SUCCEED();
} }
virtual Result SetFileSize(FsFile *file, s64 size) {
auto& e = m_entries[file->s.object_id]; virtual Result SetFileSize(haze::File *file, s64 size) {
e.file_size = size; auto f = static_cast<File*>(file->impl);
m_entries[f->index].file_size = size;
R_SUCCEED(); 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. // 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. // the code below can be used to benchmark mtp reads.
R_THROW(FsError_NotImplemented); 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); e.file_size = std::max<s64>(e.file_size, off + write_size);
R_SUCCEED(); 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 { Result CreateDirectory(const char* path) override {
R_THROW(FsError_NotImplemented); R_THROW(FsError_NotImplemented);
} }
Result DeleteDirectoryRecursively(const char* path) override { Result DeleteDirectoryRecursively(const char* path) override {
R_THROW(FsError_NotImplemented); R_THROW(FsError_NotImplemented);
} }
Result RenameDirectory(const char *old_path, const char *new_path) override { Result RenameDirectory(const char *old_path, const char *new_path) override {
R_THROW(FsError_NotImplemented); 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(); 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); Result ReadDirectory(haze::Dir *d, s64 *out_total_entries, size_t max_entries, haze::DirEntry *buf) override {
std::memcpy(buf, m_entries.data() + d->s.object_id, max_entries * sizeof(*buf)); auto dir = static_cast<Dir*>(d->impl);
d->s.object_id += max_entries;
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; *out_total_entries = max_entries;
R_SUCCEED(); R_SUCCEED();
} }
Result GetDirectoryEntryCount(FsDir *d, s64 *out_count) override {
Result GetDirectoryEntryCount(haze::Dir *d, s64 *out_count) override {
*out_count = m_entries.size(); *out_count = m_entries.size();
R_SUCCEED(); 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: protected:
@@ -398,13 +490,11 @@ struct FsDevNullProxy final : FsProxyVfs {
*out = 1024ULL * 1024ULL * 1024ULL * 256ULL; *out = 1024ULL * 1024ULL * 1024ULL * 256ULL;
R_SUCCEED(); R_SUCCEED();
} }
Result GetFreeSpace(const char *path, s64 *out) override { Result GetFreeSpace(const char *path, s64 *out) override {
*out = 1024ULL * 1024ULL * 1024ULL * 256ULL; *out = 1024ULL * 1024ULL * 1024ULL * 256ULL;
R_SUCCEED(); R_SUCCEED();
} }
bool MultiThreadTransfer(s64 size, bool read) override {
return true;
}
}; };
struct FsInstallProxy final : FsProxyVfs { struct FsInstallProxy final : FsProxyVfs {
@@ -447,6 +537,7 @@ struct FsInstallProxy final : FsProxyVfs {
return fs::FsNativeContentStorage(FsContentStorageId_User).GetTotalSpace("/", out); return fs::FsNativeContentStorage(FsContentStorageId_User).GetTotalSpace("/", out);
} }
} }
Result GetFreeSpace(const char *path, s64 *out) override { Result GetFreeSpace(const char *path, s64 *out) override {
if (App::GetApp()->m_install_sd.Get()) { if (App::GetApp()->m_install_sd.Get()) {
return fs::FsNativeContentStorage(FsContentStorageId_SdCard).GetFreeSpace("/", out); 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)); 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_TRY(FailedIfNotEnabled());
} }
R_SUCCEED(); 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(FailedIfNotEnabled());
R_TRY(IsValidFileType(path)); R_TRY(IsValidFileType(path));
R_TRY(FsProxyVfs::CreateFile(path, size, option)); R_TRY(FsProxyVfs::CreateFile(path, size));
R_SUCCEED(); 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(FailedIfNotEnabled());
R_TRY(IsValidFileType(path)); R_TRY(IsValidFileType(path));
R_TRY(FsProxyVfs::OpenFile(path, mode, out_file)); R_TRY(FsProxyVfs::OpenFile(path, mode, out_file));
log_write("[MTP] done file open: %s mode: 0x%X\n", path, mode); log_write("[MTP] done file open: %s mode: 0x%X\n", path, mode);
if (mode & FsOpenMode_Write) { if (mode == haze::FileOpenMode_WRITE) {
const auto& e = m_entries[out_file->s.object_id]; auto f = static_cast<File*>(out_file->impl);
const auto& e = m_entries[f->index];
// check if we already have this file queued. // check if we already have this file queued.
log_write("[MTP] checking if empty\n"); log_write("[MTP] checking if empty\n");
@@ -488,7 +582,8 @@ struct FsInstallProxy final : FsProxyVfs {
log_write("[MTP] got file: %s\n", path); log_write("[MTP] got file: %s\n", path);
R_SUCCEED(); 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); SCOPED_MUTEX(&g_shared_data.mutex);
if (!g_shared_data.enabled) { if (!g_shared_data.enabled) {
log_write("[MTP] failing as not enabled\n"); log_write("[MTP] failing as not enabled\n");
@@ -500,14 +595,20 @@ struct FsInstallProxy final : FsProxyVfs {
R_THROW(FsError_NotImplemented); 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(); 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{}; bool update{};
{ {
SCOPED_MUTEX(&g_shared_data.mutex); 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"); log_write("[MTP] closing current file\n");
if (g_shared_data.on_close) { if (g_shared_data.on_close) {
g_shared_data.on_close(); g_shared_data.on_close();
@@ -525,40 +626,36 @@ struct FsInstallProxy final : FsProxyVfs {
FsProxyVfs::CloseFile(file); 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; auto& e = *data;
switch (e.type) { switch (e.type) {
case ::haze::CallbackType_OpenSession: log_write("[LIBHAZE] Opening 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_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_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_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_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_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_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_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_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_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_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_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_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_WriteEnd: log_write("[LIBHAZE] Writing File Finished: %s\n", e.file.filename); break;
} }
#endif
App::NotifyFlashLed(); App::NotifyFlashLed();
} }
@@ -572,14 +669,43 @@ bool Init() {
return false; 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::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")); if (App::GetApp()->m_mtp_show_album.Get()) {
g_fs_entries.emplace_back(std::make_shared<FsDevNullProxy>("DevNull", "DevNull (Speed Test)")); g_fs_entries.emplace_back(std::make_shared<FsProxy>(std::make_unique<fs::FsNativeImage>(FsImageDirectoryId_Sd), "Album", "Album (Image SD)"));
g_fs_entries.emplace_back(std::make_shared<FsInstallProxy>("install", "Install (NSP, XCI, NSZ, XCZ)")); }
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; 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; return false;
} }
@@ -598,7 +724,7 @@ void Exit() {
return; return;
} }
::haze::Exit(); haze::Exit();
g_is_running = false; g_is_running = false;
g_should_exit = true; g_should_exit = true;
g_fs_entries.clear(); g_fs_entries.clear();
@@ -619,4 +745,4 @@ void DisableInstallMode() {
g_shared_data.enabled = false; g_shared_data.enabled = false;
} }
} // namespace sphaira::haze } // namespace sphaira::libhaze

View File

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

View File

@@ -1,84 +1,50 @@
#include "location.hpp" #include "location.hpp"
#include "fs.hpp" #include "fs.hpp"
#include "app.hpp" #include "app.hpp"
#include "usbdvd.hpp" #include "utils/devoptab.hpp"
#include <ff.h>
#include <cstring> #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 sphaira::location {
namespace { namespace {
constexpr fs::FsPath location_path{"/config/sphaira/locations.ini"};
} // namespace } // 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 { auto GetStdio(bool write) -> StdioEntries {
StdioEntries out{}; 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. // try and load usbdvd entry.
// todo: check if more than 1 entry is supported. // todo: check if more than 1 entry is supported.
// todo: only call if usbdvd is init. // todo: only call if usbdvd is init.
@@ -88,7 +54,9 @@ auto GetStdio(bool write) -> StdioEntries {
out.emplace_back(entry); out.emplace_back(entry);
} }
} }
#endif // ENABLE_LIBUSBDVD
#ifdef ENABLE_LIBUSBHSFS
// bail out early if usbhdd is disabled. // bail out early if usbhdd is disabled.
if (!App::GetHddEnable()) { if (!App::GetHddEnable()) {
log_write("[USBHSFS] not enabled\n"); log_write("[USBHSFS] not enabled\n");
@@ -111,25 +79,15 @@ auto GetStdio(bool write) -> StdioEntries {
char display_name[0x100]; 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); 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); log_write("\t[USBHSFS] %s name: %s serial: %s man: %s\n", e.name, e.product_name, e.serial_number, e.manufacturer);
} }
#endif // ENABLE_LIBUSBHSFS
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);
}
return out; return out;
} }

View File

@@ -56,8 +56,6 @@ void userAppInit(void) {
diagAbortWithResult(rc); diagAbortWithResult(rc);
if (R_FAILED(rc = plInitialize(PlServiceType_User))) if (R_FAILED(rc = plInitialize(PlServiceType_User)))
diagAbortWithResult(rc); diagAbortWithResult(rc);
if (R_FAILED(rc = psmInitialize()))
diagAbortWithResult(rc);
if (R_FAILED(rc = nifmInitialize(NifmServiceType_User))) if (R_FAILED(rc = nifmInitialize(NifmServiceType_User)))
diagAbortWithResult(rc); diagAbortWithResult(rc);
if (R_FAILED(rc = accountInitialize(is_application ? AccountServiceType_Application : AccountServiceType_System))) if (R_FAILED(rc = accountInitialize(is_application ? AccountServiceType_Application : AccountServiceType_System)))
@@ -83,7 +81,6 @@ void userAppExit(void) {
setExit(); setExit();
accountExit(); accountExit();
nifmExit(); nifmExit();
psmExit();
plExit(); plExit();
socketExit(); socketExit();
// NOTE (DMC): prevents exfat corruption. // 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) { uLong minizip_read_file_func_stdio(voidpf opaque, voidpf stream, void* buf, uLong size) {
auto file = static_cast<std::FILE*>(stream); auto file = static_cast<std::FILE*>(stream);
log_write("[ZIP] doing read\n");
return std::fread(buf, 1, size, file); return std::fread(buf, 1, size, file);
} }
@@ -373,7 +372,7 @@ void FileFuncNative(zlib_filefunc64_def* funcs) {
Result PeekFirstFileName(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& name) { Result PeekFirstFileName(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& name) {
fs::File file; fs::File file;
R_TRY(fs->OpenFile(path, FsOpenMode_Read, &file)); R_TRY(fs->OpenFile(path, fs::OpenMode_ReadBuffered, &file));
mmz_LocalHeader local_hdr; mmz_LocalHeader local_hdr;
u64 bytes_read; 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>) { } else if constexpr(std::is_same_v<T, float>) {
m_value = ini_getf(m_section.c_str(), name, m_default_value, App::CONFIG_PATH); m_value = ini_getf(m_section.c_str(), name, m_default_value, App::CONFIG_PATH);
} else if constexpr(std::is_same_v<T, std::string>) { } else if constexpr(std::is_same_v<T, std::string>) {
char buf[FS_MAX_PATH]; char buf[PATH_MAX]{};
ini_gets(m_section.c_str(), name, m_default_value.c_str(), buf, sizeof(buf), App::CONFIG_PATH); ini_gets(m_section.c_str(), name, m_default_value.c_str(), buf, sizeof(buf), App::CONFIG_PATH);
m_value = buf; m_value = buf;
} }

View File

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

View File

@@ -82,6 +82,14 @@ struct ThreadData {
return read_running || decompress_running || write_running; 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 { auto GetWriteOffset() volatile const -> s64 {
return write_offset; return write_offset;
} }
@@ -94,8 +102,16 @@ struct ThreadData {
return &m_uevent_done; return &m_uevent_done;
} }
auto GetProgressEvent() { auto GetReadProgressEvent() {
return &m_uevent_progres; return &m_uevent_read_progress;
}
auto GetDecompressProgressEvent() {
return &m_uevent_decompress_progress;
}
auto GetWriteProgressEvent() {
return &m_uevent_write_progress;
} }
void SetReadResult(Result result) { void SetReadResult(Result result) {
@@ -174,7 +190,9 @@ private:
CondVar can_pull_write{}; CondVar can_pull_write{};
UEvent m_uevent_done{}; 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> read_buffers{};
RingBuf<2> write_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));
condvarInit(std::addressof(can_pull_write)); condvarInit(std::addressof(can_pull_write));
ueventCreate(&m_uevent_done, false); ueventCreate(GetDoneEvent(), false);
ueventCreate(&m_uevent_progres, true); ueventCreate(GetReadProgressEvent(), true);
ueventCreate(GetDecompressProgressEvent(), true);
ueventCreate(GetWriteProgressEvent(), true);
} }
auto ThreadData::GetResults() volatile -> Result { auto ThreadData::GetResults() volatile -> Result {
@@ -379,6 +399,7 @@ Result ThreadData::readFuncInternal() {
break; break;
} }
ueventSignal(GetReadProgressEvent());
auto buf_size = bytes_read; auto buf_size = bytes_read;
R_TRY(this->SetDecompressBuf(buf, buffer_offset, buf_size)); R_TRY(this->SetDecompressBuf(buf, buffer_offset, buf_size));
} }
@@ -423,25 +444,17 @@ Result ThreadData::decompressFuncInternal() {
} }
size -= rsize; size -= rsize;
this->decompress_offset += rsize;
data += rsize; data += rsize;
this->decompress_offset += rsize;
// const auto buf_off = temp_buf.size(); ueventSignal(GetDecompressProgressEvent());
// temp_buf.resize(buf_off + size);
// std::memcpy(temp_buf.data() + buf_off, data, size);
// this->decompress_offset += size;
// if (temp_buf.size() >= temp_buf_flush_max) {
// // log_write("flushing data: %zu %.2f MiB\n", temp_buf.size(), temp_buf.size() / 1024.0 / 1024.0);
// R_TRY(this->SetWriteBuf(temp_buf, temp_buf.size()));
// temp_buf.resize(0);
// }
} }
R_SUCCEED(); R_SUCCEED();
})); }));
} else { } else {
this->decompress_offset += buf.size(); this->decompress_offset += buf.size();
ueventSignal(GetDecompressProgressEvent());
R_TRY(this->SetWriteBuf(buf, buf.size())); R_TRY(this->SetWriteBuf(buf, buf.size()));
} }
} }
@@ -479,7 +492,7 @@ Result ThreadData::writeFuncInternal() {
} }
this->write_offset += size; this->write_offset += size;
ueventSignal(GetProgressEvent()); ueventSignal(GetWriteProgressEvent());
} }
log_write("finished write thread success!\n"); 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()); R_TRY(start_threads());
log_write("[THREAD] started threads\n"); 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_cancel = waiterForUEvent(pbox->GetCancelEvent());
const auto waiter_done = waiterForUEvent(t_data.GetDoneEvent()); 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; continue;
} }
const auto path_len = std::strlen(path);
if (!path_len) {
continue;
}
pbox->NewTransfer(name); pbox->NewTransfer(name);
if (path[std::strlen(path) -1] == '/') { if (path[path_len -1] == '/') {
Result rc; Result rc;
if (R_FAILED(rc = fs->CreateDirectoryRecursively(path)) && rc != FsError_PathAlreadyExists) { if (R_FAILED(rc = fs->CreateDirectoryRecursively(path)) && rc != FsError_PathAlreadyExists) {
log_write("failed to create folder: %s 0x%04X\n", path.s, rc); log_write("failed to create folder: %s 0x%04X\n", path.s, rc);

View File

@@ -28,6 +28,7 @@ struct ThreadData {
void Clear(); void Clear();
void PushAsync(u64 id); void PushAsync(u64 id);
void PushAsync(const std::span<const NsApplicationRecord> app_ids);
auto GetAsync(u64 app_id) -> ThreadResultData*; auto GetAsync(u64 app_id) -> ThreadResultData*;
auto Get(u64 app_id, bool* cached = nullptr) -> 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* { auto ThreadData::GetAsync(u64 app_id) -> ThreadResultData* {
SCOPED_MUTEX(&m_mutex_result); 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* { auto GetAsync(u64 app_id) -> ThreadResultData* {
SCOPED_MUTEX(&g_mutex); SCOPED_MUTEX(&g_mutex);
if (g_thread_data) { if (g_thread_data) {

View File

@@ -51,6 +51,19 @@ auto GetCodeMessage(Result rc) -> const char* {
case Result_FsLoadingCancelled: return "SphairaError_FsLoadingCancelled"; case Result_FsLoadingCancelled: return "SphairaError_FsLoadingCancelled";
case Result_FsBrokenRoot: return "SphairaError_FsBrokenRoot"; case Result_FsBrokenRoot: return "SphairaError_FsBrokenRoot";
case Result_FsUnknownStdioError: return "SphairaError_FsUnknownStdioError"; 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_FsReadOnly: return "SphairaError_FsReadOnly";
case Result_FsNotActive: return "SphairaError_FsNotActive"; case Result_FsNotActive: return "SphairaError_FsNotActive";
case Result_FsFailedStdioStat: return "SphairaError_FsFailedStdioStat"; case Result_FsFailedStdioStat: return "SphairaError_FsFailedStdioStat";

View File

@@ -65,11 +65,7 @@ auto List::ScrollDown(s64& index, s64 step, s64 count) -> bool {
return false; return false;
} }
if (index + step < count) { index = std::min(index + step, count - 1);
index += step;
} else {
index = count - 1;
}
if (index != old_index) { if (index != old_index) {
App::PlaySoundEffect(SoundEffect::Scroll); App::PlaySoundEffect(SoundEffect::Scroll);
@@ -103,11 +99,7 @@ auto List::ScrollUp(s64& index, s64 step, s64 count) -> bool {
return false; return false;
} }
if (index >= step) { index = std::max<s64>(0, index - step);
index -= step;
} else {
index = 0;
}
if (index != old_index) { if (index != old_index) {
App::PlaySoundEffect(SoundEffect::Scroll); 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_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 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 (controller->GotDown(Button::DOWN)) {
if (ScrollDown(index, m_row, count)) { if (ScrollDown(index, hotkey ? end_page : m_row, count)) {
callback(false, index); callback(false, index);
} }
} else if (controller->GotDown(Button::UP)) { } else if (controller->GotDown(Button::UP)) {
if (ScrollUp(index, m_row, count)) { if (ScrollUp(index, hotkey ? end_page : m_row, count)) {
callback(false, index); callback(false, index);
} }
} else if (controller->GotDown(page_down_button)) { } 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); callback(false, index);
} }
} else if (controller->GotDown(page_up_button)) { } 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); callback(false, index);
} }
} else if (m_row > 1 && controller->GotDown(Button::RIGHT)) { } else if (m_row > 1 && controller->GotDown(Button::RIGHT)) {

View File

@@ -20,6 +20,8 @@
#include "web.hpp" #include "web.hpp"
#include "minizip_helper.hpp" #include "minizip_helper.hpp"
#include "utils/utils.hpp"
#include <minIni.h> #include <minIni.h>
#include <string> #include <string>
#include <cstring> #include <cstring>
@@ -597,7 +599,8 @@ EntryMenu::EntryMenu(Entry& entry, const LazyImage& default_icon, Menu& menu)
options->Add<SidebarEntryCallback>("Leave Feedback"_i18n, [this](){ options->Add<SidebarEntryCallback>("Leave Feedback"_i18n, [this](){
std::string out; 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 post = "name=" "switch_user" "&package=" + m_entry.name + "&message=" + out;
const auto file = BuildFeedbackCachePath(m_entry); const auto file = BuildFeedbackCachePath(m_entry);
@@ -738,7 +741,7 @@ void EntryMenu::Draw(NVGcontext* vg, Theme* theme) {
text_start_y += text_inc_y; 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()); 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; 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; 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()); 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; 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](){ options->Add<SidebarEntryCallback>("Search"_i18n, [this](){
std::string out; std::string out;
if (R_SUCCEEDED(swkbd::ShowText(out)) && !out.empty()) { if (R_SUCCEEDED(swkbd::ShowText(out, "Search for app")) && !out.empty()) {
SetSearch(out); SetSearch(out);
log_write("got %s\n", out.c_str()); log_write("got %s\n", out.c_str());
} }

View File

@@ -10,6 +10,7 @@
#include "ui/error_box.hpp" #include "ui/error_box.hpp"
#include "ui/music_player.hpp" #include "ui/music_player.hpp"
#include "utils/utils.hpp"
#include "utils/devoptab.hpp" #include "utils/devoptab.hpp"
#include "log.hpp" #include "log.hpp"
@@ -31,7 +32,6 @@
#include "yati/yati.hpp" #include "yati/yati.hpp"
#include "yati/source/file.hpp" #include "yati/source/file.hpp"
#include <usbdvd.h>
#include <minIni.h> #include <minIni.h>
#include <minizip/zip.h> #include <minizip/zip.h>
#include <minizip/unzip.h> #include <minizip/unzip.h>
@@ -45,12 +45,16 @@
#include <utility> #include <utility>
#include <ranges> #include <ranges>
#ifdef ENABLE_LIBUSBDVD
#include <usbdvd.h>
#endif // ENABLE_LIBUSBDVD
namespace sphaira::ui::menu::filebrowser { namespace sphaira::ui::menu::filebrowser {
namespace { namespace {
using RomDatabaseIndexs = std::vector<size_t>; 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); explicit ForwarderForm(const FileAssocEntry& assoc, const RomDatabaseIndexs& db_indexs, const FileEntry& entry, const fs::FsPath& arg_path);
private: private:
@@ -70,7 +74,7 @@ private:
SidebarEntryFilePicker* m_icon{}; SidebarEntryFilePicker* m_icon{};
}; };
constinit UEvent g_change_uevent; std::atomic_bool g_change_signalled{};
constexpr FsEntry FS_ENTRY_DEFAULT{ constexpr FsEntry FS_ENTRY_DEFAULT{
"microSD card", "/", FsType::Sd, FsEntryFlag_Assoc | FsEntryFlag_IsSd, "microSD card", "/", FsType::Sd, FsEntryFlag_Assoc | FsEntryFlag_IsSd,
@@ -78,8 +82,7 @@ constexpr FsEntry FS_ENTRY_DEFAULT{
constexpr FsEntry FS_ENTRIES[]{ constexpr FsEntry FS_ENTRIES[]{
FS_ENTRY_DEFAULT, FS_ENTRY_DEFAULT,
{ "Image System memory", "/", FsType::ImageNand }, { "Album", "/", FsType::ImageSd},
{ "Image microSD card", "/", FsType::ImageSd},
}; };
constexpr std::string_view AUDIO_EXTENSIONS[] = { constexpr std::string_view AUDIO_EXTENSIONS[] = {
@@ -115,7 +118,7 @@ constexpr std::string_view ZIP_EXTENSIONS[] = {
}; };
// supported music playback extensions. // supported music playback extensions.
constexpr std::string_view MUSIC_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. // supported theme music playback extensions.
constexpr std::span THEME_MUSIC_EXTENSIONS = MUSIC_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) 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_assoc{assoc}
, m_db_indexs{db_indexs} , m_db_indexs{db_indexs}
, m_arg_path{arg_path} { , m_arg_path{arg_path} {
@@ -312,17 +315,17 @@ ForwarderForm::ForwarderForm(const FileAssocEntry& assoc, const RomDatabaseIndex
const auto icon = m_assoc.path; const auto icon = m_assoc.path;
m_name = this->Add<SidebarEntryTextInput>( 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 "Set the name of the application"_i18n
); );
m_author = this->Add<SidebarEntryTextInput>( 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 "Set the author of the application"_i18n
); );
m_version = this->Add<SidebarEntryTextInput>( 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 "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() { 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} { 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) { 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) { if (touch && m_index == i) {
FireAction(Button::A); FireAction(Button::A);
} else { } else {
App::PlaySoundEffect(SoundEffect::Focus); App::PlaySoundEffect(SoundEffect::Focus);
auto old_index = m_index;
SetIndex(i); 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); 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() && !m_fs_entry.IsNoStatDir() && (e.dir_count != -1 || !e.done_stat)) {
if (e.IsDir()) {
// NOTE: this takes longer than 16ms when opening a new folder due to it // NOTE: this takes longer than 16ms when opening a new folder due to it
// checking all 9 folders at once. // checking all 9 folders at once.
if (!got_dir_count && !e.done_stat && e.file_count == -1 && e.dir_count == -1) { 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) { 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) { 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) { if (!e.time_stamp.is_valid && !e.done_stat) {
e.done_stat = true; e.done_stat = true;
const auto path = GetNewPath(e); 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); const auto t = (time_t)(e.time_stamp.modified);
struct tm tm{}; struct tm tm{};
localtime_r(&t, &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_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(text_id), "%.2f KiB", (double)e.file_size / 1024.0); 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());
} 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);
}
} }
}); });
} }
@@ -710,20 +728,22 @@ void FsView::OnClick() {
} }
}); });
} else if (IsExtension(entry.GetExtension(), NCA_EXTENSIONS)) { } else if (IsExtension(entry.GetExtension(), NCA_EXTENSIONS)) {
MountFileFs(devoptab::MountNca, devoptab::UmountNca); MountFileFs(devoptab::MountNca, devoptab::UmountNeworkDevice);
} else if (IsExtension(entry.GetExtension(), NSP_EXTENSIONS)) { } else if (IsExtension(entry.GetExtension(), NSP_EXTENSIONS)) {
MountFileFs(devoptab::MountNsp, devoptab::UmountNsp); MountFileFs(devoptab::MountNsp, devoptab::UmountNeworkDevice);
} else if (IsExtension(entry.GetExtension(), XCI_EXTENSIONS)) { } else if (IsExtension(entry.GetExtension(), XCI_EXTENSIONS)) {
MountFileFs(devoptab::MountXci, devoptab::UmountXci); MountFileFs(devoptab::MountXci, devoptab::UmountNeworkDevice);
} else if (IsExtension(entry.GetExtension(), "zip")) { } else if (IsExtension(entry.GetExtension(), "zip")) {
MountFileFs(devoptab::MountZip, devoptab::UmountZip); MountFileFs(devoptab::MountZip, devoptab::UmountNeworkDevice);
} else if (IsExtension(entry.GetExtension(), "bfsar")) { } else if (IsExtension(entry.GetExtension(), "bfsar")) {
MountFileFs(devoptab::MountBfsar, devoptab::UmountBfsar); MountFileFs(devoptab::MountBfsar, devoptab::UmountNeworkDevice);
} else if (IsExtension(entry.GetExtension(), MUSIC_EXTENSIONS)) { } else if (IsExtension(entry.GetExtension(), MUSIC_EXTENSIONS)) {
App::Push<music::Menu>(GetFs(), GetNewPathCurrent()); App::Push<music::Menu>(GetFs(), GetNewPathCurrent());
} else if (IsExtension(entry.GetExtension(), IMAGE_EXTENSIONS)) { } else if (IsExtension(entry.GetExtension(), IMAGE_EXTENSIONS)) {
App::Push<imageview::Menu>(GetFs(), GetNewPathCurrent()); 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; std::shared_ptr<CUSBDVD> usbdvd;
if (entry.GetExtension() == "cue") { if (entry.GetExtension() == "cue") {
@@ -747,7 +767,9 @@ void FsView::OnClick() {
} else { } else {
log_write("[USBDVD] failed to mount\n"); 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(); InstallFiles();
} else if (IsSd()) { } else if (IsSd()) {
const auto assoc_list = m_menu->FindFileAssocFor(); 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); auto zfile = zipOpen2_64(zip_out, APPEND_STATUS_CREATE, nullptr, &file_func);
R_UNLESS(zfile, Result_ZipOpen2_64); R_UNLESS(zfile, Result_ZipOpen2_64);
ON_SCOPE_EXIT(zipClose(zfile, "sphaira v" APP_VERSION_HASH)); ON_SCOPE_EXIT(zipClose(zfile, "sphaira v" APP_DISPLAY_VERSION));
const auto zip_add = [&](const fs::FsPath& file_path) -> Result { const auto zip_add = [&](const fs::FsPath& file_path) -> Result {
// the file name needs to be relative to the current directory. // 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 { auto FsView::Scan(fs::FsPath new_path, bool is_walk_up) -> Result {
App::SetBoostMode(true); App::SetBoostMode(true);
ON_SCOPE_EXIT(App::SetBoostMode(false)); 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); m_previous_highlighted_file.emplace_back(f);
} }
g_change_signalled = false;
m_path = new_path; m_path = new_path;
m_entries.clear(); m_entries.clear();
m_entries_index.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 { 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 { 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; SidebarEntryArray::Items mount_items;
std::vector<FsEntry> fs_entries; 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) { for (const auto& e: FS_ENTRIES) {
fs_entries.emplace_back(e); fs_entries.emplace_back(e);
mount_items.push_back(i18n::get(e.name)); 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); mount_items.push_back(m_menu->m_custom_fs_entry.name);
} }
const auto fat_entries = location::GetFat(); const auto stdio_locations = location::GetStdio(false);
for (const auto& e: fat_entries) { for (const auto& e: stdio_locations) {
u32 flags{}; if (e.fs_hidden) {
if (e.write_protect) { continue;
flags |= FsEntryFlag_ReadOnly;
} }
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); mount_items.push_back(e.name);
} }
@@ -1808,7 +1711,8 @@ void FsView::DisplayOptions() {
std::string out; std::string out;
const auto& entry = GetEntry(); const auto& entry = GetEntry();
const auto name = entry.GetName(); 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(); App::PopToMenu();
const auto src_path = GetNewPath(entry); const auto src_path = GetNewPath(entry);
@@ -1901,7 +1805,7 @@ void FsView::DisplayOptions() {
options->Add<SidebarEntryCallback>("Extract to..."_i18n, [this](){ options->Add<SidebarEntryCallback>("Extract to..."_i18n, [this](){
std::string out; 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); UnzipFiles(out);
} }
}); });
@@ -1919,7 +1823,7 @@ void FsView::DisplayOptions() {
options->Add<SidebarEntryCallback>("Compress to..."_i18n, [this](){ options->Add<SidebarEntryCallback>("Compress to..."_i18n, [this](){
std::string out; 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); ZipFiles(out);
} }
}); });
@@ -1939,7 +1843,8 @@ void FsView::DisplayAdvancedOptions() {
if (!m_fs_entry.IsReadOnly()) { if (!m_fs_entry.IsReadOnly()) {
options->Add<SidebarEntryCallback>("Create File"_i18n, [this](){ options->Add<SidebarEntryCallback>("Create File"_i18n, [this](){
std::string out; 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(); App::PopToMenu();
fs::FsPath full_path; fs::FsPath full_path;
@@ -1961,7 +1866,8 @@ void FsView::DisplayAdvancedOptions() {
options->Add<SidebarEntryCallback>("Create Folder"_i18n, [this](){ options->Add<SidebarEntryCallback>("Create Folder"_i18n, [this](){
std::string out; 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(); App::PopToMenu();
fs::FsPath full_path; 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)) { if (m_entries_current.size() && !m_selected_count && IsExtension(GetEntry().GetExtension(), THEME_MUSIC_EXTENSIONS)) {
options->Add<SidebarEntryCallback>("Set as background music"_i18n, [this](){ options->Add<SidebarEntryCallback>("Set as background music"_i18n, [this](){
const auto rc = App::SetDefaultBackgroundMusic(GetFs(), GetNewPathCurrent()); const auto rc = App::SetDefaultBackgroundMusic(GetFs(), GetNewPathCurrent());
@@ -2020,12 +1920,6 @@ void FsView::DisplayAdvancedOptions() {
options->Add<SidebarEntryCallback>("/dev/null (Speed Test)"_i18n, [this](){ options->Add<SidebarEntryCallback>("/dev/null (Speed Test)"_i18n, [this](){
DisplayHash(hash::Type::Null); 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) { void Base::Update(Controller* controller, TouchInfo* touch) {
if (R_SUCCEEDED(waitSingle(waiterForUEvent(&g_change_uevent), 0))) { if (g_change_signalled.exchange(false)) {
if (IsSplitScreen()) { if (IsSplitScreen()) {
view_left->SortAndFindLastFile(true); view_left->SortAndFindLastFile(true);
view_right->SortAndFindLastFile(true); view_right->SortAndFindLastFile(true);
@@ -2237,8 +2132,16 @@ void Base::LoadAssocEntriesPath(const fs::FsPath& path) {
if (!assoc.path.empty()) { if (!assoc.path.empty()) {
file_exists = view->m_fs->FileExists(assoc.path); file_exists = view->m_fs->FileExists(assoc.path);
} else { } 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"; 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); const auto len = std::strlen(nro.path);
if (len < nro_name.length()) { if (len < nro_name.length()) {
continue; 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_left = std::make_unique<FsView>(this, fs, path, fs_entry, ViewSide::Left);
view = view_left.get(); view = view_left.get();
ueventCreate(&g_change_uevent, true);
} }
void MountFsHelper(const std::shared_ptr<fs::Fs>& fs, const fs::FsPath& name) { void MountFsHelper(const std::shared_ptr<fs::Fs>& fs, const fs::FsPath& name) {

View File

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

View File

@@ -10,6 +10,8 @@
#include "yati/nx/ncm.hpp" #include "yati/nx/ncm.hpp"
#include "yati/nx/es.hpp" #include "yati/nx/es.hpp"
#include "utils/utils.hpp"
#include "title_info.hpp" #include "title_info.hpp"
#include "app.hpp" #include "app.hpp"
#include "defines.hpp" #include "defines.hpp"
@@ -231,14 +233,8 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
GetNcmSizeOfMetaStatus(e); 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)); 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));
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(ThemeEntryID_TEXT_INFO), "%s", utils::formatSizeStorage(e.size).c_str());
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);
}
if (e.selected) { 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)); 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, 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); 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 ((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);
}
if (e.missing) { 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)); 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)); R_TRY(devoptab::MountNcaNcm(m_meta.cs, &e.content_id, root));
auto fs = std::make_shared<filebrowser::FsStdioWrapper>(root, [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); filebrowser::MountFsHelper(fs, utils::hexIdToStr(e.content_id).str);

View File

@@ -235,6 +235,12 @@ struct XciSource final : dump::BaseSource {
int icon{}; int icon{};
Result Read(const std::string& path, void* buf, s64 off, s64 size, u64* bytes_read) override { Result Read(const std::string& path, void* buf, s64 off, s64 size, u64* bytes_read) override {
if (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))) { if (path.ends_with(GetDumpTypeStr(DumpFileType_XCI)) || path.ends_with(GetDumpTypeStr(DumpFileType_XCZ))) {
size = ClipSize(off, size, xci_size); size = ClipSize(off, size, xci_size);
*bytes_read = size; *bytes_read = size;
@@ -323,6 +329,7 @@ private:
const s64 m_offset; 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) { Result NszExport(ProgressBox* pbox, const keys::Keys& keys, dump::BaseSource* _source, dump::WriteSource* writer, const fs::FsPath& path) {
auto source = (XciSource*)_source; auto source = (XciSource*)_source;
@@ -464,6 +471,7 @@ Result NszExport(ProgressBox* pbox, const keys::Keys& keys, dump::BaseSource* _s
R_SUCCEED(); R_SUCCEED();
} }
#endif // ENABLE_NSZ
struct GcSource final : yati::source::Base { struct GcSource final : yati::source::Base {
GcSource(const ApplicationEntry& entry, fs::FsNativeGameCard* fs); 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 Certificate"_i18n, DumpFileFlag_Cert);
add("Export Initial Data"_i18n, DumpFileFlag_Initial); add("Export Initial Data"_i18n, DumpFileFlag_Initial);
} else if (m_option_index == 2) { } else if (m_option_index == 2) {
#ifdef ENABLE_NSZ
DumpXcz(0); DumpXcz(0);
#endif // ENABLE_NSZ
} else if (m_option_index == 3) { } else if (m_option_index == 3) {
const auto rc = MountGcFs(); const auto rc = MountGcFs();
App::PushErrorBox(rc, "Failed to mount GameCard filesystem"_i18n); App::PushErrorBox(rc, "Failed to mount GameCard filesystem"_i18n);
@@ -729,6 +739,11 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
if (!m_mounted) { if (!m_mounted) {
colour = ThemeEntryID_TEXT_INFO; 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()); 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) { Result Menu::DumpXcz(u32 flags) {
R_TRY(GcMountStorage()); R_TRY(GcMountStorage());
@@ -1122,6 +1138,7 @@ Result Menu::DumpXcz(u32 flags) {
R_SUCCEED(); R_SUCCEED();
} }
#endif // ENABLE_NSZ
Result Menu::DumpGames(u32 flags) { Result Menu::DumpGames(u32 flags) {
// first, try and mount the storage. // first, try and mount the storage.
@@ -1183,18 +1200,8 @@ Result Menu::DumpGames(u32 flags) {
paths.emplace_back(BuildFullDumpPath(DumpFileType_Initial, m_entries)); 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(); R_SUCCEED();
}; };
@@ -1322,7 +1329,7 @@ Result Menu::MountGcFs() {
R_TRY(devoptab::MountXciSource(source, m_storage_trimmed_size, e.lang_entry.name, root)); R_TRY(devoptab::MountXciSource(source, m_storage_trimmed_size, e.lang_entry.name, root));
auto fs = std::make_shared<filebrowser::FsStdioWrapper>(root, [root](){ auto fs = std::make_shared<filebrowser::FsStdioWrapper>(root, [root](){
devoptab::UmountXci(root); devoptab::UmountNeworkDevice(root);
}); });
filebrowser::MountFsHelper(fs, e.lang_entry.name); filebrowser::MountFsHelper(fs, e.lang_entry.name);

View File

@@ -233,7 +233,7 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
nvgRestore(vg); nvgRestore(vg);
if (!e.tag.empty()) { 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/menus/grid_menu_base.hpp"
#include "ui/nvg_util.hpp" #include "ui/nvg_util.hpp"
#include <cmath>
namespace sphaira::ui::menu::grid { 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) { 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; const auto& [x, y, w, h] = v;
auto text_id = ThemeEntryID_TEXT; auto text_id = ThemeEntryID_TEXT;
auto info_id = ThemeEntryID_TEXT_INFO;
if (selected) { if (selected) {
text_id = ThemeEntryID_TEXT_SELECTED; text_id = info_id = ThemeEntryID_TEXT_SELECTED;
gfx::drawRectOutline(vg, theme, 4.f, v); gfx::drawRectOutline(vg, theme, 4.f, v);
} else { } else {
DrawElement(v, ThemeEntryID_GRID); 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 auto text_clip_w = w - 30.f - text_off;
const float font_size = 18; 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_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_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(text_id), version); m_scroll_version.Draw(vg, selected, text_x, y + 115, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(info_id), version);
} else { } else {
if (selected) { if (selected) {
gfx::drawAppLable(vg, theme, m_scroll_name, x, y, w, name); 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) { 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; return image_v;

View File

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

View File

@@ -16,34 +16,26 @@ enum class InstallState {
Finished, Finished,
}; };
constexpr u64 MAX_BUFFER_SIZE = 1024ULL*1024ULL*8ULL; constexpr u64 MAX_BUFFER_SIZE = 1024ULL*1024ULL*1ULL;
constexpr u64 MAX_BUFFER_RESERVE_SIZE = 1024ULL*1024ULL*32ULL;
std::atomic<InstallState> INSTALL_STATE{InstallState::None}; 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 } // namespace
Stream::Stream(const fs::FsPath& path, std::stop_token token) { Stream::Stream(const fs::FsPath& path, std::stop_token token) {
m_path = path; m_path = path;
m_token = token; m_token = token;
m_active = true; m_active = true;
m_buffer.reserve(MAX_BUFFER_RESERVE_SIZE); m_buffer.reserve(MAX_BUFFER_SIZE);
mutexInit(&m_mutex); mutexInit(&m_mutex);
condvarInit(&m_can_read); condvarInit(&m_can_read);
condvarInit(&m_can_write); 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"); log_write("[Stream::ReadChunk] inside\n");
ON_SCOPE_EXIT( ON_SCOPE_EXIT(
log_write("[Stream::ReadChunk] exiting\n"); log_write("[Stream::ReadChunk] exiting\n");
@@ -59,18 +51,30 @@ Result Stream::ReadChunk(void* buf, s64 size, u64* bytes_read) {
break; break;
} }
size = std::min<s64>(size, m_buffer.size()); const auto rsize = std::min<s64>(size, m_buffer.size());
std::memcpy(buf, m_buffer.data(), size); std::memcpy(buf, m_buffer.data(), rsize);
m_buffer.erase(m_buffer.begin(), m_buffer.begin() + size); m_buffer.erase(m_buffer.begin(), m_buffer.begin() + rsize);
*bytes_read = size; condvarWakeOne(&m_can_write);
return condvarWakeOne(&m_can_write);
size -= rsize;
buf += rsize;
*bytes_read += rsize;
if (!size) {
R_SUCCEED();
}
} }
log_write("[Stream::ReadChunk] failed to read\n"); log_write("[Stream::ReadChunk] failed to read\n");
R_THROW(Result_TransferCancelled); 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"); log_write("[Stream::Push] inside\n");
ON_SCOPE_EXIT( ON_SCOPE_EXIT(
log_write("[Stream::Push] exiting\n"); log_write("[Stream::Push] exiting\n");
@@ -83,31 +87,27 @@ bool Stream::Push(const void* buf, s64 size) {
} }
SCOPED_MUTEX(&m_mutex); SCOPED_MUTEX(&m_mutex);
#if USE_CONDI_VAR
if (m_active && m_buffer.size() >= MAX_BUFFER_SIZE) { if (m_active && m_buffer.size() >= MAX_BUFFER_SIZE) {
R_TRY(condvarWait(std::addressof(m_can_write), std::addressof(m_mutex))); 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) { if (!m_active) {
log_write("[Stream::Push] file not active\n"); log_write("[Stream::Push] file not active\n");
break; break;
} }
const auto wsize = std::min<s64>(size, MAX_BUFFER_SIZE - m_buffer.size());
const auto offset = m_buffer.size(); const auto offset = m_buffer.size();
m_buffer.resize(offset + size); m_buffer.resize(offset + wsize);
std::memcpy(m_buffer.data() + offset, buf, size);
std::memcpy(m_buffer.data() + offset, buf, wsize);
condvarWakeOne(&m_can_read); condvarWakeOne(&m_can_read);
return true;
size -= wsize;
buf += wsize;
if (!size) {
return true;
}
} }
log_write("[Stream::Push] failed to push\n"); log_write("[Stream::Push] failed to push\n");

View File

@@ -49,55 +49,57 @@ auto MiscMenuFuncGenerator(u32 flags) {
} }
const MiscMenuEntry MISC_MENU_ENTRIES[] = { 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 = { .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." }, "Internet connection required." },
{ .name = "Games", .title = "Games", .func = MiscMenuFuncGenerator<ui::menu::game::Menu>, .flag = MiscMenuFlag_Shortcut, .info = { .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." }, "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 = { .name = "FileBrowser", .title = "FileBrowser", .func = MiscMenuFuncGenerator<ui::menu::filebrowser::Menu>, .flag = MiscMenuFlag_Shortcut, .info =
"Browse files on you SD Card. "\ "Browse files on you SD Card. "
"You can move, copy, delete, extract zip, create zip, upload and much more.\n\n"\ "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." }, "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 = { .name = "Saves", .title = "Saves", .func = MiscMenuFuncGenerator<ui::menu::save::Menu>, .flag = MiscMenuFlag_Shortcut, .info =
"View save data for each user. "\ "View save data for each user. "
"You can backup and restore saves.\n\n"\ "You can backup and restore saves.\n\n"
"Experimental support for backing up system saves is possible." }, "Experimental support for backing up system saves is possible." },
#if 0 #if 0
{ .name = "Themezer", .title = "Themezer", .func = MiscMenuFuncGenerator<ui::menu::themezer::Menu>, .flag = MiscMenuFlag_Shortcut, .info = { .name = "Themezer", .title = "Themezer", .func = MiscMenuFuncGenerator<ui::menu::themezer::Menu>, .flag = MiscMenuFlag_Shortcut, .info =
"Download themes from themezer.net. "\ "Download themes from themezer.net. "
"Themes are downloaded to /themes/sphaira\n"\ "Themes are downloaded to /themes/sphaira\n"
"To install the themes, NXThemesInstaller needs to be installed (can be downloaded via the AppStore)." }, "To install the themes, NXThemesInstaller needs to be installed (can be downloaded via the AppStore)." },
#endif #endif
{ .name = "GitHub", .title = "GitHub", .func = MiscMenuFuncGenerator<ui::menu::gh::Menu>, .flag = MiscMenuFlag_Shortcut, .info = { .name = "GitHub", .title = "GitHub", .func = MiscMenuFuncGenerator<ui::menu::gh::Menu>, .flag = MiscMenuFlag_Shortcut, .info =
"Download releases directly from GitHub. "\ "Download releases directly from GitHub. "
"Custom entries can be added to /config/sphaira/github" }, "Custom entries can be added to /config/sphaira/github" },
#ifdef ENABLE_FTPSRV
{ .name = "FTP", .title = "FTP Install", .func = MiscMenuFuncGenerator<ui::menu::ftp::Menu>, .flag = MiscMenuFlag_Install, .info = { .name = "FTP", .title = "FTP Install", .func = MiscMenuFuncGenerator<ui::menu::ftp::Menu>, .flag = MiscMenuFlag_Install, .info =
"Install apps via FTP.\n\n"\ "Install apps via FTP." },
"NOTE: This feature does not always work, use at your own risk. "\ #endif // ENABLE_FTPSRV
"If you encounter an issue, do not open an issue, it will not be fixed." },
#ifdef ENABLE_LIBHAZE
{ .name = "MTP", .title = "MTP Install", .func = MiscMenuFuncGenerator<ui::menu::mtp::Menu>, .flag = MiscMenuFlag_Install, .info = { .name = "MTP", .title = "MTP Install", .func = MiscMenuFuncGenerator<ui::menu::mtp::Menu>, .flag = MiscMenuFlag_Install, .info =
"Install apps via MTP.\n\n"\ "Install apps via MTP." },
"NOTE: This feature does not always work, use at your own risk. "\ #endif // ENABLE_LIBHAZE
"If you encounter an issue, do not open an issue, it will not be fixed." },
{ .name = "USB", .title = "USB Install", .func = MiscMenuFuncGenerator<ui::menu::usb::Menu>, .flag = MiscMenuFlag_Install, .info = { .name = "USB", .title = "USB Install", .func = MiscMenuFuncGenerator<ui::menu::usb::Menu>, .flag = MiscMenuFlag_Install, .info =
"Install apps via USB.\n\n"\ "Install apps via USB.\n\n"
"A USB client is required on PC, such as ns-usbloader and fluffy.\n\n"\ "A USB client is required on PC." },
"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." },
{ .name = "GameCard", .title = "GameCard", .func = MiscMenuFuncGenerator<ui::menu::gc::Menu>, .flag = MiscMenuFlag_Shortcut, .info = { .name = "GameCard", .title = "GameCard", .func = MiscMenuFuncGenerator<ui::menu::gc::Menu>, .flag = MiscMenuFlag_Shortcut, .info =
"View info on the inserted Game Card (GC). "\ "View info on the inserted Game Card (GC). "
"You can backup and install the inserted GC. "\ "You can backup and install the inserted GC. "
"To swap GC's, simply remove the old GC and insert the new one. "\ "To swap GC's, simply remove the old GC and insert the new one. "
"You do not need to exit the menu." }, "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 = { .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(); 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(); 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) { if (e.name == name) {
name_out = name; name_out = name;
return e.func(MenuFlag_Tab); 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); 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> { auto CreateRightSideMenu(std::string_view left_name) -> std::unique_ptr<MenuBase> {
const auto name = App::GetApp()->m_right_menu.Get(); 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) { if (e.name == name) {
return e.func(MenuFlag_Tab); return e.func(MenuFlag_Tab);
} }
@@ -204,7 +232,7 @@ auto CreateRightSideMenu(std::string_view left_name) -> std::unique_ptr<MenuBase
} // namespace } // namespace
auto GetMiscMenuEntries() -> std::span<const MiscMenuEntry> { auto GetMenuMenuEntries() -> std::span<const MiscMenuEntry> {
return MISC_MENU_ENTRIES; return MISC_MENU_ENTRIES;
} }
@@ -276,9 +304,9 @@ MainMenu::MainMenu() {
this->SetActions( this->SetActions(
std::make_pair(Button::START, Action{App::Exit}), 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](){ 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))); ON_SCOPE_EXIT(App::Push(std::move(options)));
SidebarEntryArray::Items language_items; SidebarEntryArray::Items language_items;
@@ -298,9 +326,22 @@ MainMenu::MainMenu() {
language_items.push_back("Vietnamese"_i18n); language_items.push_back("Vietnamese"_i18n);
language_items.push_back("Ukrainian"_i18n); language_items.push_back("Ukrainian"_i18n);
options->Add<SidebarEntryCallback>("Theme"_i18n, [](){ // build menus info.
App::DisplayThemeOptions(); std::string menus_info = "Launch one of Sphaira's menus:\n"_i18n;
}, "Customise the look of Sphaira by changing the theme"_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](){ options->Add<SidebarEntryCallback>("Network"_i18n, [this](){
auto options = std::make_unique<Sidebar>("Network Options"_i18n, Sidebar::Side::LEFT); 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){ options->Add<SidebarEntryCallback>("FTP"_i18n, [](){ App::DisplayFtpOptions(); },
App::SetFtpEnable(enable); "Enable / modify the FTP server settings such as port, user/pass and the folders that are shown.\n\n"
}, "Enable FTP server to run in the background.\n\n"\ "NOTE: Changing any of the options will automatically restart the FTP server when exiting the options menu."_i18n
"The default port is 5000 with no user/pass set. "\ );
"You can change this behaviour in /config/ftpsrv/config.ini"_i18n);
options->Add<SidebarEntryBool>("Mtp"_i18n, App::GetMtpEnable(), [](bool& enable){ options->Add<SidebarEntryCallback>("MTP"_i18n, [](){ App::DisplayMtpOptions(); },
App::SetMtpEnable(enable); "Enable / modify the MTP responder settings such as the folders that are shown.\n\n"
}, "Enable MTP server to run in the background."_i18n); "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); App::SetNxlinkEnable(enable);
}, "Enable NXlink server to run in the background. "\ }, "Enable NXlink server to run in the background. "
"NXlink is used to send .nro's from PC to the switch\n\n"\ "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); "If you are not a developer, you can disable this option."_i18n);
options->Add<SidebarEntryBool>("Hdd"_i18n, App::GetHddEnable(), [](bool& enable){ }, "Toggle FTP, MTP, HDD and NXlink\n\n"
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" \
"If Sphaira has a update available, you can download it from this menu"_i18n); "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){ options->Add<SidebarEntryArray>("Language"_i18n, language_items, [](s64& index_out){
App::SetLanguage(index_out); App::SetLanguage(index_out);
}, (s64)App::GetLanguage(), }, (s64)App::GetLanguage(),
"Change the language.\n\n" "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); "github.com/ITotalJustice/sphaira"_i18n);
options->Add<SidebarEntryCallback>("Misc"_i18n, [](){ options->Add<SidebarEntryCallback>("Advanced Options"_i18n, [](){
App::DisplayMiscOptions();
}, "View and launch one of Sphaira's menus"_i18n);
options->Add<SidebarEntryCallback>("Advanced"_i18n, [](){
App::DisplayAdvancedOptions(); App::DisplayAdvancedOptions();
}, "Change the advanced options. "\ }, "Change the advanced options. "
"Please view the info boxes to better understand each option."_i18n); "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(); m_current_menu = m_centre_menu.get();
std::string left_side_name; 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); m_right_menu = CreateRightSideMenu(left_side_name);
AddOnLRPress(); AddOnLRPress();

View File

@@ -20,20 +20,24 @@ auto MenuBase::GetPolledData(bool force_refresh) -> PolledData {
// doesn't have focus. // doesn't have focus.
if (force_refresh || timestamp.GetSeconds() >= 1) { if (force_refresh || timestamp.GetSeconds() >= 1) {
data.tm = {}; data.tm = {};
data.battery_percetange = {};
data.charger_type = {};
data.type = {}; data.type = {};
data.status = {}; data.status = {};
data.strength = {}; data.strength = {};
data.ip = {}; 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); const auto t = std::time(NULL);
localtime_r(&t, &data.tm); localtime_r(&t, &data.tm);
psmGetBatteryChargePercentage(&data.battery_percetange);
psmGetChargerType(&data.charger_type);
nifmGetInternetConnectionStatus(&data.type, &data.strength, &data.status); nifmGetInternetConnectionStatus(&data.type, &data.strength, &data.status);
nifmGetCurrentIpAddress(&data.ip); nifmGetCurrentIpAddress(&data.ip);
App::GetSdSize(&data.sd_free, &data.sd_total);
App::GetEmmcSize(&data.emmc_free, &data.emmc_total);
timestamp.Update(); timestamp.Update();
} }
@@ -60,7 +64,7 @@ void MenuBase::Draw(NVGcontext* vg, Theme* theme) {
const auto pdata = GetPolledData(); const auto pdata = GetPolledData();
const float start_y = 70; const float start_y = 70;
const float font_size = 22; const float font_size = 20;
const float spacing = 30; const float spacing = 30;
float start_x = 1220; float start_x = 1220;
@@ -77,21 +81,59 @@ void MenuBase::Draw(NVGcontext* vg, Theme* theme) {
start_x -= spacing + (bounds[2] - bounds[0]); \ 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()) { const auto rounding = 2;
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"); const auto storage_font = 19;
} else { const auto storage_y = start_y - 30;
draw(ThemeEntryID_TEXT, 90, "%02u:%02u", pdata.tm.tm_hour, pdata.tm.tm_min); auto storage_x = start_x - STORAGE_BAR_W;
}
if (pdata.ip) { 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);
draw(ThemeEntryID_TEXT, 0, "%u.%u.%u.%u", pdata.ip&0xFF, (pdata.ip>>8)&0xFF, (pdata.ip>>16)&0xFF, (pdata.ip>>24)&0xFF); // 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);
} else { #if 0
draw(ThemeEntryID_TEXT, 0, ("No Internet"_i18n).c_str()); 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()) { if (!App::IsApplication()) {
draw(ThemeEntryID_ERROR, 0, ("[Applet Mode]"_i18n).c_str()); 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 #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()); 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_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 } // namespace sphaira::ui::menu

View File

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

View File

@@ -9,6 +9,7 @@
#include "threaded_file_transfer.hpp" #include "threaded_file_transfer.hpp"
#include "minizip_helper.hpp" #include "minizip_helper.hpp"
#include "dumper.hpp" #include "dumper.hpp"
#include "swkbd.hpp"
#include "utils/devoptab.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 u32 NX_SAVE_META_VERSION = 1;
constexpr const char* NX_SAVE_META_NAME = ".nx_save_meta.bin"; 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 { struct DumpSource final : dump::BaseSource {
DumpSource(std::span<const std::reference_wrapper<Entry>> entries, std::span<const fs::FsPath> paths) 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 } // namespace
void SignalChange() { void SignalChange() {
ueventSignal(&g_change_uevent); g_change_signalled = true;
} }
Menu::Menu(u32 flags) : grid::Menu{"Saves"_i18n, flags} { Menu::Menu(u32 flags) : grid::Menu{"Saves"_i18n, flags} {
@@ -388,7 +389,6 @@ Menu::Menu(u32 flags) : grid::Menu{"Saves"_i18n, flags} {
} }
title::Init(); title::Init();
ueventCreate(&g_change_uevent, true);
} }
Menu::~Menu() { Menu::~Menu() {
@@ -399,7 +399,7 @@ Menu::~Menu() {
} }
void Menu::Update(Controller* controller, TouchInfo* touch) { 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; m_dirty = true;
} }
@@ -508,9 +508,9 @@ void Menu::ScanHomebrew() {
constexpr auto ENTRY_CHUNK_COUNT = 1000; constexpr auto ENTRY_CHUNK_COUNT = 1000;
TimeStamp ts; TimeStamp ts;
g_change_signalled = false;
FreeEntries(); FreeEntries();
ClearSelection(); ClearSelection();
ueventClear(&g_change_uevent);
m_entries.reserve(ENTRY_CHUNK_COUNT); m_entries.reserve(ENTRY_CHUNK_COUNT);
m_is_reversed = false; m_is_reversed = false;
m_dirty = false; m_dirty = false;
@@ -690,13 +690,38 @@ void Menu::DisplayOptions() {
entries.emplace_back(m_entries[m_index]); entries.emplace_back(m_entries[m_index]);
} }
BackupSaves(entries); BackupSaves(entries, BackupFlag_None);
}, true); }, 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) { 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](){ options->Add<SidebarEntryCallback>("Restore"_i18n, [this](){
RestoreSave(); 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){ 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); 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){ options->Add<SidebarEntryBool>("Compress backup"_i18n, m_compress_save_backup.Get(), [this](bool& v_out){
m_compress_save_backup.Set(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) { 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](const dump::DumpLocation& location){ 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](auto pbox) -> Result { App::Push<ProgressBox>(0, "Backup"_i18n, "", [this, entries, location, flags](auto pbox) -> Result {
return BackupSaveInternal(pbox, location, entries, m_compress_save_backup.Get()); return BackupSaveInternal(pbox, location, entries, flags);
}, [](Result rc){ }, [](Result rc){
App::PushErrorBox(rc, "Backup failed!"_i18n); App::PushErrorBox(rc, "Backup failed!"_i18n);
@@ -730,17 +758,23 @@ void Menu::BackupSaves(std::vector<std::reference_wrapper<Entry>>& entries) {
void Menu::RestoreSave() { void Menu::RestoreSave() {
dump::DumpGetLocation("Select restore location"_i18n, dump::DumpLocationFlag_SdCard|dump::DumpLocationFlag_Stdio, [this](const dump::DumpLocation& location){ 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) { 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); fs = std::make_unique<fs::FsStdio>(true, location.stdio[location.entry.index].mount);
} else if (location.entry.type == dump::DumpLocationType_SdCard) { } else if (location.entry.type == dump::DumpLocationType_SdCard) {
fs = std::make_unique<fs::FsNativeSd>(); 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 // get saves in /Saves/Name and /Saves/app_id
filebrowser::FsDirCollection collections[2]{}; filebrowser::FsDirCollection collections[2]{};
for (auto i = 0; i < std::size(collections); i++) { 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); filebrowser::FsView::get_collection(fs.get(), save_path, "", collections[i], true, false, false);
// reverse as they will be sorted in oldest -> newest. // reverse as they will be sorted in oldest -> newest.
// todo: better impl when both id and normal app folders are used. // todo: better impl when both id and normal app folders are used.
@@ -763,7 +797,7 @@ void Menu::RestoreSave() {
if (paths.empty()) { if (paths.empty()) {
App::Push<ui::OptionBox>( 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 "OK"_i18n
); );
return; return;
@@ -789,7 +823,7 @@ void Menu::RestoreSave() {
if (m_auto_backup_on_restore.Get()) { if (m_auto_backup_on_restore.Get()) {
pbox->SetActionName("Auto backup"_i18n); 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); 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 t = std::time(NULL);
const auto tm = std::localtime(&t); const auto tm = std::localtime(&t);
const auto base = BuildSaveBasePath(e); const auto base = BuildSaveBasePath(e);
@@ -817,27 +851,39 @@ auto Menu::BuildSavePath(const Entry& e, bool is_auto) const -> fs::FsPath {
char time[64]; 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); 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) { if (e.save_data_type == FsSaveDataType_Account) {
const auto acc = m_accounts[m_account_index]; const auto acc = m_accounts[m_account_index];
fs::FsPath name_buf; fs::FsPath name_buf;
if (is_auto) { if (flags & BackupFlag_IsAuto) {
std::snprintf(name_buf, sizeof(name_buf), "AUTO - %s", acc.nickname); std::snprintf(name_buf, sizeof(name_buf), "AUTO - %s", acc.nickname);
} else { } else {
std::snprintf(name_buf, sizeof(name_buf), "%s", acc.nickname); std::snprintf(name_buf, sizeof(name_buf), "%s", acc.nickname);
} }
title::utilsReplaceIllegalCharacters(name_buf, true); 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 { } 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()); pbox->SetTitle(e.GetName());
if (e.image) { if (e.image) {
pbox->SetImage(e.image); pbox->SetImage(e.image);
@@ -955,14 +1001,15 @@ Result Menu::RestoreSaveInternal(ProgressBox* pbox, const Entry& e, const fs::Fs
R_SUCCEED(); 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; std::vector<fs::FsPath> paths;
for (auto& e : entries) { for (auto& e : entries) {
// ensure that we have title name and icon loaded. // ensure that we have title name and icon loaded.
LoadControlEntry(e); 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); 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 { 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); auto zfile = zipOpen2_64(path, APPEND_STATUS_CREATE, nullptr, &file_func);
R_UNLESS(zfile, Result_ZipOpen2_64); R_UNLESS(zfile, Result_ZipOpen2_64);
ON_SCOPE_EXIT(zipClose(zfile, "sphaira v" APP_VERSION_HASH)); ON_SCOPE_EXIT(zipClose(zfile, "sphaira v" APP_DISPLAY_VERSION));
// add save meta. // 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; std::vector<std::reference_wrapper<Entry>> entries;
entries.emplace_back(e); entries.emplace_back(e);
return BackupSaveInternal(pbox, location, entries, compressed, is_auto); return BackupSaveInternal(pbox, location, entries, flags);
} }
Result Menu::MountSaveFs() { Result Menu::MountSaveFs() {
@@ -1125,8 +1172,8 @@ Result Menu::MountSaveFs() {
fs::FsPath root; fs::FsPath root;
R_TRY(devoptab::MountSaveSystem(e.system_save_data_id, root)); R_TRY(devoptab::MountSaveSystem(e.system_save_data_id, root));
auto fs = std::make_shared<filebrowser::FsStdioWrapper>(root, [&e](){ auto fs = std::make_shared<filebrowser::FsStdioWrapper>(root, [root](){
devoptab::UnmountSave(e.system_save_data_id); devoptab::UmountNeworkDevice(root);
}); });
filebrowser::MountFsHelper(fs, e.GetName()); filebrowser::MountFsHelper(fs, e.GetName());

View File

@@ -5,7 +5,6 @@
#include "log.hpp" #include "log.hpp"
#include "ui/nvg_util.hpp" #include "ui/nvg_util.hpp"
#include "i18n.hpp" #include "i18n.hpp"
#include "haze_helper.hpp"
#include "utils/thread.hpp" #include "utils/thread.hpp"
@@ -35,10 +34,10 @@ Menu::Menu(u32 flags) : MenuBase{"USB"_i18n, flags} {
}}); }});
// if mtp is enabled, disable it for now. // if mtp is enabled, disable it for now.
m_was_mtp_enabled = haze::IsInit(); m_was_mtp_enabled = App::GetMtpEnable();
if (m_was_mtp_enabled) { if (m_was_mtp_enabled) {
App::Notify("Disable MTP for usb install"_i18n); App::Notify("Disable MTP for usb install"_i18n);
haze::Exit(); App::SetMtpEnable(false);
} }
// 3 second timeout for transfers. // 3 second timeout for transfers.
@@ -70,7 +69,7 @@ Menu::~Menu() {
if (m_was_mtp_enabled) { if (m_was_mtp_enabled) {
App::Notify("Re-enabled MTP"_i18n); App::Notify("Re-enabled MTP"_i18n);
haze::Init(); App::SetMtpEnable(true);
} }
} }
@@ -97,6 +96,9 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
m_state = State::Progress; m_state = State::Progress;
log_write("got connection\n"); log_write("got connection\n");
App::Push<ui::ProgressBox>(0, "Installing "_i18n, "", [this](auto pbox) -> Result { App::Push<ui::ProgressBox>(0, "Installing "_i18n, "", [this](auto pbox) -> Result {
pbox->AddCancelEvent(m_usb_source->GetCancelEvent());
ON_SCOPE_EXIT(pbox->RemoveCancelEvent(m_usb_source->GetCancelEvent()));
log_write("inside progress box\n"); log_write("inside progress box\n");
for (u32 i = 0; i < std::size(m_names); i++) { for (u32 i = 0; i < std::size(m_names); i++) {
const auto& file_name = 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); 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 #define HIGHLIGHT_SPEED 350.0
static double highlightGradientX = 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::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_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::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) { m_list->Draw(vg, theme, m_items.size(), [this](auto* vg, auto* theme, auto& v, auto i) {
const auto& [x, y, w, h] = v; const auto& [x, y, w, h] = v;

View File

@@ -7,6 +7,7 @@
#include "threaded_file_transfer.hpp" #include "threaded_file_transfer.hpp"
#include "i18n.hpp" #include "i18n.hpp"
#include "utils/utils.hpp"
#include "utils/thread.hpp" #include "utils/thread.hpp"
#include <cstring> #include <cstring>
@@ -21,36 +22,6 @@ void threadFunc(void* arg) {
d->pbox->RequestExit(); d->pbox->RequestExit();
} }
// https://github.com/memononen/nanovg/blob/f93799c078fa11ed61c078c65a53914c8782c00b/example/demo.c#L500
void drawSpinner(NVGcontext* vg, Theme* theme, float cx, float cy, float r, float t)
{
float a0 = 0.0f + t*6;
float a1 = NVG_PI + t*6;
float r0 = r;
float r1 = r * 0.75f;
float ax,ay, bx,by;
NVGpaint paint;
nvgSave(vg);
auto colourb = theme->GetColour(ThemeEntryID_PROGRESSBAR);
colourb.a = 0.5;
nvgBeginPath(vg);
nvgArc(vg, cx,cy, r0, a0, a1, NVG_CW);
nvgArc(vg, cx,cy, r1, a1, a0, NVG_CCW);
nvgClosePath(vg);
ax = cx + cosf(a0) * (r0+r1)*0.5f;
ay = cy + sinf(a0) * (r0+r1)*0.5f;
bx = cx + cosf(a1) * (r0+r1)*0.5f;
by = cy + sinf(a1) * (r0+r1)*0.5f;
paint = nvgLinearGradient(vg, ax,ay, bx,by, nvgRGBA(0,0,0,0), colourb);
nvgFillPaint(vg, paint);
nvgFill(vg);
nvgRestore(vg);
}
} // namespace } // namespace
ProgressBox::ProgressBox(int image, const std::string& action, const std::string& title, const ProgressBoxCallback& callback, const ProgressBoxDoneCallback& done) 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); 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; const auto rad = 15;
drawSpinner(vg, theme, prog_bar.x - pad - rad, prog_bar.y + prog_bar.h / 2, rad, armTicksToNs(armGetSystemTick()) / 1e+9); gfx::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);
}
const auto left = size - last_offset; const auto left = size - last_offset;
const auto left_seconds = left / speed; 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); 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()); 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& { auto ProgressBox::SetActionName(const std::string& action) -> ProgressBox& {
mutexLock(&m_mutex); SCOPED_MUTEX(&m_mutex);
m_action = action; m_action = action;
mutexUnlock(&m_mutex);
Yield();
return *this; return *this;
} }
auto ProgressBox::SetTitle(const std::string& title) -> ProgressBox& { auto ProgressBox::SetTitle(const std::string& title) -> ProgressBox& {
mutexLock(&m_mutex); SCOPED_MUTEX(&m_mutex);
m_title = title; m_title = title;
mutexUnlock(&m_mutex);
Yield();
return *this; return *this;
} }
auto ProgressBox::NewTransfer(const std::string& transfer) -> ProgressBox& { auto ProgressBox::NewTransfer(const std::string& transfer) -> ProgressBox& {
mutexLock(&m_mutex); SCOPED_MUTEX(&m_mutex);
m_transfer = transfer; m_transfer = transfer;
m_size = 0; m_size = 0;
m_offset = 0; m_offset = 0;
m_last_offset = 0; m_last_offset = 0;
m_timestamp.Update(); m_timestamp.Update();
mutexUnlock(&m_mutex);
Yield();
return *this; return *this;
} }
auto ProgressBox::ResetTranfser() -> ProgressBox& { auto ProgressBox::ResetTranfser() -> ProgressBox& {
mutexLock(&m_mutex); SCOPED_MUTEX(&m_mutex);
m_size = 0; m_size = 0;
m_offset = 0; m_offset = 0;
m_last_offset = 0; m_last_offset = 0;
m_timestamp.Update(); m_timestamp.Update();
mutexUnlock(&m_mutex);
Yield();
return *this; return *this;
} }
auto ProgressBox::UpdateTransfer(s64 offset, s64 size) -> ProgressBox& { auto ProgressBox::UpdateTransfer(s64 offset, s64 size) -> ProgressBox& {
mutexLock(&m_mutex); SCOPED_MUTEX(&m_mutex);
m_size = size; m_size = size;
m_offset = offset; m_offset = offset;
mutexUnlock(&m_mutex);
Yield();
return *this; return *this;
} }
auto ProgressBox::SetImage(int image) -> ProgressBox& { auto ProgressBox::SetImage(int image) -> ProgressBox& {
mutexLock(&m_mutex); SCOPED_MUTEX(&m_mutex);
m_image_pending = image; m_image_pending = image;
m_is_image_pending = true; m_is_image_pending = true;
mutexUnlock(&m_mutex);
return *this; return *this;
} }
auto ProgressBox::SetImageData(std::vector<u8>& data) -> ProgressBox& { auto ProgressBox::SetImageData(std::vector<u8>& data) -> ProgressBox& {
mutexLock(&m_mutex); SCOPED_MUTEX(&m_mutex);
std::swap(m_image_data, data); std::swap(m_image_data, data);
mutexUnlock(&m_mutex);
return *this; return *this;
} }
auto ProgressBox::SetImageDataConst(std::span<const u8> data) -> ProgressBox& { auto ProgressBox::SetImageDataConst(std::span<const u8> data) -> ProgressBox& {
mutexLock(&m_mutex); SCOPED_MUTEX(&m_mutex);
m_image_data.resize(data.size()); m_image_data.resize(data.size());
std::memcpy(m_image_data.data(), data.data(), m_image_data.size()); std::memcpy(m_image_data.data(), data.data(), m_image_data.size());
mutexUnlock(&m_mutex);
return *this; return *this;
} }
void ProgressBox::RequestExit() { void ProgressBox::RequestExit() {
SCOPED_MUTEX(&m_mutex);
m_stop_source.request_stop(); m_stop_source.request_stop();
ueventSignal(GetCancelEvent()); ueventSignal(GetCancelEvent());
// cancel any registered events.
for (auto& e : m_cancel_events) {
ueventSignal(e);
}
} }
auto ProgressBox::ShouldExit() -> bool { auto ProgressBox::ShouldExit() -> bool {
@@ -318,6 +272,26 @@ auto ProgressBox::ShouldExitResult() -> Result {
R_SUCCEED(); 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 { 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_file_based_emummc = App::IsFileBaseEmummc();
const auto is_both_native = fs_src->IsNative() && fs_dst->IsNative(); 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 { } else {
m_option ^= 1; m_option ^= 1;
m_callback(m_option); 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::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} { : SidebarEntryBool{title, option, Callback{}, info, true_str, false_str} {
m_callback = [&option](bool&){ m_callback = [this, &option](bool&){
option ^= 1; 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::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} { : 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) { if (cb) {
cb(v_out); cb(v_out);
} }
option.Set(v_out); option.Set(v_out);
SetDirty();
}; };
} }
@@ -166,6 +169,7 @@ SidebarEntrySlider::SidebarEntrySlider(const std::string& title, float value, fl
DependsClick(); DependsClick();
} else { } else {
m_value = std::clamp(m_value - m_inc, m_min, m_max); m_value = std::clamp(m_value - m_inc, m_min, m_max);
SetDirty();
// m_callback(m_option); // m_callback(m_option);
} } } }
}); });
@@ -174,6 +178,7 @@ SidebarEntrySlider::SidebarEntrySlider(const std::string& title, float value, fl
DependsClick(); DependsClick();
} else { } else {
m_value = std::clamp(m_value + m_inc, m_min, m_max); m_value = std::clamp(m_value + m_inc, m_min, m_max);
SetDirty();
// m_callback(m_option); // m_callback(m_option);
} } } }
}); });
@@ -240,6 +245,8 @@ SidebarEntryArray::SidebarEntryArray(const std::string& title, const Items& item
App::Push<PopupList>( App::Push<PopupList>(
m_title, m_items, index, m_index m_title, m_items, index, m_index
); );
SetDirty();
}; };
} }
@@ -275,6 +282,7 @@ SidebarEntryArray::SidebarEntryArray(const std::string& title, const Items& item
} else { } else {
// m_callback(m_index); // m_callback(m_index);
m_list_callback(); m_list_callback();
SetDirty();
}} }}
}); });
} }
@@ -291,6 +299,7 @@ SidebarEntryTextBase::SidebarEntryTextBase(const std::string& title, const std::
SetAction(Button::A, Action{"OK"_i18n, [this](){ SetAction(Button::A, Action{"OK"_i18n, [this](){
if (m_callback) { if (m_callback) {
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); 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} : 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_min{len_min}
, m_len_max{len_max} { , m_len_max{len_max}
, m_callback{callback} {
SetCallback([this](){ SetCallback([this](){
std::string out; 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); 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>( App::Push<menu::filebrowser::picker::Menu>(
[this](const fs::FsPath& path) { [this](const fs::FsPath& path) {
SetValue(path); SetValue(path);
SetDirty();
return true; return true;
}, },
m_filter 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::Sidebar(const std::string& title, Side side, float width)
: Sidebar{title, "", side, std::forward<decltype(items)>(items)} { : Sidebar{title, "", side, width} {
} }
Sidebar::Sidebar(const std::string& title, Side side) Sidebar::Sidebar(const std::string& title, const std::string& sub, Side side, float width)
: Sidebar{title, "", side, {}} {
}
Sidebar::Sidebar(const std::string& title, const std::string& sub, Side side, Items&& items)
: m_title{title} : m_title{title}
, m_sub{sub} , m_sub{sub}
, m_side{side} , m_side{side} {
, m_items{std::forward<decltype(items)>(items)} {
switch (m_side) { switch (m_side) {
case Side::LEFT: case Side::LEFT:
SetPos(Vec4{0.f, 0.f, 450.f, SCREEN_HEIGHT}); SetPos(Vec4{0.f, 0.f, width, SCREEN_HEIGHT});
break; break;
case Side::RIGHT: 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; 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); 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::~Sidebar() {
: Sidebar{title, sub, side, {}} { 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 { auto Sidebar::Update(Controller* controller, TouchInfo* touch) -> void {
Widget::Update(controller, touch); 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_top_bar, theme->GetColour(ThemeEntryID_LINE));
gfx::drawRect(vg, m_bottom_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); Widget::Draw(vg, theme);

View File

@@ -36,7 +36,7 @@ Base::Base(u64 transfer_timeout) {
App::SetAutoSleepDisabled(true); App::SetAutoSleepDisabled(true);
m_transfer_timeout = transfer_timeout; 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]); 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() { Usb::~Usb() {
if (m_was_connected && R_SUCCEEDED(m_usb->IsUsbConnected(0))) { if (m_was_connected && R_SUCCEEDED(m_usb->IsUsbConnected(0))) {
const auto send_header = SendPacket::Build(CMD_QUIT); 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() { Usb::~Usb() {
if (m_was_connected && R_SUCCEEDED(m_usb->IsUsbConnected(0))) { if (m_was_connected && R_SUCCEEDED(m_usb->IsUsbConnected(0))) {
const auto send_header = SendPacket::Build(CMD_QUIT); 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.mount = fs.mountpoint;
out.name = display_name; out.name = display_name;
out.write_protect = true; out.flags = location::FsEntryFlag::FsEntryFlag_ReadOnly;
return true; return true;
} }

View File

@@ -21,52 +21,55 @@
#pragma GCC diagnostic push #pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wcast-qual" #pragma GCC diagnostic ignored "-Wcast-qual"
#pragma GCC diagnostic ignored "-Wunused-function" #pragma GCC diagnostic ignored "-Wunused-function"
#if 0 #pragma GCC diagnostic ignored "-Wunused-variable"
#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 "-Walloca" #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 #ifdef ENABLE_AUDIO_FLAC
#pragma GCC diagnostic ignored "-Wunused-variable" #define DR_FLAC_IMPLEMENTATION
#include <id3v2lib.h> #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 #pragma GCC diagnostic pop
#include <pulsar.h> #include <pulsar.h>
@@ -130,6 +133,7 @@ private:
s64 m_size{}; s64 m_size{};
}; };
#ifdef ENABLE_AUDIO_MP3
// gta vice "encrypted" mp3's using xor 0x22, very cool. // gta vice "encrypted" mp3's using xor 0x22, very cool.
struct GTAViceCityFile final : File { struct GTAViceCityFile final : File {
size_t ReadFile(void* _buf, size_t read_size) override { 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; buf[sz] = 0;
return buf; return buf;
} }
#endif // ENABLE_AUDIO_MP3
struct Base { struct Base {
virtual ~Base() = default; virtual ~Base() = default;
@@ -458,6 +463,7 @@ struct PlsrBFWAV final : PlsrBase {
} }
}; };
#ifdef ENABLE_AUDIO_WAV
struct DrWAV final : CustomBase { struct DrWAV final : CustomBase {
~DrWAV() { ~DrWAV() {
drwav_uninit(&m_wav); drwav_uninit(&m_wav);
@@ -532,7 +538,9 @@ private:
drwav m_wav{}; drwav m_wav{};
File m_file{}; File m_file{};
}; };
#endif // ENABLE_AUDIO_WAV
#ifdef ENABLE_AUDIO_MP3
struct DrMP3 final : CustomBase { struct DrMP3 final : CustomBase {
DrMP3(std::unique_ptr<File>&& file = std::make_unique<File>()) : m_file{std::forward<decltype(file)>(file)} { 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{}; std::vector<drmp3_seek_point> m_seek_points{};
#endif #endif
}; };
#endif // ENABLE_AUDIO_MP3
#if 0 #ifdef ENABLE_AUDIO_FLAC
struct DrFLAC final : CustomBase { struct DrFLAC final : CustomBase {
~DrFLAC() { ~DrFLAC() {
drflac_close(m_flac); drflac_close(m_flac);
@@ -718,8 +727,9 @@ private:
drflac* m_flac{}; drflac* m_flac{};
File m_file{}; File m_file{};
}; };
#endif #endif // ENABLE_AUDIO_FLAC
#ifdef ENABLE_AUDIO_OGG
// api is not ready, leaving this here for when it is. // api is not ready, leaving this here for when it is.
#if 0 #if 0
struct DrOGG final : CustomBase { struct DrOGG final : CustomBase {
@@ -864,6 +874,7 @@ private:
stb_vorbis* m_ogg{}; stb_vorbis* m_ogg{};
File m_file{}; File m_file{};
}; };
#endif // ENABLE_AUDIO_OGG
constexpr u32 MAX_SONGS = 4; constexpr u32 MAX_SONGS = 4;
@@ -886,11 +897,12 @@ struct SongEntry {
} }
}; };
Mutex g_mutex; Mutex g_mutex{};
SongEntry g_songs[MAX_SONGS]; SongEntry g_songs[MAX_SONGS]{};
PLSR_PlayerSoundId g_sound_ids[std::to_underlying(SoundEffect::MAX)]{}; PLSR_PlayerSoundId g_sound_ids[std::to_underlying(SoundEffect::MAX)]{};
Thread g_thread{}; Thread g_thread{};
UEvent g_cancel_uevent{}; UEvent g_cancel_uevent{};
std::atomic_bool g_is_init{};
void thread_func(void* arg) { void thread_func(void* arg) {
auto player = plsrPlayerGetInstance(); auto player = plsrPlayerGetInstance();
@@ -950,6 +962,10 @@ void thread_func(void* arg) {
} // namespace } // namespace
Result Init() { Result Init() {
if (g_is_init) {
R_SUCCEED();
}
SCOPED_MUTEX(&g_mutex); SCOPED_MUTEX(&g_mutex);
R_TRY(plsrPlayerInit()); R_TRY(plsrPlayerInit());
@@ -996,14 +1012,21 @@ Result Init() {
R_TRY(utils::CreateThread(&g_thread, thread_func, nullptr, 1024*128, 0x20)); R_TRY(utils::CreateThread(&g_thread, thread_func, nullptr, 1024*128, 0x20));
R_TRY(threadStart(&g_thread)); R_TRY(threadStart(&g_thread));
g_is_init = true;
R_SUCCEED(); R_SUCCEED();
} }
void ExitSignal() { void ExitSignal() {
ueventSignal(&g_cancel_uevent); if (g_is_init) {
ueventSignal(&g_cancel_uevent);
}
} }
void Exit() { void Exit() {
if (!g_is_init) {
return;
}
ExitSignal(); ExitSignal();
threadWaitForExit(&g_thread); threadWaitForExit(&g_thread);
threadClose(&g_thread); threadClose(&g_thread);
@@ -1025,9 +1048,12 @@ void Exit() {
std::memset(g_songs, 0, sizeof(g_songs)); std::memset(g_songs, 0, sizeof(g_songs));
std::memset(g_sound_ids, 0, sizeof(g_sound_ids)); std::memset(g_sound_ids, 0, sizeof(g_sound_ids));
g_is_init = false;
} }
Result PlaySoundEffect(SoundEffect effect) { Result PlaySoundEffect(SoundEffect effect) {
R_UNLESS(g_is_init, 0x1);
SCOPED_MUTEX(&g_mutex); SCOPED_MUTEX(&g_mutex);
const auto id = g_sound_ids[std::to_underlying(effect)]; 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) { Result OpenSong(fs::Fs* fs, const fs::FsPath& path, u32 flags, SongID* id) {
R_UNLESS(g_is_init, 0x1);
SCOPED_MUTEX(&g_mutex); SCOPED_MUTEX(&g_mutex);
R_UNLESS(fs && id && !path.empty(), 0x1); 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")) { else if (path.ends_with(".bfwav")) {
source = std::make_unique<PlsrBFWAV>(); source = std::make_unique<PlsrBFWAV>();
} }
#ifdef ENABLE_AUDIO_WAV
else if (path.ends_with(".wav")) { else if (path.ends_with(".wav")) {
source = std::make_unique<DrWAV>(); 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")) { else if (path.ends_with(".mp3") || path.ends_with(".mp2") || path.ends_with(".mp1")) {
source = std::make_unique<DrMP3>(); source = std::make_unique<DrMP3>();
} }
else if (path.ends_with(".adf")) { else if (path.ends_with(".adf")) {
source = std::make_unique<DrMP3>(std::make_unique<GTAViceCityFile>()); source = std::make_unique<DrMP3>(std::make_unique<GTAViceCityFile>());
} }
// else if (path.ends_with(".flac")) { #endif // ENABLE_AUDIO_MP3
// source = std::make_unique<DrFLAC>(); #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")) { // else if (path.ends_with(".ogg")) {
// source = std::make_unique<DrOGG>(); // source = std::make_unique<DrOGG>();
// } // }
else if (path.ends_with(".ogg")) { else if (path.ends_with(".ogg")) {
source = std::make_unique<stbOGG>(); source = std::make_unique<stbOGG>();
} }
#endif // ENABLE_AUDIO_OGG
R_UNLESS(source, 0x1); R_UNLESS(source, 0x1);
R_TRY(source->LoadFile(fs, path, flags)); 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) { Result CloseSong(SongID* id) {
R_UNLESS(g_is_init, 0x1);
R_UNLESS(id && *id, 0x1); R_UNLESS(id && *id, 0x1);
auto e = static_cast<SongEntry*>(*id); auto e = static_cast<SongEntry*>(*id);
@@ -1104,6 +1142,8 @@ Result CloseSong(SongID* id) {
} }
#define LockSongAndDo(cond_func, ...) do { \ #define LockSongAndDo(cond_func, ...) do { \
R_UNLESS(g_is_init, 0x1); \
\
R_UNLESS(id, 0x1); \ R_UNLESS(id, 0x1); \
auto e = static_cast<SongEntry*>(id); \ 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 "defines.hpp"
#include "log.hpp" #include "log.hpp"
#include "yati/container/nsp.hpp"
#include "yati/container/xci.hpp"
#include "yati/source/file.hpp" #include "yati/source/file.hpp"
#include <pulsar.h> #include <pulsar.h>
@@ -15,24 +13,16 @@
#include <array> #include <array>
#include <memory> #include <memory>
#include <algorithm> #include <algorithm>
#include <sys/iosupport.h>
namespace sphaira::devoptab { namespace sphaira::devoptab {
namespace { namespace {
struct Device {
PLSR_BFSAR bfsar;
std::FILE* file; // points to archive file.
};
struct File { struct File {
Device* device;
PLSR_BFWARFileInfo info; PLSR_BFWARFileInfo info;
size_t off; size_t off;
}; };
struct Dir { struct Dir {
Device* device;
u32 index; 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) { struct Device final : common::MountDevice {
r->_errno = err; Device(const PLSR_BFSAR& _bfsar, const common::MountConfig& _config)
return -1; : MountDevice{_config}
} , bfsar{_bfsar} {
this->file = this->bfsar.ar.handle->f;
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);
} }
~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; PLSR_BFWARFileInfo info;
if (R_FAILED(GetFileInfo(&device->bfsar, path, info))) { if (R_FAILED(GetFileInfo(&this->bfsar, path, info))) {
return set_errno(r, ENOENT); return -ENOENT;
} }
file->device = device;
file->info = info; 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); auto file = static_cast<File*>(fd);
std::memset(file, 0, sizeof(*file)); 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); auto file = static_cast<File*>(fd);
const auto& info = file->info; const auto& info = file->info;
// const auto real_len = len;
// plsr seems to read oob, so allow for some tollerance. // plsr seems to read oob, so allow for some tollerance.
const auto oob_allowed = 64; const auto oob_allowed = 64;
len = std::min(len, info.size + oob_allowed - file->off); len = std::min(len, info.size + oob_allowed - file->off);
std::fseek(file->device->file, file->info.offset + file->off, SEEK_SET); std::fseek(this->file, file->info.offset + file->off, SEEK_SET);
const auto bytes_read = std::fread(ptr, 1, len, file->device->file); const auto bytes_read = std::fread(ptr, 1, len, this->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);
file->off += bytes_read; file->off += bytes_read;
return 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); auto file = static_cast<File*>(fd);
const auto& info = file->info; 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; pos = info.size;
} }
r->_errno = 0;
return file->off = std::clamp<u64>(pos, 0, info.size); 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); auto file = static_cast<File*>(fd);
const auto& info = file->info; 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_nlink = 1;
st->st_size = info.size; st->st_size = info.size;
st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH; 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) { int Device::devoptab_diropen(void* fd, 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;
}
if (!std::strcmp(path, "/")) { if (!std::strcmp(path, "/")) {
dir->device = device; return 0;
} else {
set_errno(r, ENOENT);
return NULL;
} }
r->_errno = 0; return -ENOENT;
return dirState;
} }
int devoptab_dirreset(struct _reent *r, DIR_ITER *dirState) { int Device::devoptab_dirreset(void* fd) {
auto dir = static_cast<Dir*>(dirState->dirStruct); auto dir = static_cast<Dir*>(fd);
dir->index = 0; dir->index = 0;
return r->_errno = 0; return 0;
} }
int devoptab_dirnext(struct _reent *r, DIR_ITER *dirState, char *filename, struct stat *filestat) { int Device::devoptab_dirnext(void* fd, char *filename, struct stat *filestat) {
auto dir = static_cast<Dir*>(dirState->dirStruct); auto dir = static_cast<Dir*>(fd);
std::memset(filestat, 0, sizeof(*filestat));
do { do {
if (dir->index >= plsrBFSARSoundCount(&dir->device->bfsar)) { if (dir->index >= plsrBFSARSoundCount(&this->bfsar)) {
log_write("finished getting call entries: %u vs %u\n", dir->index, plsrBFSARSoundCount(&dir->device->bfsar)); log_write("finished getting call entries: %u vs %u\n", dir->index, plsrBFSARSoundCount(&this->bfsar));
return set_errno(r, ENOENT); return -ENOENT;
} }
PLSR_BFSARSoundInfo info{}; PLSR_BFSARSoundInfo info{};
if (R_FAILED(plsrBFSARSoundGet(&dir->device->bfsar, dir->index, &info))) { if (R_FAILED(plsrBFSARSoundGet(&this->bfsar, dir->index, &info))) {
continue; continue;
} }
@@ -206,7 +193,7 @@ int devoptab_dirnext(struct _reent *r, DIR_ITER *dirState, char *filename, struc
continue; 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; 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; filestat->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
dir->index++; dir->index++;
break; break;
} while (dir->index++); } while (++dir->index);
return r->_errno = 0; return 0;
} }
int devoptab_dirclose(struct _reent *r, DIR_ITER *dirState) { int Device::devoptab_dirclose(void* fd) {
auto dir = static_cast<Dir*>(dirState->dirStruct); auto dir = static_cast<Dir*>(fd);
std::memset(dir, 0, sizeof(*dir)); std::memset(dir, 0, sizeof(*dir));
log_write("[BFSAR] devoptab_dirclose\n"); return 0;
return r->_errno = 0;
} }
int devoptab_lstat(struct _reent *r, const char *_path, struct stat *st) { int Device::devoptab_lstat(const char *path, struct stat *st) {
auto device = (Device*)r->deviceData; 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));
if (!std::strcmp(path, "/")) { if (!std::strcmp(path, "/")) {
st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH; st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
} else { } else {
PLSR_BFWARFileInfo info{}; PLSR_BFWARFileInfo info{};
if (R_FAILED(GetFileInfo(&device->bfsar, path, info))) { if (R_FAILED(GetFileInfo(&this->bfsar, path, info))) {
return set_errno(r, ENOENT); return -ENOENT;
} }
st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH; st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
st->st_size = info.size; st->st_size = info.size;
} }
st->st_nlink = 1; return 0;
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() {
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 } // namespace
Result MountBfsar(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path) { 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. if (!common::MountReadOnlyIndexDevice(
for (auto& e : g_entries) { [&bfsar](const common::MountConfig& config) {
if (e && e->path == path) { return std::make_unique<Device>(bfsar, config);
e->ref_count++; },
out_path = e->mount; sizeof(File), sizeof(Dir),
R_SUCCEED(); "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(); 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 } // 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 "utils/devoptab_common.hpp"
#include "fatfs.hpp" #include "utils/profile.hpp"
#include "defines.hpp"
#include "log.hpp" #include "log.hpp"
#include "ff.h" #include "defines.hpp"
#include <array> #include <fcntl.h>
#include <algorithm>
#include <span> #include <string>
#include <vector>
#include <memory> #include <memory>
#include <cstring> #include <cstring>
#include <cstdio> #include <sys/stat.h>
#include <cerrno> #include <ff.h>
#include <sys/iosupport.h>
namespace sphaira::fatfs { namespace sphaira::devoptab {
namespace { namespace {
auto is_archive(BYTE attr) -> bool { enum BisMountType {
const auto archive_attr = AM_DIR | AM_ARC; BisMountType_PRODINFOF,
return (attr & archive_attr) == archive_attr; 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 // todo: replace with off+size and have the data be in another struct
// in order to be more lcache efficient. // in order to be more lcache efficient.
@@ -48,14 +70,51 @@ struct File {
FIL* files; FIL* files;
u32 file_count; u32 file_count;
size_t off; size_t off;
char path[256]; char path[PATH_MAX];
}; };
struct Dir { struct Dir {
FDIR 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 get_size_from_files(const File* file) {
u64 size = 0; u64 size = 0;
for (u32 i = 0; i < file->file_count; i++) { 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) { 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; 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_mon = ((fno->fdate >> 5) & 0xF) - 1;
tm.tm_year = (fno->fdate >> 9) + 80; 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_mtime = st->st_atime;
st->st_ctime = 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]; char file_path[256];
for (u16 i = 0; i < 256; i++) { for (u16 i = 0; i < 256; i++) {
std::snprintf(file_path, sizeof(file_path), "%s/%02u", path, 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)) { if (FR_OK != f_stat(file_path, &file_info)) {
break; 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; st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
} else } else if (fno->fattrib & AM_DIR) {
if (fno->fattrib & AM_DIR) {
st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH; st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
} else { } else {
st->st_size = fno->fsize; 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) { Device::~Device() {
r->_errno = err; if (mounted) {
return -1; 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); auto file = static_cast<File*>(fileStruct);
std::memset(file, 0, sizeof(*file));
// todo: init array // todo: init array
// todo: handle dir. // todo: handle dir.
FIL fil{}; FIL fil{};
if (FR_OK == f_open(&fil, path, FA_READ)) { if (FR_OK == f_open(&fil, path, FA_READ)) {
file->file_count = 1;
file->files = (FIL*)std::malloc(sizeof(*file->files)); 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)); std::memcpy(file->files, &fil, sizeof(*file->files));
// todo: check what error code is returned here. // todo: check what error code is returned here.
} else { } else {
FILINFO info{}; FILINFO info{};
if (FR_OK != f_stat(path, &info)) { if (FR_OK != f_stat(path, &info)) {
return set_errno(r, ENOENT); return -ENOENT;
} }
if (!(info.fattrib & AM_ARC)) { if (!(info.fattrib & AM_ARC)) {
return set_errno(r, ENOENT); return -ENOENT;
} }
char file_path[256]; 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)); 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)); std::memcpy(&file->files[i], &fil, sizeof(fil));
file->file_count++; file->file_count++;
} }
} }
if (!file->files) { if (!file->files) {
return set_errno(r, ENOENT); return -ENOENT;
} }
std::snprintf(file->path, sizeof(file->path), "%s", path); 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); auto file = static_cast<File*>(fd);
if (file->files) { for (u32 i = 0; i < file->file_count; i++) {
for (u32 i = 0; i < file->file_count; i++) { f_close(&file->files[i]);
f_close(&file->files[i]);
}
free(file->files);
} }
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); auto file = static_cast<File*>(fd);
UINT total_bytes_read = 0; 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); auto fil = get_current_file(file);
if (!fil) { if (!fil) {
log_write("[FATFS] failed to get fil\n"); 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)) { if (FR_OK != f_read(fil, ptr, len, &bytes_read)) {
return set_errno(r, ENOENT); return -EIO;
} }
if (!bytes_read) { 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; 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); auto file = static_cast<File*>(fd);
const auto size = get_size_from_files(file); 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); file->off = std::clamp<u64>(pos, 0, size);
set_current_file_pos(file); set_current_file_pos(file);
r->_errno = 0;
return file->off; 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); auto file = static_cast<File*>(fd);
/* Only fill the attr and size field, leaving the timestamp blank. */ /* Only fill the attr and size field, leaving the timestamp blank. */
@@ -279,174 +357,118 @@ int fat_fstat(struct _reent *r, void *fd, struct stat *st) {
/* Fill stat info. */ /* Fill stat info. */
fill_stat(nullptr, &info, st); fill_stat(nullptr, &info, st);
return r->_errno = 0; return 0;
} }
DIR_ITER* fat_diropen(struct _reent *r, DIR_ITER *dirState, const char *path) { int Device::devoptab_diropen(void* fd, const char *path) {
auto dir = static_cast<Dir*>(dirState->dirStruct); auto dir = static_cast<Dir*>(fd);
std::memset(dir, 0, sizeof(*dir));
log_write("[FATFS] diropen: %s\n", path);
if (FR_OK != f_opendir(&dir->dir, path)) { if (FR_OK != f_opendir(&dir->dir, path)) {
set_errno(r, ENOENT); log_write("[FATFS] f_opendir(%s) failed\n", path);
return NULL; return -ENOENT;
} }
r->_errno = 0; log_write("[FATFS] Opened dir: %s\n", path);
return dirState; return 0;
} }
int fat_dirreset(struct _reent *r, DIR_ITER *dirState) { int Device::devoptab_dirreset(void* fd) {
auto dir = static_cast<Dir*>(dirState->dirStruct); auto dir = static_cast<Dir*>(fd);
if (FR_OK != f_rewinddir(&dir->dir)) { 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) { int Device::devoptab_dirnext(void* fd, char *filename, struct stat *filestat) {
auto dir = static_cast<Dir*>(dirState->dirStruct); auto dir = static_cast<Dir*>(fd);
FILINFO fno{}; FILINFO fno{};
if (FR_OK != f_readdir(&dir->dir, &fno)) { if (FR_OK != f_readdir(&dir->dir, &fno)) {
return set_errno(r, ENOENT); return -EIO;
} }
if (!fno.fname[0]) { 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); fill_stat(dir->path, &fno, filestat);
return r->_errno = 0; return 0;
} }
int fat_dirclose(struct _reent *r, DIR_ITER *dirState) { int Device::devoptab_dirclose(void* fd) {
auto dir = static_cast<Dir*>(dirState->dirStruct); auto dir = static_cast<Dir*>(fd);
if (FR_OK != f_closedir(&dir->dir)) { 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) { int Device::devoptab_lstat(const char *path, struct stat *st) {
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) {
FILINFO fno; FILINFO fno;
if (FR_OK != f_stat(file, &fno)) { if (FR_OK != f_stat(path, &fno)) {
return set_errno(r, ENOENT); return -ENOENT;
} }
fill_stat(file, &fno, st); fill_stat(path, &fno, st);
return r->_errno = 0; 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 } // namespace
Result MountAll() { Result MountFatfsAll() {
SCOPED_MUTEX(&g_mutex);
if (g_is_init) {
R_SUCCEED();
}
for (u32 i = 0; i < FF_VOLUMES; i++) { for (u32 i = 0; i < FF_VOLUMES; i++) {
auto& fat = g_fat_storage[i];
const auto& bis = BIS_MOUNT_ENTRIES[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; if (!common::MountNetworkDevice2(
fat.devoptab.name = bis.volume_name; std::make_unique<Device>((BisMountType)i, config),
fat.devoptab.deviceData = &fat; config,
sizeof(File), sizeof(Dir),
R_TRY(fsOpenBisStorage(&fat.storage, bis.id)); bis.volume_name, bis.mount_name
auto source = std::make_shared<FsStorageSource>(&fat.storage); )) {
log_write("[FATFS] Failed to mount %s\n", bis.volume_name);
s64 size; }
R_TRY(source->GetSize(&size));
// log_write("[FAT] BIS SUCCESS %s\n", bis.volume_name);
fat.buffered = std::make_unique<devoptab::common::LruBufferedData>(source, size);
R_UNLESS(FR_OK == f_mount(&fat.fs, bis.mount_name, 1), 0x1);
// 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);
} }
g_is_init = true;
R_SUCCEED(); R_SUCCEED();
} }
void UnmountAll() { } // namespace sphaira::devoptab
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
extern "C" { extern "C" {
const char* VolumeStr[] { const char* VolumeStr[] {
sphaira::fatfs::BIS_MOUNT_ENTRIES[0].volume_name, sphaira::devoptab::BIS_MOUNT_ENTRIES[0].volume_name,
sphaira::fatfs::BIS_MOUNT_ENTRIES[1].volume_name, sphaira::devoptab::BIS_MOUNT_ENTRIES[1].volume_name,
sphaira::fatfs::BIS_MOUNT_ENTRIES[2].volume_name, sphaira::devoptab::BIS_MOUNT_ENTRIES[2].volume_name,
sphaira::fatfs::BIS_MOUNT_ENTRIES[3].volume_name, sphaira::devoptab::BIS_MOUNT_ENTRIES[3].volume_name,
}; };
Result fatfs_read(u8 num, void* dst, u64 offset, u64 size) { Result fatfs_read(u8 num, void* dst, u64 offset, u64 size) {
// log_write("[FAT] num: %u\n", num); auto& fat = sphaira::devoptab::g_fat_storage[num];
auto& fat = sphaira::fatfs::g_fat_storage[num];
return fat.buffered->Read2(dst, offset, size); 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" } // 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 <array>
#include <memory> #include <memory>
#include <algorithm> #include <algorithm>
#include <sys/iosupport.h>
namespace sphaira::devoptab { namespace sphaira::devoptab {
namespace { namespace {
@@ -74,25 +73,18 @@ struct DirEntry {
const yati::container::Collections* pfs0; const yati::container::Collections* pfs0;
}; };
struct Device {
std::vector<NamedCollection> collections;
std::unique_ptr<yati::source::Base> source;
};
struct File { struct File {
Device* device;
FileEntry entry; FileEntry entry;
size_t off; size_t off;
}; };
struct Dir { struct Dir {
Device* device;
DirEntry entry; DirEntry entry;
u32 index; u32 index;
bool is_root; 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) { for (auto& e : named) {
if (path.starts_with("/" + e.name)) { if (path.starts_with("/" + e.name)) {
out.fs_type = e.fs_type; 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; return false;
} }
int set_errno(struct _reent *r, int err) { struct Device final : common::MountDevice {
r->_errno = err; Device(std::unique_ptr<yati::source::Base>&& _source, const std::vector<NamedCollection>& _collections, const common::MountConfig& _config)
return -1; : 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); auto file = static_cast<File*>(fileStruct);
std::memset(file, 0, sizeof(*file)); std::memset(file, 0, sizeof(*file));
char path[FS_MAX_PATH]; FileEntry entry{};
if (!common::fix_path(_path, path)) { if (!find_file(this->collections, path, entry)) {
return set_errno(r, ENOENT); 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; file->entry = entry;
return 0;
return r->_errno = 0;
} }
int devoptab_close(struct _reent *r, void *fd) { int Device::devoptab_close(void *fd) {
auto file = static_cast<File*>(fd); auto file = static_cast<File*>(fd);
std::memset(file, 0, sizeof(*file)); 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); auto file = static_cast<File*>(fd);
const auto& entry = file->entry; const auto& entry = file->entry;
u64 bytes_read; u64 bytes_read;
len = std::min(len, entry.size - file->off); len = std::min(len, entry.size - file->off);
if (R_FAILED(file->device->source->Read(ptr, entry.offset + file->off, len, &bytes_read))) { if (R_FAILED(this->source->Read(ptr, entry.offset + file->off, len, &bytes_read))) {
return set_errno(r, ENOENT); return -EIO;
} }
file->off += bytes_read; file->off += bytes_read;
return 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); auto file = static_cast<File*>(fd);
const auto& entry = file->entry; 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; pos = entry.size;
} }
r->_errno = 0;
return file->off = std::clamp<u64>(pos, 0, entry.size); 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); auto file = static_cast<File*>(fd);
const auto& entry = file->entry; const auto& entry = file->entry;
std::memset(st, 0, sizeof(*st));
st->st_nlink = 1; st->st_nlink = 1;
st->st_size = entry.size; st->st_size = entry.size;
st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH; 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) { int Device::devoptab_diropen(void* fd, const char *path) {
auto device = (Device*)r->deviceData; auto dir = static_cast<Dir*>(fd);
auto dir = static_cast<Dir*>(dirState->dirStruct);
std::memset(dir, 0, sizeof(*dir)); 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, "/")) { if (!std::strcmp(path, "/")) {
dir->device = device;
dir->is_root = true; dir->is_root = true;
r->_errno = 0; return 0;
return dirState;
} else { } else {
DirEntry entry; DirEntry entry{};
if (!find_dir(device->collections, path, entry)) { if (!find_dir(this->collections, path, entry)) {
set_errno(r, ENOENT); return -ENOENT;
return NULL;
} }
dir->device = device;
dir->entry = entry; dir->entry = entry;
return 0;
r->_errno = 0;
return dirState;
} }
} }
int devoptab_dirreset(struct _reent *r, DIR_ITER *dirState) { int Device::devoptab_dirreset(void* fd) {
auto dir = static_cast<Dir*>(dirState->dirStruct); auto dir = static_cast<Dir*>(fd);
auto& entry = dir->entry; auto& entry = dir->entry;
if (dir->is_root) { 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) { int Device::devoptab_dirnext(void* fd, char *filename, struct stat *filestat) {
auto dir = static_cast<Dir*>(dirState->dirStruct); auto dir = static_cast<Dir*>(fd);
auto& entry = dir->entry; auto& entry = dir->entry;
std::memset(filestat, 0, sizeof(*filestat));
if (dir->is_root) { if (dir->is_root) {
if (dir->index >= dir->device->collections.size()) { if (dir->index >= this->collections.size()) {
return set_errno(r, ENOENT); return -ENOENT;
} }
filestat->st_nlink = 1; filestat->st_nlink = 1;
filestat->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH; 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 { } else {
if (entry.fs_type == nca::FileSystemType_RomFS) { if (entry.fs_type == nca::FileSystemType_RomFS) {
if (!romfs::dirnext(entry.romfs, filename, filestat)) { if (!romfs::dirnext(entry.romfs, filename, filestat)) {
return set_errno(r, ENOENT); return -ENOENT;
} }
} else { } else {
if (dir->index >= entry.pfs0->size()) { if (dir->index >= entry.pfs0->size()) {
return set_errno(r, ENOENT); return -ENOENT;
} }
const auto& collection = (*entry.pfs0)[dir->index]; 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++; dir->index++;
return r->_errno = 0; return 0;
} }
int devoptab_dirclose(struct _reent *r, DIR_ITER *dirState) { int Device::devoptab_dirclose(void* fd) {
auto dir = static_cast<Dir*>(dirState->dirStruct); auto dir = static_cast<Dir*>(fd);
std::memset(dir, 0, sizeof(*dir)); std::memset(dir, 0, sizeof(*dir));
return r->_errno = 0; return 0;
} }
int devoptab_lstat(struct _reent *r, const char *_path, struct stat *st) { int Device::devoptab_lstat(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));
st->st_nlink = 1; st->st_nlink = 1;
if (!std::strcmp(path, "/")) { if (!std::strcmp(path, "/")) {
st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH; st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
} else { } else {
// can be optimised for romfs. // can be optimised for romfs.
FileEntry file_entry; FileEntry file_entry{};
DirEntry dir_entry; DirEntry dir_entry{};
if (find_file(device->collections, path, file_entry)) { if (find_file(this->collections, path, file_entry)) {
st->st_size = file_entry.size; st->st_size = file_entry.size;
st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH; 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; st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
} else { } else {
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,
};
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;
} }
Result MountNcaInternal(fs::Fs* fs, const std::shared_ptr<yati::source::Base>& source, s64 size, const fs::FsPath& path, fs::FsPath& out_path) { 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 // todo: rather than manually fetching tickets, use spl to
// decrypt the nca for use (somehow, look how ams does it?). // decrypt the nca for use (somehow, look how ams does it?).
keys::Keys keys; keys::Keys keys;
R_TRY(keys::parse_keys(keys, true)); R_TRY(keys::parse_keys(keys, true));
nca::Header header; nca::Header header{};
R_TRY(source->Read2(&header, 0, sizeof(header))); R_TRY(source->Read2(&header, 0, sizeof(header)));
R_TRY(nca::DecryptHeader(&header, keys, 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)); log_write("[NCA] got header, type: %s\n", nca::GetContentTypeStr(header.content_type));
// check if this is a ncz. // check if this is a ncz.
ncz::Header ncz_header; ncz::Header ncz_header{};
R_TRY(source->Read2(&ncz_header, NCZ_NORMAL_SIZE, sizeof(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. // read all the sections.
s64 ncz_offset = NCZ_SECTION_OFFSET; s64 ncz_offset = NCZ_SECTION_OFFSET;
ncz::Sections ncz_sections(ncz_header.total_sections); ncz::Sections ncz_sections(ncz_header.total_sections);
R_TRY(source->Read2(ncz_sections.data(), ncz_offset, ncz_sections.size() * sizeof(ncz::Section))); R_TRY(source->Read2(ncz_sections.data(), ncz_offset, ncz_sections.size() * sizeof(ncz::Section)));
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))); R_TRY(source->Read2(&ncz_block_header, ncz_offset, sizeof(ncz_block_header)));
// ensure this is a block compressed nsz, otherwise bail out // 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]; const auto& content_type_fs = CONTENT_TYPE_FS_NAMES[header.content_type];
for (u32 i = 0; i < NCA_SECTION_TOTAL; i++) { 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; continue;
} }
NamedCollection collection; NamedCollection collection{};
collection.name = content_type_fs[i].name; collection.name = content_type_fs[i].name;
collection.fs_type = fs_header.fs_type; 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); R_UNLESS(!collections.empty(), 0x9);
auto entry = std::make_unique<Entry>(); if (!common::MountReadOnlyIndexDevice(
entry->path = path; [&nca_reader, &collections](const common::MountConfig& config) {
entry->devoptab = DEVOPTAB; return std::make_unique<Device>(std::move(nca_reader), collections, config);
entry->devoptab.name = entry->name; },
entry->devoptab.deviceData = &entry->device; sizeof(File), sizeof(Dir),
entry->device.source = std::move(nca_reader); "NCA", out_path
entry->device.collections = std::move(collections); )) {
std::snprintf(entry->name, sizeof(entry->name), "nca_%zu", index); log_write("[NCA] Failed to mount %s\n", path.s);
std::snprintf(entry->mount, sizeof(entry->mount), "nca_%zu:/", index); R_THROW(0x1);
}
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);
R_SUCCEED(); R_SUCCEED();
} }
@@ -558,12 +483,6 @@ Result MountNcaInternal(fs::Fs* fs, const std::shared_ptr<yati::source::Base>& s
} // namespace } // namespace
Result MountNca(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path) { 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; s64 size;
auto source = std::make_shared<yati::source::File>(fs, path); auto source = std::make_shared<yati::source::File>(fs, path);
R_TRY(source->GetSize(&size)); 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) { 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; s64 size;
auto source = std::make_shared<ncm::NcmSource>(cs, id); auto source = std::make_shared<ncm::NcmSource>(cs, id);
R_TRY(source->GetSize(&size)); R_TRY(source->GetSize(&size));
return MountNcaInternal(nullptr, source, size, path, out_path); return MountNcaInternal(nullptr, source, size, {}, 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();
}
} }
} // namespace sphaira::devoptab } // 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 "log.hpp"
#include "nro.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 "yati/source/file.hpp"
#include <cstring> #include <cstring>
@@ -18,7 +13,6 @@
#include <array> #include <array>
#include <memory> #include <memory>
#include <algorithm> #include <algorithm>
#include <sys/iosupport.h>
namespace sphaira::devoptab { namespace sphaira::devoptab {
namespace { namespace {
@@ -48,26 +42,18 @@ struct DirEntry {
romfs::DirEntry romfs; romfs::DirEntry romfs;
}; };
struct Device {
std::unique_ptr<yati::source::Base> source;
std::vector<NamedCollection> collections;
FsTimeStampRaw timestamp;
};
struct File { struct File {
Device* device;
FileEntry entry; FileEntry entry;
size_t off; size_t off;
}; };
struct Dir { struct Dir {
Device* device;
DirEntry entry; DirEntry entry;
u32 index; u32 index;
bool is_root; 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) { for (auto& e : named) {
if (path.starts_with("/" + e.name)) { if (path.starts_with("/" + e.name)) {
out.is_romfs = e.is_romfs; 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; return false;
} }
void fill_timestamp_from_device(const Device* device, struct stat *st) { void fill_timestamp_from_device(const FsTimeStampRaw& timestamp, struct stat *st) {
st->st_atime = device->timestamp.accessed; st->st_atime = timestamp.accessed;
st->st_ctime = device->timestamp.created; st->st_ctime = timestamp.created;
st->st_mtime = device->timestamp.modified; st->st_mtime = timestamp.modified;
} }
int set_errno(struct _reent *r, int err) { struct Device final : common::MountDevice {
r->_errno = err; Device(std::unique_ptr<yati::source::Base>&& _source, const std::vector<NamedCollection>& _collections, const FsTimeStampRaw& _timestamp, const common::MountConfig& _config)
return -1; : 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); auto file = static_cast<File*>(fileStruct);
std::memset(file, 0, sizeof(*file));
char path[FS_MAX_PATH]; FileEntry entry{};
if (!common::fix_path(_path, path)) { if (!find_file(this->collections, path, entry)) {
return set_errno(r, ENOENT); 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; file->entry = entry;
return 0;
return r->_errno = 0;
} }
int devoptab_close(struct _reent *r, void *fd) { int Device::devoptab_close(void *fd) {
auto file = static_cast<File*>(fd); auto file = static_cast<File*>(fd);
std::memset(file, 0, sizeof(*file)); 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); auto file = static_cast<File*>(fd);
const auto& entry = file->entry; const auto& entry = file->entry;
u64 bytes_read; u64 bytes_read;
len = std::min(len, entry.size - file->off); len = std::min(len, entry.size - file->off);
if (R_FAILED(file->device->source->Read(ptr, entry.offset + file->off, len, &bytes_read))) { if (R_FAILED(this->source->Read(ptr, entry.offset + file->off, len, &bytes_read))) {
return set_errno(r, ENOENT); return -EIO;
} }
file->off += bytes_read; file->off += bytes_read;
return 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); auto file = static_cast<File*>(fd);
const auto& entry = file->entry; 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; pos = entry.size;
} }
r->_errno = 0;
return file->off = std::clamp<u64>(pos, 0, entry.size); 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); auto file = static_cast<File*>(fd);
const auto& entry = file->entry; const auto& entry = file->entry;
std::memset(st, 0, sizeof(*st));
st->st_nlink = 1; st->st_nlink = 1;
st->st_size = entry.size; st->st_size = entry.size;
st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH; 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) { int Device::devoptab_diropen(void* fd, const char *path) {
auto device = (Device*)r->deviceData; auto dir = static_cast<Dir*>(fd);
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;
}
if (!std::strcmp(path, "/")) { if (!std::strcmp(path, "/")) {
dir->device = device;
dir->is_root = true; dir->is_root = true;
r->_errno = 0; return 0;
return dirState;
} else { } else {
DirEntry entry; DirEntry entry{};
if (!find_dir(device->collections, path, entry)) { if (!find_dir(this->collections, path, entry)) {
set_errno(r, ENOENT); return -ENOENT;
return NULL;
} }
dir->device = device;
dir->entry = entry; dir->entry = entry;
return 0;
r->_errno = 0;
return dirState;
} }
} }
int devoptab_dirreset(struct _reent *r, DIR_ITER *dirState) { int Device::devoptab_dirreset(void* fd) {
auto dir = static_cast<Dir*>(dirState->dirStruct); auto dir = static_cast<Dir*>(fd);
auto& entry = dir->entry; auto& entry = dir->entry;
if (dir->is_root) { 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) { int Device::devoptab_dirnext(void* fd, char *filename, struct stat *filestat) {
auto dir = static_cast<Dir*>(dirState->dirStruct); auto dir = static_cast<Dir*>(fd);
auto& entry = dir->entry; auto& entry = dir->entry;
std::memset(filestat, 0, sizeof(*filestat));
if (dir->is_root) { if (dir->is_root) {
if (dir->index >= dir->device->collections.size()) { if (dir->index >= this->collections.size()) {
return set_errno(r, ENOENT); return -ENOENT;
} }
const auto& e = dir->device->collections[dir->index]; const auto& e = this->collections[dir->index];
if (e.is_romfs) { if (e.is_romfs) {
filestat->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH; filestat->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
} else { } else {
@@ -262,126 +245,60 @@ int devoptab_dirnext(struct _reent *r, DIR_ITER *dirState, char *filename, struc
} else { } else {
if (entry.is_romfs) { if (entry.is_romfs) {
if (!romfs::dirnext(entry.romfs, filename, filestat)) { 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++; dir->index++;
return r->_errno = 0; return 0;
} }
int devoptab_dirclose(struct _reent *r, DIR_ITER *dirState) { int Device::devoptab_dirclose(void* fd) {
auto dir = static_cast<Dir*>(dirState->dirStruct); auto dir = static_cast<Dir*>(fd);
std::memset(dir, 0, sizeof(*dir)); std::memset(dir, 0, sizeof(*dir));
return r->_errno = 0; return 0;
} }
int devoptab_lstat(struct _reent *r, const char *_path, struct stat *st) { int Device::devoptab_lstat(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));
st->st_nlink = 1; st->st_nlink = 1;
if (!std::strcmp(path, "/")) { if (!std::strcmp(path, "/")) {
st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH; st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
} else { } else {
// can be optimised for romfs. // can be optimised for romfs.
FileEntry file_entry; FileEntry file_entry{};
DirEntry dir_entry; DirEntry dir_entry{};
if (find_file(device->collections, path, file_entry)) { if (find_file(this->collections, path, file_entry)) {
st->st_size = file_entry.size; st->st_size = file_entry.size;
st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH; 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; st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
} else { } else {
return set_errno(r, ENOENT); return -ENOENT;
} }
} }
fill_timestamp_from_device(device, st); fill_timestamp_from_device(this->timestamp, st);
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;
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;
} }
} // namespace } // namespace
Result MountNro(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path) { 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); auto source = std::make_unique<yati::source::File>(fs, path);
NroData data; NroData data{};
R_TRY(source->Read2(&data, 0, sizeof(data))); R_TRY(source->Read2(&data, 0, sizeof(data)));
R_UNLESS(data.header.magic == NROHEADER_MAGIC, Result_NroBadMagic); R_UNLESS(data.header.magic == NROHEADER_MAGIC, Result_NroBadMagic);
NroAssetHeader asset; NroAssetHeader asset{};
R_TRY(source->Read2(&asset, data.header.size, sizeof(asset))); R_TRY(source->Read2(&asset, data.header.size, sizeof(asset)));
R_UNLESS(asset.magic == NROASSETHEADER_MAGIC, Result_NroBadMagic); R_UNLESS(asset.magic == NROASSETHEADER_MAGIC, Result_NroBadMagic);
std::vector<NamedCollection> collections; std::vector<NamedCollection> collections{};
if (asset.icon.size) { if (asset.icon.size) {
NamedCollection collection{"icon.jpg", false, AssetCollection{data.header.size + asset.icon.offset, 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); R_UNLESS(!collections.empty(), 0x9);
auto entry = std::make_unique<Entry>(); FsTimeStampRaw timestamp{};
entry->path = path; fs->GetFileTimeStampRaw(path, &timestamp);
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);
R_UNLESS(AddDevice(&entry->devoptab) >= 0, 0x1); if (!common::MountReadOnlyIndexDevice(
log_write("[NRO] DEVICE SUCCESS %s %s\n", path.s, entry->name); [&source, &collections, &timestamp](const common::MountConfig& config) {
return std::make_unique<Device>(std::move(source), collections, timestamp, config);
out_path = entry->mount; },
entry->ref_count++; sizeof(File), sizeof(Dir),
*itr = std::move(entry); "NRO", out_path
)) {
log_write("[NRO] Failed to mount %s\n", path.s);
R_THROW(0x1);
}
R_SUCCEED(); 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 } // namespace sphaira::devoptab

View File

@@ -13,76 +13,84 @@
#include <array> #include <array>
#include <memory> #include <memory>
#include <algorithm> #include <algorithm>
#include <sys/iosupport.h>
namespace sphaira::devoptab { namespace sphaira::devoptab {
namespace { namespace {
struct Device { using Collections = yati::container::Collections;
std::unique_ptr<common::LruBufferedData> source;
yati::container::Collections collections;
};
struct File { struct File {
Device* device;
const yati::container::CollectionEntry* collection; const yati::container::CollectionEntry* collection;
size_t off; size_t off;
}; };
struct Dir { struct Dir {
Device* device;
u32 index; u32 index;
}; };
int set_errno(struct _reent *r, int err) { struct Device final : common::MountDevice {
r->_errno = err; Device(std::unique_ptr<common::LruBufferedData>&& _source, const Collections& _collections, const common::MountConfig& _config)
return -1; : 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) { if (path == "/" + collection.name) {
file->device = device;
file->collection = &collection; 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); auto file = static_cast<File*>(fd);
std::memset(file, 0, sizeof(*file)); 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); auto file = static_cast<File*>(fd);
const auto& collection = file->collection; const auto& collection = file->collection;
len = std::min(len, collection->size - file->off); len = std::min(len, collection->size - file->off);
u64 bytes_read; u64 bytes_read;
if (R_FAILED(file->device->source->Read(ptr, collection->offset + file->off, len, &bytes_read))) { if (R_FAILED(this->source->Read(ptr, collection->offset + file->off, len, &bytes_read))) {
return set_errno(r, ENOENT); return -EIO;
} }
file->off += bytes_read; file->off += bytes_read;
return 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); auto file = static_cast<File*>(fd);
const auto& collection = file->collection; 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; pos = collection->size;
} }
r->_errno = 0;
return file->off = std::clamp<u64>(pos, 0, collection->size); 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); auto file = static_cast<File*>(fd);
const auto& collection = file->collection; const auto& collection = file->collection;
std::memset(st, 0, sizeof(*st));
st->st_nlink = 1; st->st_nlink = 1;
st->st_size = collection->size; st->st_size = collection->size;
st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH; 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) { int Device::devoptab_diropen(void* fd, 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;
}
if (!std::strcmp(path, "/")) { if (!std::strcmp(path, "/")) {
dir->device = device; return 0;
} else {
set_errno(r, ENOENT);
return NULL;
} }
r->_errno = 0; log_write("[NSP] failed to open dir %s\n", path);
return dirState; return -ENOENT;
} }
int devoptab_dirreset(struct _reent *r, DIR_ITER *dirState) { int Device::devoptab_dirreset(void* fd) {
auto dir = static_cast<Dir*>(dirState->dirStruct); auto dir = static_cast<Dir*>(fd);
dir->index = 0; dir->index = 0;
return 0;
return r->_errno = 0;
} }
int devoptab_dirnext(struct _reent *r, DIR_ITER *dirState, char *filename, struct stat *filestat) { int Device::devoptab_dirnext(void* fd, char *filename, struct stat *filestat) {
auto dir = static_cast<Dir*>(dirState->dirStruct); auto dir = static_cast<Dir*>(fd);
std::memset(filestat, 0, sizeof(*filestat));
if (dir->index >= dir->device->collections.size()) { if (dir->index >= this->collections.size()) {
return set_errno(r, ENOENT); return -ENOENT;
} }
const auto& collection = dir->device->collections[dir->index]; const auto& collection = this->collections[dir->index];
filestat->st_nlink = 1; filestat->st_nlink = 1;
filestat->st_size = collection.size; filestat->st_size = collection.size;
filestat->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH; filestat->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
std::strcpy(filename, collection.name.c_str()); std::strcpy(filename, collection.name.c_str());
dir->index++; dir->index++;
return r->_errno = 0; return 0;
} }
int devoptab_dirclose(struct _reent *r, DIR_ITER *dirState) { int Device::devoptab_dirclose(void* fd) {
auto dir = static_cast<Dir*>(dirState->dirStruct); auto dir = static_cast<Dir*>(fd);
std::memset(dir, 0, sizeof(*dir)); std::memset(dir, 0, sizeof(*dir));
return r->_errno = 0; return 0;
} }
int devoptab_lstat(struct _reent *r, const char *_path, struct stat *st) { int Device::devoptab_lstat(const char *path, struct stat *st) {
auto device = (Device*)r->deviceData; 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));
if (!std::strcmp(path, "/")) { if (!std::strcmp(path, "/")) {
st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH; st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
} else { } 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; return path == "/" + e.name;
}); });
if (it == device->collections.end()) { if (it == this->collections.end()) {
return set_errno(r, ENOENT); return -ENOENT;
} }
st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH; st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
st->st_size = it->size; st->st_size = it->size;
} }
st->st_nlink = 1; return 0;
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;
} // namespace } // namespace
Result MountNsp(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path) { 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); auto source = std::make_shared<yati::source::File>(fs, path);
s64 size; s64 size;
@@ -255,44 +187,18 @@ Result MountNsp(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path) {
yati::container::Collections collections; yati::container::Collections collections;
R_TRY(nsp.GetCollections(collections)); R_TRY(nsp.GetCollections(collections));
auto entry = std::make_unique<Entry>(); if (!common::MountReadOnlyIndexDevice(
entry->path = path; [&buffered, &collections](const common::MountConfig& config) {
entry->devoptab = DEVOPTAB; return std::make_unique<Device>(std::move(buffered), collections, config);
entry->devoptab.name = entry->name; },
entry->devoptab.deviceData = &entry->device; sizeof(File), sizeof(Dir),
entry->device.source = std::move(buffered); "NSP", out_path
entry->device.collections = collections; )) {
std::snprintf(entry->name, sizeof(entry->name), "nsp_%zu", index); log_write("[NSP] Failed to mount %s\n", path.s);
std::snprintf(entry->mount, sizeof(entry->mount), "nsp_%zu:/", index); R_THROW(0x1);
}
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);
R_SUCCEED(); 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 } // namespace sphaira::devoptab

View File

@@ -12,18 +12,11 @@
#include <array> #include <array>
#include <memory> #include <memory>
#include <algorithm> #include <algorithm>
#include <sys/iosupport.h>
namespace sphaira::devoptab { namespace sphaira::devoptab {
namespace { namespace {
struct Device {
save_ctx_t* ctx;
hierarchical_save_file_table_ctx_t* file_table;
};
struct File { struct File {
Device* device;
save_fs_list_entry_t entry; save_fs_list_entry_t entry;
allocation_table_storage_ctx_t storage; allocation_table_storage_ctx_t storage;
size_t off; size_t off;
@@ -35,138 +28,140 @@ struct DirNext {
}; };
struct Dir { struct Dir {
Device* device;
save_fs_list_entry_t entry; save_fs_list_entry_t entry;
u32 next_directory; u32 next_directory;
u32 next_file; u32 next_file;
}; };
int set_errno(struct _reent *r, int err) { struct Device final : common::MountDevice {
r->_errno = err; Device(save_ctx_t* _ctx, const common::MountConfig& _config)
return -1; : 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) { ~Device() {
auto device = (Device*)r->deviceData; 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); auto file = static_cast<File*>(fileStruct);
std::memset(file, 0, sizeof(*file));
char path[FS_MAX_PATH]; if (!save_hierarchical_file_table_get_file_entry_by_path(this->file_table, path, &file->entry)) {
if (!common::fix_path(_path, path)) { return -ENOENT;
return set_errno(r, ENOENT);
} }
if (!save_hierarchical_file_table_get_file_entry_by_path(device->file_table, path, &file->entry)) { if (!save_open_fat_storage(&this->ctx->save_filesystem_core, &file->storage, file->entry.value.save_file_info.start_block)) {
return set_errno(r, ENOENT); return -ENOENT;
} }
if (!save_open_fat_storage(&device->ctx->save_filesystem_core, &file->storage, file->entry.value.save_file_info.start_block)) { return 0;
return set_errno(r, ENOENT);
}
file->device = device;
return r->_errno = 0;
} }
int devoptab_close(struct _reent *r, void *fd) { int Device::devoptab_close(void *fd) {
auto file = static_cast<File*>(fd); auto file = static_cast<File*>(fd);
std::memset(file, 0, sizeof(*file)); 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); 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? // todo: maybe eof here?
const auto bytes_read = save_allocation_table_storage_read(&file->storage, ptr, file->off, len); const auto bytes_read = save_allocation_table_storage_read(&file->storage, ptr, file->off, len);
if (!bytes_read) { if (!bytes_read) {
return set_errno(r, ENOENT); return -ENOENT;
} }
file->off += bytes_read; file->off += bytes_read;
return 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); auto file = static_cast<File*>(fd);
if (dir == SEEK_CUR) { if (dir == SEEK_CUR) {
pos += file->off; pos += file->off;
} else if (dir == SEEK_END) { } 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->entry.value.save_file_info.length);
return file->off = std::clamp<u64>(pos, 0, file->storage._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); auto file = static_cast<File*>(fd);
log_write("[\t\tDEV] fstat\n");
std::memset(st, 0, sizeof(*st));
st->st_nlink = 1; 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; 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) { int Device::devoptab_diropen(void* fd, const char *path) {
auto device = (Device*)r->deviceData; auto dir = static_cast<Dir*>(fd);
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;
}
if (!std::strcmp(path, "/")) { if (!std::strcmp(path, "/")) {
save_entry_key_t key{}; 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) { if (idx == 0xFFFFFFFF) {
set_errno(r, ENOENT); return -ENOENT;
return NULL;
} }
if (!save_fs_list_get_value(&device->file_table->directory_table, idx, &dir->entry)) { if (!save_fs_list_get_value(&this->file_table->directory_table, idx, &dir->entry)) {
set_errno(r, ENOENT); return -ENOENT;
return NULL;
} }
} else if (!save_hierarchical_directory_table_get_file_entry_by_path(device->file_table, path, &dir->entry)) { } else if (!save_hierarchical_directory_table_get_file_entry_by_path(this->file_table, path, &dir->entry)) {
set_errno(r, ENOENT); return -ENOENT;
return NULL;
} }
dir->device = device;
dir->next_file = dir->entry.value.save_find_position.next_file; dir->next_file = dir->entry.value.save_find_position.next_file;
dir->next_directory = dir->entry.value.save_find_position.next_directory; dir->next_directory = dir->entry.value.save_find_position.next_directory;
r->_errno = 0; return 0;
return dirState;
} }
int devoptab_dirreset(struct _reent *r, DIR_ITER *dirState) { int Device::devoptab_dirreset(void* fd) {
auto dir = static_cast<Dir*>(dirState->dirStruct); auto dir = static_cast<Dir*>(fd);
dir->next_file = dir->entry.value.save_find_position.next_file; dir->next_file = dir->entry.value.save_find_position.next_file;
dir->next_directory = dir->entry.value.save_find_position.next_directory; 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) { int Device::devoptab_dirnext(void* fd, char *filename, struct stat *filestat) {
auto dir = static_cast<Dir*>(dirState->dirStruct); auto dir = static_cast<Dir*>(fd);
std::memset(filestat, 0, sizeof(*filestat));
save_fs_list_entry_t entry{}; save_fs_list_entry_t entry{};
if (dir->next_directory) { if (dir->next_directory) {
// todo: use save_allocation_table_storage_read for faster reads // 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)) { if (!save_fs_list_get_value(&this->file_table->directory_table, dir->next_directory, &entry)) {
return set_errno(r, ENOENT); return -ENOENT;
} }
filestat->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH; 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) { else if (dir->next_file) {
// todo: use save_allocation_table_storage_read for faster reads // 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)) { if (!save_fs_list_get_value(&this->file_table->file_table, dir->next_file, &entry)) {
return set_errno(r, ENOENT); return -ENOENT;
} }
filestat->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH; 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; dir->next_file = entry.value.next_sibling;
} }
else { else {
return set_errno(r, ENOENT); return -ENOENT;
} }
filestat->st_nlink = 1; 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) { int Device::devoptab_dirclose(void* fd) {
auto dir = static_cast<Dir*>(dirState->dirStruct); auto dir = static_cast<Dir*>(fd);
std::memset(dir, 0, sizeof(*dir)); std::memset(dir, 0, sizeof(*dir));
return r->_errno = 0; return 0;
} }
int devoptab_lstat(struct _reent *r, const char *_path, struct stat *st) { int Device::devoptab_lstat(const char *path, struct stat *st) {
auto device = (Device*)r->deviceData; 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{}; save_fs_list_entry_t entry{};
// NOTE: this is very slow. // 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_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
st->st_size = entry.value.save_file_info.length; 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; st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
} else { } else {
return set_errno(r, ENOENT); return -ENOENT;
} }
st->st_nlink = 1; return 0;
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{};
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 } // namespace
Result MountSaveSystem(u64 id, fs::FsPath& out_path) { 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. fs::FsPath path{};
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];
std::snprintf(path, sizeof(path), "SYSTEM:/save/%016lx", id); std::snprintf(path, sizeof(path), "SYSTEM:/save/%016lx", id);
auto ctx = save_open_savefile(path, 0); auto ctx = save_open_savefile(path, 0);
@@ -288,46 +225,18 @@ Result MountSaveSystem(u64 id, fs::FsPath& out_path) {
R_THROW(0x1); R_THROW(0x1);
} }
log_write("[SAVE] OPEN SUCCESS %s\n", path); if (!common::MountReadOnlyIndexDevice(
[&ctx](const common::MountConfig& config) {
auto entry = std::make_unique<Entry>(); return std::make_unique<Device>(ctx, config);
entry->id = id; },
entry->device.ctx = ctx; sizeof(File), sizeof(Dir),
entry->device.file_table = &ctx->save_filesystem_core.file_table; "SAVE", out_path
entry->devoptab = DEVOPTAB; )) {
entry->devoptab.name = entry->name; log_write("[SAVE] Failed to mount %s\n", path.s);
entry->devoptab.deviceData = &entry->device; R_THROW(0x1);
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);
R_SUCCEED(); 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 } // 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 <array>
#include <memory> #include <memory>
#include <algorithm> #include <algorithm>
#include <sys/iosupport.h>
namespace sphaira::devoptab { namespace sphaira::devoptab {
namespace { namespace {
struct Device {
std::unique_ptr<common::LruBufferedData> source;
yati::container::Xci::Partitions partitions;
};
struct File { struct File {
Device* device;
const yati::container::CollectionEntry* collection; const yati::container::CollectionEntry* collection;
size_t off; size_t off;
}; };
struct Dir { struct Dir {
Device* device;
const yati::container::Collections* collections; const yati::container::Collections* collections;
u32 index; u32 index;
}; };
int set_errno(struct _reent *r, int err) { struct Device final : common::MountDevice {
r->_errno = err; Device(std::unique_ptr<common::LruBufferedData>&& _source, const yati::container::Xci::Partitions& _partitions, const common::MountConfig& _config)
return -1; : 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) { for (const auto& collection : partition.collections) {
if (path == "/" + partition.name + "/" + collection.name) { if (path == "/" + partition.name + "/" + collection.name) {
file->device = device;
file->collection = &collection; 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); auto file = static_cast<File*>(fd);
std::memset(file, 0, sizeof(*file)); 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); auto file = static_cast<File*>(fd);
const auto& collection = file->collection; const auto& collection = file->collection;
len = std::min(len, collection->size - file->off); len = std::min(len, collection->size - file->off);
u64 bytes_read; u64 bytes_read;
if (R_FAILED(file->device->source->Read(ptr, collection->offset + file->off, len, &bytes_read))) { if (R_FAILED(this->source->Read(ptr, collection->offset + file->off, len, &bytes_read))) {
return set_errno(r, ENOENT); return -EIO;
} }
file->off += bytes_read; file->off += bytes_read;
return 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); auto file = static_cast<File*>(fd);
const auto& collection = file->collection; 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; pos = collection->size;
} }
r->_errno = 0;
return file->off = std::clamp<u64>(pos, 0, collection->size); 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); auto file = static_cast<File*>(fd);
const auto& collection = file->collection; const auto& collection = file->collection;
std::memset(st, 0, sizeof(*st));
st->st_nlink = 1; st->st_nlink = 1;
st->st_size = collection->size; st->st_size = collection->size;
st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH; 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) { int Device::devoptab_diropen(void* fd, const char *path) {
auto device = (Device*)r->deviceData; auto dir = static_cast<Dir*>(fd);
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;
}
if (!std::strcmp(path, "/")) { if (!std::strcmp(path, "/")) {
dir->device = device; return 0;
r->_errno = 0;
return dirState;
} else { } else {
for (const auto& partition : device->partitions) { for (const auto& partition : this->partitions) {
if (path == "/" + partition.name) { if (path == "/" + partition.name) {
dir->collections = &partition.collections; dir->collections = &partition.collections;
r->_errno = 0; return 0;
return dirState;
} }
} }
} }
set_errno(r, ENOENT); return -ENOENT;
return NULL;
} }
int devoptab_dirreset(struct _reent *r, DIR_ITER *dirState) { int Device::devoptab_dirreset(void* fd) {
auto dir = static_cast<Dir*>(dirState->dirStruct); auto dir = static_cast<Dir*>(fd);
dir->index = 0; dir->index = 0;
return r->_errno = 0; return 0;
} }
int devoptab_dirnext(struct _reent *r, DIR_ITER *dirState, char *filename, struct stat *filestat) { int Device::devoptab_dirnext(void* fd, char *filename, struct stat *filestat) {
auto dir = static_cast<Dir*>(dirState->dirStruct); auto dir = static_cast<Dir*>(fd);
std::memset(filestat, 0, sizeof(*filestat));
if (!dir->collections) { if (!dir->collections) {
if (dir->index >= dir->device->partitions.size()) { if (dir->index >= this->partitions.size()) {
return set_errno(r, ENOENT); return -ENOENT;
} }
filestat->st_nlink = 1; filestat->st_nlink = 1;
filestat->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH; 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 { } else {
if (dir->index >= dir->collections->size()) { if (dir->index >= dir->collections->size()) {
return set_errno(r, ENOENT); return -ENOENT;
} }
const auto& collection = (*dir->collections)[dir->index]; 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++; dir->index++;
return r->_errno = 0; return 0;
} }
int devoptab_dirclose(struct _reent *r, DIR_ITER *dirState) { int Device::devoptab_dirclose(void* fd) {
auto dir = static_cast<Dir*>(dirState->dirStruct); auto dir = static_cast<Dir*>(fd);
std::memset(dir, 0, sizeof(*dir)); std::memset(dir, 0, sizeof(*dir));
return r->_errno = 0; return 0;
} }
int devoptab_lstat(struct _reent *r, const char *_path, struct stat *st) { int Device::devoptab_lstat(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));
st->st_nlink = 1; st->st_nlink = 1;
if (!std::strcmp(path, "/")) { if (!std::strcmp(path, "/")) {
st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH; st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
} else { } else {
for (const auto& partition : device->partitions) { for (const auto& partition : this->partitions) {
if (path == "/" + partition.name) { if (path == "/" + partition.name) {
st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH; st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
return r->_errno = 0; return 0;
} }
for (const auto& collection : partition.collections) { for (const auto& collection : partition.collections) {
if (path == "/" + partition.name + "/" + collection.name) { if (path == "/" + partition.name + "/" + collection.name) {
st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH; st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
st->st_size = collection.size; st->st_size = collection.size;
return r->_errno = 0; return 0;
} }
} }
} }
} }
return set_errno(r, ENOENT); return -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;
} }
Result MountXciInternal(const std::shared_ptr<yati::source::Base>& source, s64 size, const fs::FsPath& path, fs::FsPath& out_path) { 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); auto buffered = std::make_unique<common::LruBufferedData>(source, size);
yati::container::Xci xci{buffered.get()}; yati::container::Xci xci{buffered.get()};
yati::container::Xci::Root root; yati::container::Xci::Root root;
R_TRY(xci.GetRoot(root)); R_TRY(xci.GetRoot(root));
auto entry = std::make_unique<Entry>(); if (!common::MountReadOnlyIndexDevice(
entry->path = path; [&buffered, &root](const common::MountConfig& config) {
entry->devoptab = DEVOPTAB; return std::make_unique<Device>(std::move(buffered), root.partitions, config);
entry->devoptab.name = entry->name; },
entry->devoptab.deviceData = &entry->device; sizeof(File), sizeof(Dir),
entry->device.source = std::move(buffered); "XCI", out_path
entry->device.partitions = root.partitions; )) {
std::snprintf(entry->name, sizeof(entry->name), "xci_%zu", index); log_write("[XCI] Failed to mount %s\n", path.s);
std::snprintf(entry->mount, sizeof(entry->mount), "xci_%zu:/", index); R_THROW(0x1);
}
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);
R_SUCCEED(); R_SUCCEED();
} }
@@ -305,12 +220,6 @@ Result MountXciInternal(const std::shared_ptr<yati::source::Base>& source, s64 s
} // namespace } // namespace
Result MountXci(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path) { Result MountXci(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path) {
SCOPED_MUTEX(&g_mutex);
if (IsAlreadyMounted(path, out_path)) {
R_SUCCEED();
}
s64 size; s64 size;
auto source = std::make_shared<yati::source::File>(fs, path); auto source = std::make_shared<yati::source::File>(fs, path);
R_TRY(source->GetSize(&size)); 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) { 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); 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 } // namespace sphaira::devoptab

View File

@@ -10,7 +10,6 @@
#include <array> #include <array>
#include <memory> #include <memory>
#include <algorithm> #include <algorithm>
#include <sys/iosupport.h>
#include <zlib.h> #include <zlib.h>
namespace sphaira::devoptab { namespace sphaira::devoptab {
@@ -113,11 +112,6 @@ struct DirectoryEntry {
using FileTableEntries = std::vector<FileEntry>; using FileTableEntries = std::vector<FileEntry>;
struct Device {
std::unique_ptr<common::LruBufferedData> source;
DirectoryEntry root;
};
struct Zfile { struct Zfile {
z_stream z; // zlib stream. z_stream z; // zlib stream.
Bytef* buffer; // buffer that compressed data is read into. Bytef* buffer; // buffer that compressed data is read into.
@@ -126,7 +120,6 @@ struct Zfile {
}; };
struct File { struct File {
Device* device;
const FileEntry* entry; const FileEntry* entry;
Zfile zfile; // only used if the file is compressed. Zfile zfile; // only used if the file is compressed.
size_t data_off; // offset of the file data. size_t data_off; // offset of the file data.
@@ -134,7 +127,6 @@ struct File {
}; };
struct Dir { struct Dir {
Device* device;
const DirectoryEntry* entry; const DirectoryEntry* entry;
u32 index; u32 index;
}; };
@@ -194,44 +186,58 @@ void set_stat_file(const FileEntry* entry, struct stat *st) {
st->st_ctime = st->st_atime; st->st_ctime = st->st_atime;
} }
int set_errno(struct _reent *r, int err) { struct Device final : common::MountDevice {
r->_errno = err; Device(std::unique_ptr<common::LruBufferedData>&& _source, const DirectoryEntry& _root, const common::MountConfig& _config)
return -1; : 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) { if (!entry) {
return set_errno(r, ENOENT); return -ENOENT;
} }
if ((entry->flags & mmz_Flag_Encrypted) || (entry->flags & mmz_Flag_StrongEncrypted)) { if ((entry->flags & mmz_Flag_Encrypted) || (entry->flags & mmz_Flag_StrongEncrypted)) {
log_write("[ZIP] encrypted zip not supported\n"); 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) { if (entry->compression_type != mmz_Compression_None && entry->compression_type != mmz_Compression_Deflate) {
log_write("[ZIP] unsuported compression type: %u\n", entry->compression_type); log_write("[ZIP] unsuported compression type: %u\n", entry->compression_type);
return set_errno(r, ENOENT); return -ENOENT;
} }
mmz_LocalHeader local_hdr{}; mmz_LocalHeader local_hdr{};
auto offset = entry->local_file_header_off; auto offset = entry->local_file_header_off;
if (R_FAILED(device->source->Read2(&local_hdr, offset, sizeof(local_hdr)))) { if (R_FAILED(this->source->Read2(&local_hdr, offset, sizeof(local_hdr)))) {
return set_errno(r, ENOENT); return -ENOENT;
} }
if (local_hdr.sig != LOCAL_HEADER_SIG) { 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; 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? // todo: does a decs take prio over file header?
if (local_hdr.flags & mmz_Flag_DataDescriptor) { if (local_hdr.flags & mmz_Flag_DataDescriptor) {
mmz_DataDescriptor data_desc{}; mmz_DataDescriptor data_desc{};
if (R_FAILED(device->source->Read2(&data_desc, offset, sizeof(data_desc)))) { if (R_FAILED(this->source->Read2(&data_desc, offset, sizeof(data_desc)))) {
return set_errno(r, ENOENT); return -ENOENT;
} }
if (data_desc.sig != DATA_DESCRIPTOR_SIG) { if (data_desc.sig != DATA_DESCRIPTOR_SIG) {
return set_errno(r, ENOENT); return -ENOENT;
} }
offset += sizeof(data_desc); 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) { if (entry->compression_type == mmz_Compression_Deflate) {
auto& zfile = file->zfile; auto& zfile = file->zfile;
zfile.buffer_size = 1024 * 64; 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) { if (!zfile.buffer) {
return set_errno(r, ENOENT); return -ENOENT;
} }
// skip zlib header. // skip zlib header.
if (Z_OK != inflateInit2(&zfile.z, -MAX_WBITS)) { if (Z_OK != inflateInit2(&zfile.z, -MAX_WBITS)) {
free(zfile.buffer); std::free(zfile.buffer);
zfile.buffer = nullptr; zfile.buffer = nullptr;
return set_errno(r, ENOENT); return -ENOENT;
} }
} }
file->device = device;
file->entry = entry; file->entry = entry;
file->data_off = offset; 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); 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); inflateEnd(&file->zfile.z);
if (file->zfile.buffer) { if (file->zfile.buffer) {
free(file->zfile.buffer); std::free(file->zfile.buffer);
} }
} }
std::memset(file, 0, sizeof(*file)); return 0;
return r->_errno = 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); auto file = static_cast<File*>(fd);
len = std::min(len, file->entry->uncompressed_size - file->off); 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 (file->entry->compression_type == mmz_Compression_None) {
if (R_FAILED(file->device->source->Read2(ptr, file->data_off + file->off, len))) { if (R_FAILED(this->source->Read2(ptr, file->data_off + file->off, len))) {
return set_errno(r, ENOENT); return -ENOENT;
} }
} else if (file->entry->compression_type == mmz_Compression_Deflate) { } else if (file->entry->compression_type == mmz_Compression_Deflate) {
auto& zfile = file->zfile; 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. // check if we need to fetch more data.
if (!zfile.z.next_in || !zfile.z.avail_in) { if (!zfile.z.next_in || !zfile.z.avail_in) {
const auto clen = std::min(zfile.buffer_size, file->entry->compressed_size - zfile.compressed_off); 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))) { if (R_FAILED(this->source->Read2(zfile.buffer, file->data_off + zfile.compressed_off, clen))) {
return set_errno(r, ENOENT); return -ENOENT;
} }
zfile.compressed_off += clen; 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; len -= zfile.z.avail_out;
} else { } else {
log_write("[ZLIB] failed to inflate: %d %s\n", rc, zfile.z.msg); 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; 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); auto file = static_cast<File*>(fd);
// seek like normal. // 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); 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); auto file = static_cast<File*>(fd);
std::memset(st, 0, sizeof(*st));
set_stat_file(file->entry, 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) { int Device::devoptab_diropen(void* fd, const char *path) {
auto device = (Device*)r->deviceData; auto dir = static_cast<Dir*>(fd);
auto dir = static_cast<Dir*>(dirState->dirStruct);
std::memset(dir, 0, sizeof(*dir));
char path[FS_MAX_PATH]; const auto entry = find_dir_entry(this->root, path);
if (!common::fix_path(_path, path)) {
set_errno(r, ENOENT);
return NULL;
}
const auto entry = find_dir_entry(device->root, path);
if (!entry) { if (!entry) {
set_errno(r, ENOENT); return -ENOENT;
return NULL;
} }
dir->device = device;
dir->entry = entry; dir->entry = entry;
r->_errno = 0; return 0;
return dirState;
} }
int devoptab_dirreset(struct _reent *r, DIR_ITER *dirState) { int Device::devoptab_dirreset(void* fd) {
auto dir = static_cast<Dir*>(dirState->dirStruct); auto dir = static_cast<Dir*>(fd);
dir->index = 0; dir->index = 0;
return r->_errno = 0; return 0;
} }
int devoptab_dirnext(struct _reent *r, DIR_ITER *dirState, char *filename, struct stat *filestat) { int Device::devoptab_dirnext(void* fd, char *filename, struct stat *filestat) {
auto dir = static_cast<Dir*>(dirState->dirStruct); auto dir = static_cast<Dir*>(fd);
std::memset(filestat, 0, sizeof(*filestat));
u32 index = dir->index; u32 index = dir->index;
if (index >= dir->entry->dir_child.size()) { if (index >= dir->entry->dir_child.size()) {
index -= dir->entry->dir_child.size(); index -= dir->entry->dir_child.size();
if (index >= dir->entry->file_child.size()) { if (index >= dir->entry->file_child.size()) {
return set_errno(r, ENOENT); return -ENOENT;
} else { } else {
const auto& entry = dir->entry->file_child[index]; const auto& entry = dir->entry->file_child[index];
const auto rel_path = entry.path.substr(entry.path.find_last_of('/') + 1); 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++; dir->index++;
return r->_errno = 0; return 0;
} }
int devoptab_dirclose(struct _reent *r, DIR_ITER *dirState) { int Device::devoptab_dirclose(void* fd) {
auto dir = static_cast<Dir*>(dirState->dirStruct); auto dir = static_cast<Dir*>(fd);
std::memset(dir, 0, sizeof(*dir)); std::memset(dir, 0, sizeof(*dir));
return r->_errno = 0; return 0;
} }
int devoptab_lstat(struct _reent *r, const char *_path, struct stat *st) { int Device::devoptab_lstat(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));
st->st_nlink = 1; 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; 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); set_stat_file(entry, st);
} else { } else {
log_write("[ZIP] didn't find in lstat\n"); 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 { auto BuildPath(const std::string& path) -> std::string {
if (path.starts_with('/')) { if (path.starts_with('/')) {
return path; return path;
@@ -600,48 +562,13 @@ Result ParseZip(common::LruBufferedData* source, s64 size, FileTableEntries& out
R_SUCCEED(); 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 } // namespace
Result MountZip(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path) { Result MountZip(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path) {
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); auto source = std::make_shared<yati::source::File>(fs, path);
s64 size; s64 size;
R_TRY(source->GetSize(&size)); R_TRY(source->GetSize(&size));
auto buffered = std::make_unique<common::LruBufferedData>(source, size); auto buffered = std::make_unique<common::LruBufferedData>(source, size);
FileTableEntries table_entries; FileTableEntries table_entries;
@@ -651,44 +578,18 @@ Result MountZip(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path) {
DirectoryEntry root; DirectoryEntry root;
Parse(table_entries, root); Parse(table_entries, root);
auto entry = std::make_unique<Entry>(); if (!common::MountReadOnlyIndexDevice(
entry->path = path; [&buffered, &root](const common::MountConfig& config) {
entry->devoptab = DEVOPTAB; return std::make_unique<Device>(std::move(buffered), root, config);
entry->devoptab.name = entry->name; },
entry->devoptab.deviceData = &entry->device; sizeof(File), sizeof(Dir),
entry->device.source = std::move(buffered); "ZIP", out_path
entry->device.root = root; )) {
std::snprintf(entry->name, sizeof(entry->name), "zip_%zu", index); log_write("[ZIP] Failed to mount %s\n", path.s);
std::snprintf(entry->mount, sizeof(entry->mount), "zip_%zu:/", index); R_THROW(0x1);
}
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);
R_SUCCEED(); 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 } // namespace sphaira::devoptab

View File

@@ -14,6 +14,25 @@ HashStr hexIdToStrInternal(auto id) {
return str; 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 } // namespace
HashStr hexIdToStr(FsRightsId id) { HashStr hexIdToStr(FsRightsId id) {
@@ -28,4 +47,12 @@ HashStr hexIdToStr(NcmContentId id) {
return hexIdToStrInternal(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 } // namespace sphaira::utils

View File

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

View File

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

View File

@@ -16,6 +16,8 @@
#include "utils/thread.hpp" #include "utils/thread.hpp"
#include "ui/progress_box.hpp" #include "ui/progress_box.hpp"
#include "ui/menus/game_menu.hpp"
#include "app.hpp" #include "app.hpp"
#include "i18n.hpp" #include "i18n.hpp"
#include "log.hpp" #include "log.hpp"
@@ -873,6 +875,9 @@ Yati::~Yati() {
} }
App::SetAutoSleepDisabled(false); 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) { 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