Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f2d0e72f2 | ||
|
|
c9552f9785 | ||
|
|
444ff3e2d1 | ||
|
|
7d56c8a381 | ||
|
|
da051f8d8f | ||
|
|
81e6bc5833 | ||
|
|
ca5ea827b2 | ||
|
|
b700fff9ac | ||
|
|
81741364a7 | ||
|
|
faebc42f0d | ||
|
|
63e11ca377 | ||
|
|
54a2215e04 | ||
|
|
5edc3869cd | ||
|
|
a772d660f3 | ||
|
|
3c504cc85d | ||
|
|
0a2c16db0c | ||
|
|
2bd84c8d5a | ||
|
|
7cd668efb7 | ||
|
|
a6265c3089 | ||
|
|
a2300c1a96 | ||
|
|
3dae3f9173 | ||
|
|
63c420d5d8 | ||
|
|
a94c6bb581 | ||
|
|
9fe0044a65 | ||
|
|
c05ce5eff4 | ||
|
|
a019103ed5 | ||
|
|
50e55f4fca | ||
|
|
0706683690 | ||
|
|
9cdb77bafa | ||
|
|
b476c54825 | ||
|
|
8b2e541b1d | ||
|
|
931531e799 | ||
|
|
1695d69aa3 | ||
|
|
217bd3bed3 | ||
|
|
384e8794bf | ||
|
|
61b398a89a | ||
|
|
ba78fd0dc5 | ||
|
|
43969a773e | ||
|
|
6e1eabbe0f | ||
|
|
b99d1e5dea | ||
|
|
6ce566aea5 | ||
|
|
a4209961e2 | ||
|
|
181ff3f2bf | ||
|
|
b85b522643 | ||
|
|
5158e264c0 | ||
|
|
fd67da0527 | ||
|
|
7bdec8457f | ||
|
|
bc75c9a89f |
8
.github/workflows/build_presets.yml
vendored
8
.github/workflows/build_presets.yml
vendored
@@ -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:
|
||||||
|
|||||||
4
.github/workflows/python-usb-export.yml
vendored
4
.github/workflows/python-usb-export.yml
vendored
@@ -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
|
||||||
|
|||||||
4
.github/workflows/python-usb-install.yml
vendored
4
.github/workflows/python-usb-install.yml
vendored
@@ -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
3
.gitignore
vendored
@@ -27,3 +27,6 @@ out
|
|||||||
usb_test/
|
usb_test/
|
||||||
__pycache__
|
__pycache__
|
||||||
usb_*.spec
|
usb_*.spec
|
||||||
|
|
||||||
|
CMakeUserPresets.json
|
||||||
|
build_patreon.sh
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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{};
|
||||||
|
|
||||||
|
|||||||
@@ -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))
|
|
||||||
|
|||||||
@@ -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{};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <switch.h>
|
|
||||||
|
|
||||||
namespace sphaira::fatfs {
|
|
||||||
|
|
||||||
Result MountAll();
|
|
||||||
void UnmountAll();
|
|
||||||
|
|
||||||
} // namespace sphaira::fatfs
|
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ enum class Type {
|
|||||||
Sha1,
|
Sha1,
|
||||||
Sha256,
|
Sha256,
|
||||||
Null,
|
Null,
|
||||||
Deflate,
|
|
||||||
Zstd,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct BaseSource {
|
struct BaseSource {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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};
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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"; };
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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{};
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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{};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
10908
sphaira/source/ff16/ffunicode.c
Normal file
10908
sphaira/source/ff16/ffunicode.c
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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)++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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), ×tamp, &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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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)) {
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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); \
|
||||||
\
|
\
|
||||||
|
|||||||
342
sphaira/source/utils/devoptab.cpp
Normal file
342
sphaira/source/utils/devoptab.cpp
Normal 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
|
||||||
@@ -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
@@ -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"
|
||||||
717
sphaira/source/utils/devoptab_ftp.cpp
Normal file
717
sphaira/source/utils/devoptab_ftp.cpp
Normal 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
|
||||||
499
sphaira/source/utils/devoptab_game.cpp
Normal file
499
sphaira/source/utils/devoptab_game.cpp
Normal 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
|
||||||
443
sphaira/source/utils/devoptab_http.cpp
Normal file
443
sphaira/source/utils/devoptab_http.cpp
Normal 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..></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
|
||||||
311
sphaira/source/utils/devoptab_mounts.cpp
Normal file
311
sphaira/source/utils/devoptab_mounts.cpp
Normal 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
|
||||||
@@ -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
|
||||||
|
|||||||
416
sphaira/source/utils/devoptab_nfs.cpp
Normal file
416
sphaira/source/utils/devoptab_nfs.cpp
Normal 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, ¤t_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
|
||||||
@@ -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, ×tamp);
|
||||||
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, ×tamp](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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
543
sphaira/source/utils/devoptab_sftp.cpp
Normal file
543
sphaira/source/utils/devoptab_sftp.cpp
Normal 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
|
||||||
402
sphaira/source/utils/devoptab_smb2.cpp
Normal file
402
sphaira/source/utils/devoptab_smb2.cpp
Normal 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, ¤t_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
|
||||||
284
sphaira/source/utils/devoptab_vfs.cpp
Normal file
284
sphaira/source/utils/devoptab_vfs.cpp
Normal 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
|
||||||
654
sphaira/source/utils/devoptab_webdav.cpp
Normal file
654
sphaira/source/utils/devoptab_webdav.cpp
Normal 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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user