116 Commits

Author SHA1 Message Date
ITotalJustice
d50bcb650f fix crash if nro has corrupted asset entry, bump version for new release 0.10.2 -> 0.10.3
the nro that caused this was ClkrstQuery.nro

fixes #141
2025-05-21 17:29:18 +01:00
⭐️NINIKA⭐️
4244be9592 Fix handling of unicode filenames in usb_install_pc.py (#143) 2025-05-20 12:45:59 +01:00
Ny'hrarr
a7fc19e28a Pt patches (#140)
* Update pt.json

* Translate new keys

* Translate FTP, USB and GameCard menus

* Update pt.json

* Update pt.json

* Add missing comma and translate more entries

* Fix case

* Update pt.json

* Update pt.json
2025-05-20 09:00:39 +01:00
ITotalJustice
cf192fca85 fix hbmenu not being updated due to faulty string compare, bump version 0.10.1 -> 0.10.2 2025-05-19 20:34:22 +01:00
xHR
041bb2bbe5 Added Ukranian language (#139) 2025-05-19 19:57:15 +01:00
ITotalJustice
df558d5dcc bump version for new release 0.10.0 -> 0.10.1 2025-05-19 17:06:49 +01:00
ITotalJustice
33de03a923 fix sd card dumps due to the folder not being created. 2025-05-19 17:04:49 +01:00
ITotalJustice
1000b9c8ec fix sphaira not detecting latest update as we went from 0.9 to 0.10. 2025-05-19 16:23:45 +01:00
ITotalJustice
74fddecebc bump version for new release 0.9.1 -> 0.10.0 2025-05-19 16:09:33 +01:00
ITotalJustice
a64d4dce7a remove ns event from games menu (see below).
turns out that the event is auto cleared when waited.
this meant that if sphaira handled the event before qlaunch got chance to handle it,
then qlaunch won't update when app records changed.

this can result in a gamecard not being mounted, deleted games still apearing, installed games
not being displayed etc...
2025-05-19 16:07:24 +01:00
ITotalJustice
5daca4354c add support for webdav uploads by creating missing folders, game now dumps to title/title[id].nsp 2025-05-19 16:00:03 +01:00
ITotalJustice
da9235f58e fix upload url path not being encoded, add seek api for uploads. 2025-05-19 12:06:43 +01:00
ITotalJustice
bd6566524c enable ftp in the ftp menu if it isn't already enabled. 2025-05-19 08:32:05 +01:00
ITotalJustice
eadc46b0e4 add support for file uploads in the file browser, optimise curl single thread download.
- curl now keeps the handle alive for single threaded downloads, rather than creating it each time.
2025-05-18 23:00:51 +01:00
ITotalJustice
71df5317be add game dump uploading, fix download progress using u32 instead of s64, add progress and title for usb game dump.
- added support for custom upload locations, set in /config/sphaira/locations.ini
- add support for various auth options for download/upload (port, pub/priv key, user/pass, bearer).
2025-05-18 20:30:04 +01:00
ITotalJustice
bd7eadc6a0 add game dumping, add game transfer (switch2switch) via usb, add multi game selecting, fix bugs (see below).
- added more es commands.
- fixed usb install potential hang if the exit command is sent, but the client stops responding (timeout is now 3s).
- added multi select to the games menu.
- added game dumping.
- added switch2switch support by having a switch act as a usb client to transfer games.
- replace std::find with std::ranges (in a few places).
- fix rounding of icon in progress box being too round.
- fix file copy helper in progress box not updating the progress bar.
2025-05-18 13:46:10 +01:00
ITotalJustice
544272925d fix IsEmunand() failing due to the paths not being page aligned. 2025-05-15 16:13:35 +01:00
ITotalJustice
70a31be134 don't fail if the control nca cannot be parsed during install, as it may depend on ticket not yet installed. 2025-05-15 15:25:03 +01:00
ITotalJustice
55ae2a63d9 fix installing failing during setup if prod.keys isn't found. 2025-05-15 15:14:02 +01:00
ITotalJustice
5a53947a3e fix crash in list layout caused by rendering all previous hidden entries. 2025-05-14 18:00:56 +01:00
BIGBIGSUI
3bbb5ccb3c Update zh.json (#136)
A latest zh.json. Hope it helps.
2025-05-14 00:29:04 +01:00
ITotalJustice
83472f1020 fix game menu forcefully disabled manual loading of control data.
this was done during testing / benchmarking, but i forgot to undo this.
2025-05-14 00:06:36 +01:00
ITotalJustice
0167bf034c gc menu now tries to load control data from ns cache before manually loading.
on fw 19 and below, loading from cache takes ~5ms, whereas manually loading takes ~20ms.
manually loading is still faster than relying on ns to load control from storage (~50ms).
2025-05-14 00:04:47 +01:00
ITotalJustice
35abe363a6 optimise theme meta loading. 2025-05-13 23:52:34 +01:00
ITotalJustice
97d3fd396e optimise game menu for fw 20
- loading the control data is ran on its own thread, it does not block the main thread. allows for smooth scrolling like nintendos home menu.
- on fw20+, sphaira manually parses the control data, rather than using ns. manually parsing takes 20-40ms, which is faster than ms which can take 50-500ms.
- on fw19 and below, if the control data is not in ns cache, sphaira will manually parse the data as its twice as fast as ns. You can see how fast this is by loading the gamecard menu as that manually parses everything, and it loads the gamecard faster than the home menu
2025-05-13 23:51:06 +01:00
ITotalJustice
b98ccb927e fix appstore status icons no longer being rounded (bug added in e279a70) 2025-05-11 20:19:43 +01:00
ITotalJustice
db23f072a2 add sysmmc / emummc install enable options.
allows the user to enable installs for one config and disable it for the other.
by default, it will load the install option found in the config, if found.
otherwise, it will load from the new config option.
2025-05-11 20:14:34 +01:00
ITotalJustice
4d3d7e81d4 update libhaze to silence gcc 15 warning and run on core2 instead of core0. 2025-05-11 03:45:48 +01:00
ITotalJustice
441807bc53 fix building for gcc 15 2025-05-11 03:00:04 +01:00
ITotalJustice
20e2d85843 remove bubbles, no one likes easter eggs apparently.
fixes #138
2025-05-11 02:41:56 +01:00
ITotalJustice
e279a70606 add layout options to grid based menues. 2025-05-11 02:39:03 +01:00
ITotalJustice
5d9e24af31 use ns application event to detect when to re-scan for record changes. 2025-05-04 20:42:57 +01:00
Ny'hrarr
078627e07b Update pt.json (#133)
* Update pt.json

* Translate new keys

* Translate FTP, USB and GameCard menus
2025-05-03 22:27:31 +01:00
ITotalJustice
365ae2d0cb fix freeze if the usb menu is closed whilst a usb cable is not connected, 0.9.0 -> 0.9.1 2025-05-03 21:15:21 +01:00
ITotalJustice
5b6e09b926 bump version for new release 0.8.2 -> 0.9.0 2025-05-03 18:01:49 +01:00
ITotalJustice
7072647611 reduce usb install exit latency by waiting on a cancel uevent, rather than relying on a timeout. 2025-05-03 17:17:53 +01:00
ITotalJustice
30cf4826f8 add code for calculating games size, stubbed for now as the ns calls are too slow to be usable. 2025-05-03 15:30:40 +01:00
ITotalJustice
ca47fc1f89 add (limited) sort options to game menu.
getting the list of title_ids is very fast (less than 1ms), however parsing the control info, such as title names
is very slow.
depending on how many games the user has, blocking until we read all control info can take several seconds...
we would only need to block if the user wants to sort by name.
normally, we lazy load the control data, so we don't suffer from slow load times at all.
i decided that its not worth slowing the whole system down just to give the option to sort by name.
2025-05-03 15:08:25 +01:00
ITotalJustice
16a2c84edd simplify right-side shortcuts impl, add gamecard and themezer to shortcut list, fix l2/r2 using wrong icons, sort l2/r2 so l2 displays first.
some other changes:
- shorten the next page and prev page to just next/prev in themezer.
- remove misc shortcut name. the function itself still exists.
2025-05-03 14:39:20 +01:00
ITotalJustice
df5e27dd06 fix filebrowser crash caused when trying to select all files (L2) whilst a hidden file/folder exists.
this crash was found by @WE1ZARD.
to trigger it, press L2 in the filebrowser whilst a hidden file exists and the hide hidden is enabled.
the was due to GetEntry(i) internally using m_entries_current, and the select all was using the index from m_entries_current.
this would result in an index that goes oob, and as its a write, it crashes.
2025-05-03 13:25:09 +01:00
ITotalJustice
d95226f8c0 i18n::get should accept a string_view rather than char*, simplifies calling. 2025-05-03 13:25:09 +01:00
ITotalJustice
164fec5b73 fix right-side shortcut not displaying the correct name (again) when using translations. 2025-05-03 13:25:09 +01:00
glitched_nx
8dad96f39f Update de.json (#130)
Commit changes to language file (de.json), with many improvements and corrections.

The changes include:
- Modification and correction of existing translations for greater clarity and accuracy.
- Updated translations to match the terminology for homebrew and Nintendo Switch UI elements.
- Added new 'de' translation for the “ 12h time format” setting function.
2025-05-02 22:48:48 +01:00
ITotalJustice
2244e73c53 change "yes, no" for boolean options to "on, off", to match N's sidebar. 2025-05-02 00:57:11 +01:00
ITotalJustice
456cb02d2a [breaking change] update forwarder id generation, add "hide forwarders" to game menu. 2025-05-02 00:52:08 +01:00
ITotalJustice
f310704472 bump nca key generation values for fw 20.0.0 2025-05-02 00:02:09 +01:00
ITotalJustice
96e5a7081b clip rect and text drawing that go offscreen.
this is already handled by the gpu, but cpu side still has to do some work.
this wasn't a performance issue (we only use 4%) but its a free win, so we might as well.
2025-05-01 23:49:01 +01:00
ITotalJustice
1c93e18822 replace all "enabled" and "disabled" options with the default "On" and "Off". 2025-05-01 22:10:47 +01:00
ITotalJustice
ac152454f0 fix menu shortcut setting translated names for the config. 2025-05-01 22:00:40 +01:00
ITotalJustice
7851f7f400 add option to extract zip to root, better name extract / compress options. 2025-05-01 18:28:35 +01:00
ITotalJustice
2b561dd438 add option to change the right-side menu. 2025-05-01 18:06:10 +01:00
ITotalJustice
3545f557fc add scrolling text to popup list, handling clipping inside scrolling text, game menu changes
- added delete entity in game menu
- added list meta records to game menu
2025-05-01 15:14:50 +01:00
ITotalJustice
8dfb9b9ba6 reduce time for scrolling text from 1.5s to 1s. 2025-05-01 00:04:32 +01:00
ITotalJustice
7cf36cd25f reduces rounding of icons in grid based menus (15 -> 5). 2025-04-30 23:59:05 +01:00
ITotalJustice
c53692022b add scrolling text to all grid based menus.
fixes #122
2025-04-30 23:56:40 +01:00
ITotalJustice
0f3b7da0b2 fix memleak when deleting homebrew, add game menu. 2025-04-30 22:45:52 +01:00
ITotalJustice
e22daefb08 slightly round edges for progress and option box, add image support to option boxes 2025-04-30 18:19:13 +01:00
ITotalJustice
6fb5319da3 bump version for new release 0.8.1 -> 0.8.2 2025-04-30 17:21:04 +01:00
ITotalJustice
6970fec554 irs connect to first available handle, irs display connected pad in the title. 2025-04-30 17:16:59 +01:00
ITotalJustice
36be56647f Revert "remove IRS menu"
This reverts commit 1dafa2748c.
2025-04-30 17:05:44 +01:00
ITotalJustice
cca6326314 filebrowser add select al option by pressing L2 2025-04-30 16:55:33 +01:00
ITotalJustice
9176c6780a filebrowser move install forwarder option out of the advanced menu. 2025-04-30 16:49:19 +01:00
ITotalJustice
b1a6b12cf3 add zip extraction, add zip creation, themezer now displays the file name its extracting. 2025-04-30 16:42:05 +01:00
ITotalJustice
c7cc11cc98 only add etag is dst file already exists, enable curl --compressed option.
curl/libcurl does not send Accept-Encoding by default.
many servers support sending compressed versions of files, to speed up transfers.
this is ideal for the switch as its io is shit, but the cpu is mostly idle (4% cpu usage for sphaira).

github and appstore support sending gzip json files, themezer doesn't seem to.
2025-04-30 00:40:04 +01:00
ITotalJustice
ec4b96b95d remove stale etag if the server stops sending etags back
workaround for appstore images which stopped sending etags back.
2025-04-29 22:41:33 +01:00
ITotalJustice
a2e343daa7 improve popup_list to highlight the currently selected item. 2025-04-29 22:40:32 +01:00
BIGBIGSUI
b811c9e3cd Update zh.json (#129)
A latest zh.json. Hope it will be helpful
2025-04-29 20:35:05 +01:00
ITotalJustice
8ffaa56bc3 bump version for new release 0.8.0 -> 0.8.1 2025-04-29 20:03:23 +01:00
ITotalJustice
eca3358e57 add option to download default music. 2025-04-29 20:01:51 +01:00
ITotalJustice
757e380e08 play sound effect when gamecard is inserted. 2025-04-29 19:23:37 +01:00
ITotalJustice
6c1b5de932 label the shortcut for misc 2025-04-29 19:06:33 +01:00
ITotalJustice
d79ac126f7 remove all strings and error codes in error_box.cpp, reduce binary by a further 60k. 2025-04-29 18:56:55 +01:00
ITotalJustice
2d7763444e remove Web menu
the web browser on the switch is really bad, it shouldnt be used.
i am removing this menu because its another option that gets in the way of other options, and code bloat.
2025-04-29 18:42:32 +01:00
ITotalJustice
1dafa2748c remove IRS menu
i added the irs menu when i wanted to mess around with the sensor on the joycon.
since then, i have used it a total of 0 times, and i don't think any users use it either.
2025-04-29 18:37:43 +01:00
ITotalJustice
9f7bf9581c add boost mode option for installing 2025-04-29 18:33:02 +01:00
ITotalJustice
8f39acbaa2 replace usb protocol with tinfoils protocol, in order to support applications supporting said protocol.
- replace the python script with the one included with tinfoil, minor changes such as changing the supported extension,
  removing unused imports.
- tested with the included script, fluffy and ns-usbloader on linux.
  a user was unable to get it working on mac however...
- added build instructions to the readme, i think they're correct.
- added install instructions to the readme.
2025-04-29 18:11:07 +01:00
ITotalJustice
81469d0ac9 remove PageAlignedVector from yati as it's no longer needed due to previous commit.
the previous commit changed usb transfers to always transfer to/from page aligned buffers.
i wanted to keep the commits seperate so that its easier revert or git bisect later on, if needed.
2025-04-29 14:19:37 +01:00
ITotalJustice
1eae35f072 simplify the usb transfer process by using an aligned buffer to transfer to/from. 2025-04-29 14:17:12 +01:00
ITotalJustice
5b82e07b1c fix building due to previous commit 2025-04-29 13:08:32 +01:00
ITotalJustice
73886c28ae add gc event waiting, fix control nca mounting, better skip nca support.
- gamecards now wait for an event to change, rather than polling each frame.
  this reduces cpu load on core 3 slightly (3-4% less).
- my understanding of fsOpenFileSystemWithId() was wrong, i thought it used the app_id for the id param.
  turns out it needs the program id (found in the nca header), this is why mounting some control ncas
  would fail.
  fs (and ncm) have a call in 17+ to get the program id, it does so by parsing the nca header.
  in yati, we already have the header so we can avoid the call.
  for the gamecard menu, we don't. so we can parse the nca header, or use the id offset (which we already have)
  to form the program id.
- std::find_if in yati now takes args by ref rather than by value, avoid quite large copies.
- stream installs can now parse the control nca.
- if an nca is already installed, it is now skipped. this is regardless to whether it is not in ncm db.
- nca skipping is technically supported for stream installs, however it is disabled for now as there needs
  to be a way to allow for the stream to continue reading and discarding data until the stream has finished.
  currently, if a ftp (stream) install is skipped, it will close the progress box and cause spahira to hang.
  this is because sphaira expects the stream to only be closed upon all data being read, so there's nothing more
  to process.
- renamed the title_id field in nca header to program_id.
2025-04-29 12:47:38 +01:00
ITotalJustice
eea09f6e57 [appstore] make author search case insensitive. 2025-04-28 22:06:44 +01:00
ITotalJustice
282c6e5493 bump version for release 0.7.0 -> 0.8.0 2025-04-27 21:01:45 +01:00
ITotalJustice
2c2f602d14 add gc_menu, add progress, icon, time remaining to progress bar (see full commit message).
- fix ignore distribution bit doing nothing.
- fix yati failing to parse control nca causing the transfer to abort.
- yati now uses ncm rather than ns to get the latest app version.
- improve ui::list input handling (it handles directional buttons now).
- progress bar displays speed and time remaining.
- added gc menu (taken from my gc installer nx and gci).
2025-04-27 20:01:13 +01:00
ITotalJustice
f7f1254699 Merge pull request #128 from ITotalJustice/stream_installs
Stream installs (FTP), and many fixes
2025-04-23 01:02:26 +01:00
ITotalJustice
90f8a62823 display useful info in ftp menu (ip, port, user, pass, ssid, passphrase) 2025-04-23 01:00:36 +01:00
ITotalJustice
e2a1c8b5e3 fix yati not setting correct version, add support for using zip name when creating forwarder, remove some dead code.
fixes #126
fixes #127
2025-04-22 23:15:16 +01:00
ITotalJustice
21f6f4b74d [skip ci] fix file assoc always using internal name, fix menu showing wrong time
fixes #126
2025-04-22 00:08:26 +01:00
ITotalJustice
75d3b3ee0d [skip-ci] initial support for stream installs, add ftp installs.
do NOT build or release binaries of this version, it is not complete and there will be dragons.
2025-04-21 23:23:59 +01:00
ITotalJustice
0dde379932 don't return from usb menu on error, wait until the user presses B 2025-04-21 13:33:36 +01:00
ITotalJustice
9800bbecdf add basic support for gamecard installing 2025-04-21 13:30:46 +01:00
ITotalJustice
60e915c255 enable screenshot permissions in applet mode. 2025-04-21 12:40:37 +01:00
ITotalJustice
786f8a42fa send file name and size via usb, add requirements.txt for usb.py 2025-04-21 01:41:20 +01:00
ITotalJustice
5a4a0f75f2 add support for mame and neogeo, as well as alias for rom folder names 2025-04-20 22:03:53 +01:00
ITotalJustice
5aca92a2cc fix usb menu name being set as irs (copy-paste bug). 2025-04-20 18:11:17 +01:00
ITotalJustice
7471885119 bump version for release 0.6.3 -> 0.7.0 2025-04-20 18:08:55 +01:00
ITotalJustice
5038fb0c28 add basic usb install support (see commit message).
transfers seems to work, although i have done very little testing.
i plan to extend the usb script so that it supports normal usb transfers, such as uploading and
downloading files to and from the switch.

however, most users are likely better off using mtp for said transfers.

the usb transfer code was taken from Haze, which is part of Atmosphere.
2025-04-20 18:04:35 +01:00
ITotalJustice
ff9f493460 fix skwbd numpa not showing 2025-04-20 14:18:02 +01:00
ITotalJustice
89e82927ee add basic support for title installing 2025-04-20 14:12:12 +01:00
ITotalJustice
651d9fa495 remove theme shuffle option
fixes #121
2025-04-19 22:55:08 +01:00
ITotalJustice
3141100457 fix themezer search...by actually *doing* the search.
fixes #111
2025-04-19 22:50:56 +01:00
ITotalJustice
6b4e81c935 bump version for new release 0.6.3 2025-04-18 13:18:14 +01:00
ITotalJustice
e243d5b64e fix themezer 2025-04-18 13:16:17 +01:00
shadow2560
252cd0cee6 Add 12 our time string in translation files and translated it for French language. (#120)
Signed-off-by: shadow2560 <24191064+shadow2560@users.noreply.github.com>
2025-04-09 18:05:14 +01:00
therealbungus
14abcc50b5 add 12 hour clock (#113) 2025-04-08 12:04:22 +01:00
BIGBIGSUI
134aadad5a Update zh.json (#110)
This is my updated zh.json file, I hope it helps.
2025-04-08 09:39:31 +01:00
Ny'hrarr
a56bc9e4fa Update pt.json (#109)
* Update pt.json

* Update pt.json
2025-04-08 09:39:18 +01:00
shadow2560
5bd466a9b6 Update french language. (#108)
Signed-off-by: shadow2560 <24191064+shadow2560@users.noreply.github.com>
2025-04-08 09:38:58 +01:00
LNLenost
16c58512ec Edited README, updated Italian translations (#117)
* Update README.md

* Update README.md

* Italian translations 1/2

* Italian translations 2/2
2025-04-08 09:38:32 +01:00
ITotalJustice
b1b0b13f2a fix filebrowser FsDirOpenMode_NoFileSize being passed
looks like fs ignores this flag as it was reporting the filesize regardless. however, if a file is created using
FsCreateOption_BigFile, then the filesize would return 0.
2025-01-22 13:01:38 +00:00
ITotalJustice
03e77faf06 draw "applet mode" text using error colour
fixes #104
2025-01-22 12:59:54 +00:00
ITotalJustice
7e381924ab fix forwarder creation bug on ams 1.7.1, fix cmake project version bug
i was checking against ams 1.7.1, however the change was introduced in ams 1.8.0.
fixes #106

as for cmake, i have no idea why this bug happens, but sometimes it will use the project version for hbl.
which results in sphaira having a version number of 3.0.0.
this only seemed to happen when building the zip with no current build.
2025-01-22 12:54:30 +00:00
spkats1
5763610e54 [Theme] alt icons + theme (#102)
* adding "sp icons"
2025-01-22 12:49:15 +00:00
HenryBaby
49956a3f84 Updated se.json with the latest added string. (#103)
Co-authored-by: ITotalJustice <47043333+ITotalJustice@users.noreply.github.com>
2025-01-22 12:48:00 +00:00
Yorunokyujitsu
b2915a8142 add new string, update ko, ja.json (#101) 2025-01-22 12:47:04 +00:00
ITotalJustice
e002aa9ec2 only disable audio in applet mode if an app is suspended, bump version. 2025-01-17 03:52:32 +00:00
ITotalJustice
0aaf460dbf bump version for release (i forgot to do this before making a new release...) 2025-01-16 21:48:40 +00:00
145 changed files with 12219 additions and 2924 deletions

3
.gitignore vendored
View File

@@ -11,6 +11,7 @@ old_code
created_ncas
assets/romfs/shaders
.vscode/settings.json
.idea
info/
romfs/shaders
assets/unused
@@ -22,3 +23,5 @@ libs/tweeny
compile_commands.json
out
usb_test/

View File

@@ -1,12 +1,12 @@
# sphaira
# Sphaira
A homebrew menu for the switch.
A homebrew menu for the Nintendo Switch.
[See the gbatemp thread for more details / discussion](https://gbatemp.net/threads/sphaira-hbmenu-replacement.664523/).
[See the GBATemp thread for more details / discussion](https://gbatemp.net/threads/sphaira-hbmenu-replacement.664523/).
[We have now have a Discord server!](https://discord.gg/8vZBsrprEc). Please use the issues tab to report bugs, as it is much easier for me to track.
## showcase
## Showcase
| | |
:-------------------------:|:-------------------------:
@@ -15,29 +15,29 @@ A homebrew menu for the switch.
![Img](assets/screenshots/2024121522505300-879193CD6A8B96CD00931A628B1187CB.jpg) | ![Img](assets/screenshots/2024121522502300-879193CD6A8B96CD00931A628B1187CB.jpg)
![Img](assets/screenshots/2024121523033200-879193CD6A8B96CD00931A628B1187CB.jpg) | ![Img](assets/screenshots/2024121523070300-879193CD6A8B96CD00931A628B1187CB.jpg)
## bug reports
## Bug reports
for any bug reports, please use the issues tab and explain in as much detail as possible!
For any bug reports, please use the issues tab and explain in as much detail as possible!
please include:
Please include:
- CFW type (i assume Atmosphere, but someone out there is still using Rajnx)
- CFW version
- FW version
- The bug itself and how to reproduce it
- CFW type (i assume Atmosphere, but someone out there is still using Rajnx);
- CFW version;
- FW version;
- The bug itself and how to reproduce it.
## ftp
## FTP
ftp can be enabled via the network menu. It uses the same config as ftpsrv `/config/ftpsrv/config.ini`. [See here for the full list
FTP can be enabled via the network menu. It uses the same config as ftpsrv `/config/ftpsrv/config.ini`. [See here for the full list
of all configs available](https://github.com/ITotalJustice/ftpsrv/blob/master/assets/config.ini.template).
## mtp
## MTP
mtp can be enabled via the network menu.
MTP can be enabled via the Network menu.
## file assoc
## File association
sphaira has file assoc support. lets say your app supports loading .png files, then you could write an assoc file, then when using the file browser, clicking on a .png file will launch your app along with the .png file as argv[1]. This was primarly added for rom loading support for emulators / frontends such as retroarch, melonds, mgba etc.
Sphaira has file association support. Let's say your app supports loading .png files, then you could write an association file, then when using the file browser, clicking on a .png file will launch your app along with the .png file as argv[1]. This was primarly added for rom loading support for emulators / frontends such as RetroArch, MelonDS, mGBA etc.
```ini
[config]
@@ -45,9 +45,43 @@ path=/switch/your_app.nro
supported_extensions=jpg|png|mp4|mp3
```
the `path` field is optional. if left out, it will use the name of the ini to find the nro. For example, if the ini is called mgba.ini, it will try to find the nro in /switch/mgba.nro and /switch/folder/mgba.nro.
The `path` field is optional. If left out, it will use the name of the ini to find the nro. For example, if the ini is called mgba.ini, it will try to find the nro in /switch/mgba.nro and /switch/folder/mgba.nro.
see `assets/romfs/assoc/` for more examples of file assoc entries
See `assets/romfs/assoc/` for more examples of file assoc entries.
## Installing (applications)
Sphaira can install applications (nsp, xci, nsz, xcz) from various sources (sd card, gamecard, ftp, usb).
For informantion about the install options, [see the wiki](https://github.com/ITotalJustice/sphaira/wiki/Install).
### Usb (install)
The USB protocol is the same as tinfoil, so tools such as [ns-usbloader](https://github.com/developersu/ns-usbloader) and [fluffy](https://github.com/fourminute/Fluffy) should work with sphaira. You may also use the provided python script found [here](tools/usb_install_pc.py).
### Ftp (install)
Once you have connected your ftp client to your switch, you can upload files to install into the `install` folder.
## Building from source
You will first need to install [devkitPro](https://devkitpro.org/wiki/Getting_Started).
Next you will need to install the dependencies:
```sh
sudo pacman -S switch-dev deko3d switch-cmake switch-curl switch-glm switch-zlib
```
Once devkitPro and all dependencies are installed, you can now build sphaira.
```sh
git clone https://github.com/ITotalJustice/sphaira.git
cd sphaira
cmake --preset MinSizeRel
cmake --build --preset MinSizeRel
```
The output will be found in `build/MinSizeRel/sphaira.nro`
## Credits
@@ -59,7 +93,7 @@ see `assets/romfs/assoc/` for more examples of file assoc entries
- deko3d-nanovg
- libpulsar
- minIni
- gbatemp
- GBATemp
- hb-appstore
- haze
- everyone who has contributed to this project!
- Everyone who has contributed to this project!

View File

@@ -2,3 +2,4 @@
path=/retroarch/cores/fbneo_libretro_libnx.nro
supported_extensions=zip|7z|cue|ccd
database=FBNeo - Arcade Games
use_base_name=true

View File

@@ -2,3 +2,4 @@
path=/retroarch/cores/mame2000_libretro_libnx.nro
supported_extensions=zip|7z
database=MAME 2000
use_base_name=true

View File

@@ -2,3 +2,4 @@
path=/retroarch/cores/mame2003_libretro_libnx.nro
supported_extensions=zip
database=MAME 2003
use_base_name=true

View File

@@ -2,3 +2,4 @@
path=/retroarch/cores/mame2003_plus_libretro_libnx.nro
supported_extensions=zip
database=MAME 2003-Plus
use_base_name=true

View File

@@ -2,3 +2,4 @@
path=/retroarch/cores/xrick_libretro_libnx.nro
supported_extensions=zip
database=Rick Dangerous
use_base_name=true

View File

@@ -1,21 +1,21 @@
{
"[Applet Mode]": "[Applet-Modus]",
"No Internet": "Keine Internetverbindung",
"[Applet Mode]": " | Applet Modus |",
"No Internet": "Kein Internet",
"Files": "Dateien",
"Apps": "Apps",
"Store": "Store",
"Apps": "hb-Apps",
"Store": "hb-Store",
"Menu": "Menü",
"Options": "Optionen",
"OK": "OK",
"Back": "Zurück",
"Select": "Auswählen",
"Open": "Öffnen",
"Launch": "Starten",
"Open": "Öffne",
"Launch": "Starte",
"Info": "Info",
"Install": "Installieren",
"Delete": "Löschen",
"Restart": "Neustart",
"Changelog": "Changelog",
"Changelog": "Neuerungen",
"Details": "Details",
"Update": "Update",
"Remove": "Entfernen",
@@ -23,61 +23,62 @@
"Download": "Download",
"Next Page": "Nächste Seite",
"Prev Page": "Vorherige Seite",
"Unstar": "Favorit entfernen",
"Unstar": "Kein Favorit",
"Star": "Favorit",
"System memory": "System-Speicher",
"microSD card": "microSD-Karte",
"Sd": "SD",
"Image System memory": "System-Speicher Bild",
"Image microSD card": "microSD-Karten Bild",
"Slow": "Langsam",
"Normal": "Normal",
"Fast": "Schnell",
"System memory": "NAND Systemspeicher",
"microSD card": "SD-Karte",
"Sd": "SD-Karte | Root-Verzeichnis",
"Image System memory": "Album | NAND Systemspeicher",
"Image microSD card": "Album | SD-Karte",
"Slow": "Niedrig",
"Normal": "Mittel",
"Fast": "Hoch",
"Yes": "Ja",
"No": "Nein",
"Enabled": "Aktiviert",
"Disabled": "Deaktiviert",
"Enabled": "An",
"Disabled": "Aus",
"Sort By": "Sortieren nach",
"Sort Options": "Sortieroptionen",
"Filter": "Filter",
"Sort": "Sortieren",
"Order": "Reihenfolge",
"Sort By": "Sortierung",
"Sort Options": " Sortierung | Optionen",
"Filter": "Rubrik",
"Sort": "Sortiert nach",
"Order": "Anordnung",
"Search": "Suchen",
"Updated": "Aktualisiert",
"Updated (Star)": "Aktualisiert (Favoriten)",
"Updated": "zuletzt aktualisiert",
"Updated (Star)": "Favorit | zuletzt aktualisiert",
"Downloads": "Downloads",
"Size": "Größe",
"Size (Star)": "Größe (Favoriten)",
"Alphabetical": "Alphabetisch",
"Alphabetical (Star)": "Alphabetisch (Favoriten)",
"Likes": "Likes",
"ID": "ID",
"Descending": "Absteigend",
"Descending (down)": "Absteigend",
"Desc": "Abst.",
"Ascending": "Aufsteigend",
"Ascending (Up)": "Aufsteigend",
"Asc": "Aufst.",
"Size (Star)": "Favorit | Größe",
"Alphabetical": "Name",
"Alphabetical (Star)": "Favorit | Name",
"Likes": "Beliebtheit",
"ID": "Theme | Paket ID",
"Descending": "Absteigend",
"Descending (down)": "Absteigend",
"Desc": "",
"Ascending": "Aufsteigend",
"Ascending (Up)": "Aufsteigend",
"Asc": "",
"Menu Options": "Menü-Optionen",
"Theme": "Theme",
"Theme Options": "Theme-Optionen",
"Select Theme": "Theme auswählen",
"Menu Options": " Menü | Optionen",
"Theme": "Themes",
"Theme Options": " Themes | Optionen",
"Select Theme": "Theme wählen",
"Shuffle": "Zufällig",
"Music": "Musik",
"Network": "Netzwerk",
"Network Options": "Netzwerk-Optionen",
"12 Hour Time": "12-Std Zeitformat",
"Network": "Konnektivität",
"Network Options": "Konnektivität | Optionen",
"Ftp": "FTP",
"Mtp": "MTP",
"Nxlink": "Nxlink",
"Nxlink Connected": "Nxlink verbunden",
"Nxlink Upload": "Nxlink Upload",
"Nxlink Finished": "Nxlink abgeschlossen",
"Switch-Handheld!": "Switch-Handheld!",
"Switch-Docked!": "Switch-Dock-Modus!",
"Nxlink": "NXLink",
"Nxlink Connected": "NXLink | Verbunden",
"Nxlink Upload": "NXLink | wird hochgeladen...",
"Nxlink Finished": "NXLink | Hochladen beendet",
"Switch-Handheld!": "Handheld!",
"Switch-Docked!": "Angedockt!",
"Language": "Sprache",
"Auto": "Auto",
"Auto": "Systemsprache",
"English": "English",
"Japanese": "日本語",
"French": "Français",
@@ -86,55 +87,55 @@
"Spanish": "Español",
"Chinese": "中文",
"Korean": "한국어",
"Dutch": "Dutch",
"Dutch": "Nederlands",
"Portuguese": "Português",
"Russian": "Русский",
"Swedish": "Svenska",
"Vietnamese": "Vietnamese",
"Logging": "Logging",
"Replace hbmenu on exit": "hbmenu beim Beenden ersetzen",
"Misc": "Sonstiges",
"Misc Options": "Weitere Optionen",
"Web": "Web",
"Vietnamese": "tiếng Việt",
"Logging": "Protokollieren",
"Replace hbmenu on exit": "hbmenu durch sphaira ersetzen",
"Misc": "Extras",
"Misc Options": " Extras | Optionen",
"Web": "WEB Browser",
"Install forwarders": "Forwarder installieren",
"Install location": "Installationsort",
"Show install warning": "Installationswarnung anzeigen",
"Text scroll speed": "Textlaufgeschwindigkeit",
"Install location": "Einhängepunkt",
"Show install warning": "Warnungen anzeigen",
"Text scroll speed": "Laufschrift Tempo",
"FileBrowser": "Datei-Browser",
"FileBrowser": "Datei-Manager",
"%zd files": "%zd Dateien",
"%zd dirs": "%zd Ordner",
"File Options": "Datei-Optionen",
"Show Hidden": "Versteckte anzeigen",
"File Options": "Datei - Ordner | Optionen",
"Show Hidden": "Versteckte zeigen",
"Folders First": "Ordner zuerst",
"Hidden Last": "Versteckte zuletzt",
"Cut": "Ausschneiden",
"Copy": "Kopieren",
"Paste": "Einfügen",
"Paste ": "Einfügen ",
" file(s)?": " Datei(en)?",
"Paste ": "Einfügen von: ",
" file(s)?": " Datei/en?",
"Rename": "Umbenennen",
"Set New File Name": "Neuen Dateinamen eingeben",
"Advanced": "Erweitert",
"Advanced Options": "Erweiterte Optionen",
"Create File": "Datei erstellen",
"Set File Name": "Dateinamen eingeben",
"Create Folder": "Ordner erstellen",
"Set Folder Name": "Ordnernamen eingeben",
"View as text (unfinished)": "Als Text anzeigen (Beta)",
"Ignore read only": "Schreibschutz ignorieren",
"Mount": "Einbinden",
"Empty...": "Leer...",
"Open with DayBreak?": "Mit DayBreak öffnen?",
"Launch ": "Starten ",
"Launch option for: ": "Startoption für: ",
"Select launcher for: ": "Launcher auswählen für: ",
"Set New File Name": "Neuen Dateinamen festlegen",
"Advanced": "Erweitert...",
"Advanced Options": " Erweitert | Optionen",
"Create File": "Neue Datei",
"Set File Name": "Dateiname festlegen",
"Create Folder": "Neuer Ordner",
"Set Folder Name": "Ordner umbenennen",
"View as text (unfinished)": "Als Text anzeigen",
"Ignore read only": "Schreibschutz umgehen?",
"Mount": "Einhängen",
"Empty...": "Keine Daten...",
"Open with DayBreak?": "Mit Daybreak öffnen?",
"Launch ": "Starte ",
"Launch option for: ": "Start Option für: ",
"Select launcher for: ": "Wähle Launcher für: ",
"Homebrew": "Homebrew",
"Homebrew Options": "Homebrew-Optionen",
"Hide Sphaira": "Sphaira ausblenden",
"Homebrew": "hbmenu",
"Homebrew Options": " hbmenu | Optionen",
"Hide Sphaira": "Verstecke sphaira",
"Install Forwarder": "Forwarder installieren",
"WARNING: Installing forwarders will lead to a ban!": "WARNUNG: Installation von Forwardern führt zum Ban!",
"WARNING: Installing forwarders will lead to a ban!": "Installiere Forwarder-NSP´s mit VORSICHT.\nEs erhöht das Risiko eines Konsolen-Banns!",
"Installing Forwarder": "Installiere Forwarder",
"Creating Program": "Erstelle Programm",
"Creating Control": "Erstelle Control",
@@ -143,26 +144,26 @@
"Updating ncm databse": "Aktualisiere NCM-Datenbank",
"Pushing application record": "Übertrage Anwendungsdaten",
"Installed!": "Installiert!",
"Failed to install forwarder": "Forwarder-Installation fehlgeschlagen",
"Failed to install forwarder": "Fehler beim installieren des Forwarders",
"Unstarred ": "Favorit entfernt ",
"Starred ": "Favorit hinzugefügt ",
"Starred ": "Favorit ",
"AppStore": "AppStore",
"Filter: %s | Sort: %s | Order: %s": "Filter: %s | Sortierung: %s | Reihenfolge: %s",
"AppStore Options": "AppStore-Optionen",
"All": "Alle",
"AppStore": "hb-AppStore",
"Filter: %s | Sort: %s | Order: %s": "Rubrik: %s | Sort.nach.: %s | Ordnung: %s",
"AppStore Options": " hb-AppStore | Optionen",
"All": "Alles anzeigen",
"Games": "Spiele",
"Emulators": "Emulatoren",
"Tools": "Tools",
"Themes": "Themes",
"Legacy": "Legacy",
"Legacy": "Älteres",
"version: %s": "Version: %s",
"updated: %s": "Aktualisiert: %s",
"category: %s": "Kategorie: %s",
"extracted: %.2f MiB": "Entpackt: %.2f MiB",
"app_dls: %s": "Downloads: %s",
"More by Author": "Mehr vom Entwickler",
"Leave Feedback": "Feedback geben",
"updated: %s": "Letztes Update am: %s",
"category: %s": "Rubrik: %s",
"extracted: %.2f MiB": "Größe: %.2f MiB",
"app_dls: %s": "Anzahl Downloads: %s",
"More by Author": "Weitere Apps des Entwicklers",
"Leave Feedback": "Feedback hinterlassen",
"Irs": "IR-Sensor",
"Ambient Noise Level: ": "Umgebungsrauschen: ",
@@ -202,54 +203,55 @@
"External Light Filter": "Externes Lichtfilter",
"Load Default": "Standard laden",
"Themezer": "Themezer",
"Themezer Options": "Themezer-Optionen",
"Themezer": "Themezer | NX Themes",
"Themezer Options": " Themezer | Optionen",
"Nsfw": "NSFW",
"Page": "Seite",
"Page %zu / %zu": "Seite %zu / %zu",
"Enter Page Number": "Seitenzahl eingeben",
"Bad Page": "Ungültige Seite",
"Page": "Seiten Nr. wählen ",
"Page %zu / %zu": " %zu / %zu",
"Enter Page Number": "Zu Seite Nr.: ___",
"Bad Page": "Seite nicht gefunden",
"Download theme?": "Theme herunterladen?",
"GitHub": "GitHub",
"Downloading json": "Lade JSON herunter",
"Select asset to download for ": "Wähle Asset zum Download für ",
"Downloading json": "Lade JSON-File",
"Select asset to download for ": "Wähle Asset für den Download von ",
"Installing ": "Installiere ",
"Uninstalling ": "Deinstalliere ",
"Deleting ": "Lösche ",
"Deleting": "Lösche",
"Pasting ": "Füge ein ",
"Pasting": "Füge ein",
"Removing ": "Entferne ",
"Scanning ": "Scanne ",
"Creating ": "Erstelle ",
"Copying ": "Kopiere ",
"Trying to load ": "Lade ",
"Downloading ": "Lade herunter ",
"Downloaded ": "Heruntergeladen ",
"Removed ": "Entfernt ",
"Checking MD5": "Prüfe MD5",
"Loading...": "Lade...",
"Loading": "Lade",
"Empty!": "Leer!",
"Installing ": "Installiert wird: ",
"Uninstalling ": "Deinstalliert wird: ",
"Deleting ": "Gelöscht wird: ",
"Deleting": "Gelöscht wurde:",
"Pasting ": "Eingefügt wird: ",
"Pasting": "Eingefügt wurde:",
"Removing ": "Entfernt wird: ",
"Scanning ": "Gescannt wird: ",
"Creating ": "Erstellt wird: ",
"Copying ": "Kopiert wird: ",
"Trying to load ": "Versucht zu laden wird: ",
"Downloading ": "Heruntergeladen wird: ",
"Downloaded ": "Heruntergeladen wurde: ",
"Removed ": "Entfernt wurde: ",
"Checking MD5": "Checke MD5 Prüfsumme",
"Loading...": "Wird geladen...",
"Loading": "Wird geladen",
"Empty!": "Keine Daten!",
"Not Ready...": "Nicht bereit...",
"Error loading page!": "Fehler beim Laden!",
"Error loading page!": "Ladefehler!",
"Update avaliable: ": "Update verfügbar: ",
"Download update: ": "Update herunterladen: ",
"Updated to ": "Aktualisiert auf ",
"Press OK to restart Sphaira": "OK drücken um Sphaira neuzustarten",
"Restart Sphaira?": "Sphaira neustarten?",
"Failed to download update": "Update-Download fehlgeschlagen",
"Download update: ": " Herunterladen des Updates: ",
"Updated to ": "Aktualisiert auf: ",
"Press OK to restart Sphaira": "Drücke OK um sphaira erneut zustarten",
"Restart Sphaira?": "sphaira erneut starten?",
"Failed to download update": "Herunterladen des Updates fehlgeschlagen!",
"Restore hbmenu?": "hbmenu wiederherstellen?",
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "Konnte /switch/hbmenu.nro nicht finden\nBitte hbmenu über den AppStore neu installieren",
"Failed to restore hbmenu, please re-download hbmenu": "Wiederherstellung fehlgeschlagen, bitte hbmenu neu herunterladen",
"Failed to restore hbmenu, using sphaira instead": "Wiederherstellung fehlgeschlagen, verwende stattdessen Sphaira",
"Restored hbmenu, closing sphaira": "hbmenu wiederhergestellt, Sphaira wird beendet",
"Restored hbmenu": "hbmenu wiederhergestellt",
"Delete Selected files?": "Ausgewählte Dateien löschen?",
"Completely remove ": "Vollständig entfernen ",
"Are you sure you want to delete ": "Wirklich löschen ",
"Are you sure you wish to cancel?": "Wirklich abbrechen?",
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "Die Datei /switch/hbmenu.nro fehlt.\nInstalliere hbmenu über den hb-AppStore.",
"Failed to restore hbmenu, please re-download hbmenu": "Fehler, hbmenu nicht wiederhergrstellt!\nInstalliere hbmenu über den hb-AppStore.",
"Failed to restore hbmenu, using sphaira instead": "Fehler, hbmenu nicht wiederhergrstellt!\nVerwende weiter sphaira",
"Restored hbmenu, closing sphaira": "hbmenu wurde wiederhergestellt, schließe sphaira",
"Restored hbmenu": "hbmenu wurde wiederhergestellt",
"Delete Selected files?": "Ausgewähle Dateien löschen?",
"Completely remove ": "Komplett gelöscht wird: ",
"Are you sure you want to delete ": "Bist du sicher zu löschen? Bestätige Löschung von: ",
"Are you sure you wish to cancel?": "Bist du sicher dass du abbrechen willst?",
"Audio disabled due to suspended game": "Audio deaktivert wegen Spielabbruch",
"If this message appears repeatedly, please open an issue.": "Bei wiederholtem Auftreten bitte Issue erstellen."
}
}

View File

@@ -66,6 +66,7 @@
"Select Theme": "Select Theme",
"Shuffle": "Shuffle",
"Music": "Music",
"12 Hour Time": "12 Hour Time",
"Network": "Network",
"Network Options": "Network Options",
"Ftp": "FTP",
@@ -251,5 +252,6 @@
"Completely remove ": "Completely remove ",
"Are you sure you want to delete ": "Are you sure you want to delete ",
"Are you sure you wish to cancel?": "Are you sure you wish to cancel?",
"Audio disabled due to suspended game": "Audio disabled due to suspended game",
"If this message appears repeatedly, please open an issue.": "If this message appears repeatedly, please open an issue."
}

View File

@@ -66,6 +66,7 @@
"Select Theme": "Seleccionar tema",
"Shuffle": "Barajar",
"Music": "Música",
"12 Hour Time": "",
"Network": "Red",
"Network Options": "Opciones de red",
"Ftp": "FTP",
@@ -251,5 +252,6 @@
"Completely remove ": "Eliminar completamente",
"Are you sure you want to delete ": "¿Estás seguro que quieres eliminar? ",
"Are you sure you wish to cancel?": "¿Estás seguro que deseas cancelar?",
"Audio disabled due to suspended game": "",
"If this message appears repeatedly, please open an issue.": "Si este mensaje aparece repetidamente, por favor abrir un 'issue'."
}
}

View File

@@ -66,6 +66,7 @@
"Select Theme": "Choisir un Thème",
"Shuffle": "Aléatoire",
"Music": "Musique",
"12 Hour Time": "Temps sur 12 heures",
"Network": "Réseau",
"Network Options": "Options Réseau",
"Ftp": "FTP",
@@ -251,5 +252,6 @@
"Completely remove ": "Supprimer totalement ",
"Are you sure you want to delete ": "Êtes-vous sûr de vouloir supprimer ",
"Are you sure you wish to cancel?": "Souhaitez-vous vraiment annuler?",
"Audio disabled due to suspended game": "Audio désactivé à cause d'un jeu suspendu",
"If this message appears repeatedly, please open an issue.": "Si ce message apparait en boucle veuillez ouvrir une issue."
}

View File

@@ -66,6 +66,7 @@
"Select Theme": "Seleziona tema",
"Shuffle": "Mescola",
"Music": "Musica",
"12 Hour Time": "",
"Network": "Rete",
"Network Options": "Opzioni di rete",
"Ftp": "FTP",
@@ -203,53 +204,54 @@
"Load Default": "Carica predefinito",
"Themezer": "Themezer",
"Themezer Options": "",
"Nsfw": "",
"Page": "",
"Page %zu / %zu": "Page %zu / %zu",
"Enter Page Number": "",
"Bad Page": "",
"Download theme?": "",
"Themezer Options": "Impostazioni Themezer",
"Nsfw": "NSFW",
"Page": "Pagina",
"Page %zu / %zu": "Pagina %zu / %zu",
"Enter Page Number": "Inserisci il numero della pagina",
"Bad Page": "Pagina invalida",
"Download theme?": "Vuoi scaricare il tema?",
"GitHub": "",
"Downloading json": "",
"Select asset to download for ": "",
"GitHub": "GitHub",
"Downloading json": "Scaricamento json",
"Select asset to download for": "Scegli l'asset da scaricare per",
"Installing ": "",
"Uninstalling ": "",
"Deleting ": "",
"Deleting": "",
"Pasting ": "",
"Pasting": "",
"Removing ": "",
"Scanning ": "",
"Creating ": "",
"Copying ": "",
"Trying to load ": "",
"Downloading ": "",
"Downloaded ": "",
"Removed ": "",
"Checking MD5": "",
"Loading...": "",
"Loading": "",
"Empty!": "",
"Not Ready...": "",
"Error loading page!": "",
"Update avaliable: ": "",
"Download update: ": "",
"Updated to ": "",
"Press OK to restart Sphaira": "",
"Restart Sphaira?": "",
"Failed to download update": "",
"Restore hbmenu?": "",
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "",
"Failed to restore hbmenu, please re-download hbmenu": "",
"Failed to restore hbmenu, using sphaira instead": "",
"Restored hbmenu, closing sphaira": "",
"Restored hbmenu": "",
"Delete Selected files?": "",
"Completely remove ": "",
"Installing ": "Installazione",
"Uninstalling ": "Disinstallazione",
"Deleting ": "Eliminazione",
"Deleting": "Eliminazione",
"Pasting ": "Incollo",
"Pasting": "Incollo",
"Removing ": "Rimozione",
"Scanning ": "Scan",
"Creating ": "Creazione",
"Copying ": "Copio",
"Trying to load ": "Cercando di caricare",
"Downloading ": "Scaricando",
"Downloaded ": "Scaricato",
"Removed ": ""Rimosso,
"Checking MD5": "Controllo MD5",
"Loading...": "Caricamento...",
"Loading": "Caricamento",
"Empty!": "Vuoto!",
"Not Ready...": "Non pronto...",
"Error loading page!": "Errore nel caricare la pagina!",
"Update avaliable: ": "Aggiornamento disponibile",
"Download update: ": "Scarica aggiornamento",
"Updated to ": "Aggiornato a",
"Press OK to restart Sphaira": "Premi OK per riavviare Sphaira",
"Restart Sphaira?": "Vuoi riavviare Sphaira?",
"Failed to download update": "Download aggiornamento fallito",
"Restore hbmenu?": "Vuoi ripristinare hbmenu?",
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "Impossibile trovare /switch/hbmenu.nro\nUsa l'Appstore per reinstallare hbmenu",
"Failed to restore hbmenu, please re-download hbmenu": "Impossibile ripristinare hbmenu, per favore riscaricalo",
"Failed to restore hbmenu, using sphaira instead": "Impossibile ripristinare hbmenu, uso Sphaira invece",
"Restored hbmenu, closing sphaira": "hbmenu ripristinato, chiudo Sphaira",
"Restored hbmenu": "hbmenu ripristinato",
"Delete Selected files?": "Vuoi rimuovere i file selezionati?",
"Completely remove ": "Elimina definitivamente",
"Are you sure you want to delete ": "Sei sicuro di voler eliminare? ",
"Are you sure you wish to cancel?": "",
"If this message appears repeatedly, please open an issue.": ""
}
"Are you sure you wish to cancel?": "Sei sicuro di voler annullare?",
"Audio disabled due to suspended game": "Audio disabilitato poichè un app è in pausa",
"If this message appears repeatedly, please open an issue.": "Se questo messaggio appare frequentemente, segnala il bug."
}

View File

@@ -66,6 +66,7 @@
"Select Theme": "テーマを選ぶ",
"Shuffle": "シャッフル",
"Music": "BGM",
"12 Hour Time": "",
"Network": "ネットワーク",
"Network Options": "ネットワーク設定",
"Ftp": "FTP",
@@ -251,5 +252,6 @@
"Completely remove ": "除去しますか ",
"Are you sure you want to delete ": "消去してもよろしいですか ",
"Are you sure you wish to cancel?": "本当に取り消しますか?",
"Audio disabled due to suspended game": "ゲームが一時停止状態の場合、オーディオは無効になります",
"If this message appears repeatedly, please open an issue.": "このメッセージが繰り返し表示される場合は、問題を開いてください。"
}

View File

@@ -66,6 +66,7 @@
"Select Theme": "테마 선택",
"Shuffle": "셔플",
"Music": "BGM",
"12 Hour Time": "",
"Network": "네트워크",
"Network Options": "네트워크 옵션",
"Ftp": "FTP (무선)",
@@ -251,5 +252,6 @@
"Completely remove ": "정말 삭제할까요 ",
"Are you sure you want to delete ": "정말 삭제할까요 ",
"Are you sure you wish to cancel?": "정말 취소할까요?",
"Audio disabled due to suspended game": "게임 실행 중에는 BGM이 비활성화 됩니다",
"If this message appears repeatedly, please open an issue.": "해당 메시지가 반복해서 나타나는 경우, 이슈를 등록하세요."
}

View File

@@ -66,6 +66,7 @@
"Select Theme": "Selecteer Thema",
"Shuffle": "Schudden",
"Music": "Muziek",
"12 Hour Time": "",
"Network": "Netwerk",
"Network Options": "Netwerkopties",
"Ftp": "FTP",
@@ -251,5 +252,6 @@
"Completely remove ": "",
"Are you sure you want to delete ": "Weet u zeker dat u wilt verwijderen ",
"Are you sure you wish to cancel?": "",
"Audio disabled due to suspended game": "",
"If this message appears repeatedly, please open an issue.": ""
}

View File

@@ -1,6 +1,6 @@
{
"[Applet Mode]": "[Modo Applet]",
"No Internet": "Sem Internet",
"[Applet Mode]": "[Modo applet]",
"No Internet": "Sem internet",
"Files": "Arquivos",
"Apps": "Aplicativos",
"Store": "Loja",
@@ -21,6 +21,8 @@
"Remove": "Remover",
"Restore": "Restaurar",
"Download": "Baixar",
"Next": "Prómixo",
"Prev": "Anterior",
"Next Page": "Próxima página",
"Prev Page": "Página anterior",
"Unstar": "Desfavoritar",
@@ -35,6 +37,9 @@
"Fast": "Rápida",
"Yes": "Sim",
"No": "Não",
"On": "Sim",
"Off": "Não",
"Enable": "Habilitar",
"Enabled": "Sim",
"Disabled": "Não",
@@ -43,6 +48,7 @@
"Filter": "Filtro",
"Sort": "Organizar por",
"Order": "Ordem",
"Layout": "Exibição",
"Search": "Buscar",
"Updated": "Atualizado",
"Updated (Star)": "Atualizado (favoritos)",
@@ -59,6 +65,9 @@
"Ascending": "Ascendente",
"Ascending (Up)": "Ascendente (cima)",
"Asc": "Asc.",
"List": "Lista",
"Icon": "Ícones",
"Grid": "Grade",
"Menu Options": "Opções do menu",
"Theme": "Tema",
@@ -66,10 +75,12 @@
"Select Theme": "Tema atual",
"Shuffle": "Embaralhar temas",
"Music": "Música",
"12 Hour Time": "Relógio de 12 horas",
"Download Default Music": "Baixar música padrão",
"Network": "Rede",
"Network Options": "Opções de rede",
"Ftp": "FTP",
"Mtp": "MTP",
"Ftp": "Servidor FTP",
"Mtp": "Escuta MTP",
"Nxlink": "Nxlink",
"Nxlink Connected": "Nxlink conectado",
"Nxlink Upload": "Envio Nxlink",
@@ -96,10 +107,11 @@
"Misc": "Diversos",
"Misc Options": "Opções diversas",
"Web": "Navegador de internet",
"Install forwarders": "Instalar forwarders",
"Install forwarders": "Instalar atalhos (forwarders)",
"Install location": "Local de instalação",
"Show install warning": "Mostrar aviso de instalação",
"Text scroll speed": "Rolagem do texto",
"Set right-side menu": "Menu do botão R",
"FileBrowser": "Arquivos",
"%zd files": "%zd arquivo(s)",
@@ -114,6 +126,7 @@
"Paste ": "Colar ",
" file(s)?": " arquivo(s)?",
"Rename": "Renomear",
"Compress to zip": "Comprimir em zip",
"Set New File Name": "Defina o nome do novo arquivo",
"Advanced": "Avançado",
"Advanced Options": "Opções avançadas",
@@ -124,18 +137,18 @@
"View as text (unfinished)": "Ver como texto (inacabado)",
"Ignore read only": "Ignorar somente leitura",
"Mount": "Montar",
"Empty...": "Vazio...",
"Empty...": "Vazio",
"Open with DayBreak?": "Abrir com DayBreak?",
"Launch ": "Iniciar ",
"Launch option for: ": "Opções de inicialização para: ",
"Select launcher for: ": "Selecionar launcher para: ",
"Homebrew": "Aplicativos",
"Homebrew Options": "Opções do aplicativo",
"Homebrew Options": "Opções de aplicativo",
"Hide Sphaira": "Esconder sphaira",
"Install Forwarder": "Instalar forwarder",
"WARNING: Installing forwarders will lead to a ban!": "AVISO: Instalar forwarders pode\nresultar em um banimento!",
"Installing Forwarder": "Instalando forwarder",
"Install Forwarder": "Instalar atalho (forwarder)",
"WARNING: Installing forwarders will lead to a ban!": "AVISO: Instalar atalhos pode resultar em um banimento!",
"Installing Forwarder": "Instalando forwarder...",
"Creating Program": "Criando Program",
"Creating Control": "Criando Control",
"Creating Meta": "Criando Meta",
@@ -148,10 +161,11 @@
"Starred ": "Favoritado ",
"AppStore": "Loja",
"Appstore": "Loja",
"Filter: %s | Sort: %s | Order: %s": "Filtro: %s | Por: %s | Ordem: %s",
"AppStore Options": "Opções da loja",
"All": "Todos",
"Games": "Jogos",
"Games": "Softwares",
"Emulators": "Emuladores",
"Tools": "Ferramentas",
"Themes": "Temas",
@@ -163,8 +177,31 @@
"app_dls: %s": "downloads: %s",
"More by Author": "Mais deste autor",
"Leave Feedback": "Deixar um feedback",
"Game Options": "Opções de software",
"Launch random game": "Iniciar um software aleatório",
"List meta records": "Registro de conteúdos",
"Entries": "Entradas",
"Hide forwarders": "Ocultar atalhos (forwarders)",
"Dump": "Exportar",
"Select content to dump": "Exportação de conteúdo",
"Dump All": "Exportar tudo",
"Dump Application": "Exportar software base",
"Dump Patch": "Exportar atualização",
"Dump AddOnContent": "Exportar DLCs",
"Dump DataPatch": "Exportar patch de dados",
"Select dump location": "Selecione o local de exportação",
"microSD card (/dumps/NSP/)": "Cartão microSD (/dump/NSP/)",
"USB transfer (Switch 2 Switch)": "Transferência via USB (Switch 2 Switch)",
"/dev/null (Speed Test)": "Teste de velocidade (/dev/null)",
"Dumping": "Extraindo...",
"Dump successfull!": "Extração foi concluída com sucesso.",
"Dump failed!": "Extração falhou.",
"Irs": "IRS",
"Irs": "Câmera de movimento IR",
"IRS": "Câmera de movimento IR",
"IRS (Infrared Joycon Camera)": "Câmera de movimento IR",
"Ambient Noise Level: ": "Nível de ruído ambiente: ",
"Controller": "Controle",
"Pad ": "Pad ",
@@ -179,7 +216,7 @@
"270 (Upside down)": "270 (de cabeça para baixo)",
"Colour": "Cor",
"Grey": "Cinza",
"Ironbow": "Arco de ferro",
"Ironbow": "Ferro",
"Green": "Verde",
"Red": "Vermelho",
"Blue": "Azul",
@@ -198,9 +235,9 @@
"80x60": "80×60",
"40x30": "40×30",
"20x15": "20×15",
"Trimming Format": "Formato de corte",
"Trimming Format": "Formato do recorte",
"External Light Filter": "Filtro de luz externa",
"Load Default": "Carregar padrão",
"Load Default": "Restaurar padrão",
"Themezer": "Themezer",
"Themezer Options": "Opções do Themezer",
@@ -213,12 +250,66 @@
"GitHub": "GitHub",
"Downloading json": "Baixando JSON",
"Select asset to download for ": "Selecione o recurso para baixar em ",
"Select asset to download for ": "Selecione o recurso para baixar de ",
"Install Options": "Opções de instalação",
"Install options": "Opções de instalação",
"Enable sysmmc": "Habilitar sysMMC",
"Enable emummc": "Habilitar emuMMC",
"Boost CPU clock": "Aumentar clock da CPU",
"Allow downgrade": "Permitir downgrade",
"Skip if already installed": "Pular se já instalado",
"Ticket only": "Instalar apenas ticket",
"Patch ticket": "Fazer patch de ticket",
"Skip base": "Pular software base",
"Skip patch": "Pular atualizações",
"Skip dlc": "Pular DLCs",
"Skip data patch": "Pular patch de dados",
"Skip ticket": "Pular ticket",
"skip NCA hash verify": "Pular checagem de hash NCA",
"Skip RSA header verify": "Pular checagem de header RSA",
"Skip RSA NPDM verify": "Pular checagem de NPDM RSA",
"Ignore distribution bit": "Ignorar bit de distribuição",
"Convert to standard crypto": "Convertr para crypto padrão",
"Lower master key": "Reduzir a master key",
"Lower system version": "Reduzir versão do sistema",
"Install Selected files?": "Instalar os arquivos selecionados?",
"Installed ": "Instalado ",
"FTP Install": "Instalação via FTP",
"USB Install": "Instalação via USB",
"GameCard Install": "Instalação de cartão de jogo",
"FTP Install (EXPERIMENTAL)": "Instalação via FTP (EXPERIMENTAL)",
"USB": "USB",
"GameCard": "Cartão de jogo",
"Disable MTP for usb install": "Escuta MTP desabilitada temporáriamente.",
"Re-enabled MTP": "Escuta MTP reabilitada.",
"Waiting for connection...": "Aguardando conexão...",
"Transferring data...": "Transferindo dados...",
"Ftp install success!": "Instalação via FTP concluída com sucesso.",
"Ftp install failed!": "Instalação via FTP falhou.",
"Usb install success!": "Instalação via USB concluída com sucesso.",
"Usb install failed!": "Instalação via USB falhou.",
"Gc install success!": "Instalação de cartão de jogo concluída com sucesso.",
"Gc install failed!": "Instalação de cartão de jogo falhou.",
"Installed via usb": "Instalado via USB",
"Failed to install via FTP, press B to exit...": "Falha ao instalar via FTP,\naperte B para sair.",
"Failed to init usb, press B to exit...": "Falha ao instalar via USB,\naperte B para sair.",
"Press B to exit...": "Aperte B para sair.",
"Connection Type: WiFi | Strength: ": "Conexão por rede Wi-Fi | Intensidade do sinal: ",
"Connection Type: Ethernet": "Conexão por cabo (ethernet)",
"Connection Type: None": "Sem conexão",
"Host:": "Host:",
"Port:": "Porta:",
"Username:": "Nome de usuário:",
"Password:": "Senha:",
"SSID:": "SSID:",
"Passphrase:": "Senha:",
"Installing ": "Instalando ",
"Uninstalling ": "Desinstalando ",
"Deleting ": "Excluindo ",
"Deleting": "Excluindo",
"Deleting": "Excluindo...",
"Pasting ": "Colando ",
"Pasting": "Colando ",
"Removing ": "Removendo ",
@@ -234,22 +325,26 @@
"Loading": "Carregando",
"Empty!": "Vazio",
"Not Ready...": "Não está pronto...",
"Error loading page!": "Erro ao carregar página!",
"Error loading page!": "Erro ao carregar página.",
"Update avaliable: ": "Atualização disponível: ",
"Download update: ": "Baixar autalização: ",
"Updated to ": "Atualizado para ",
"Press OK to restart Sphaira": "Selecione OK para reiniciar o sphaira",
"Press OK to restart Sphaira": "Selecione OK para reiniciar o sphaira.",
"Restart Sphaira?": "Reiniciar sphaira?",
"Failed to download update": "Falha ao baixar a atualização",
"Failed to download update": "Falha ao baixar a atualização.",
"Restore hbmenu?": "Restaurar hbmenu?",
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "Falha ao buscar /switch/hbmenu.nro\nUse a AppStore para reinstalar o hbmenu",
"Failed to restore hbmenu, please re-download hbmenu": "Falha ao restaurar o hbmenu, baixe o hbmenu novamente",
"Failed to restore hbmenu, using sphaira instead": "Falha ao restaurar hbmenu, usando sphaira",
"Restored hbmenu, closing sphaira": "hbmenu restaurado, fechando sphaira",
"Restored hbmenu": "hbmenu restaurado",
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "Falha ao buscar /switch/hbmenu.nro\nUse a loja (AppStore) para reinstalar o hbmenu.",
"Failed to restore hbmenu, please re-download hbmenu": "Falha ao restaurar o hbmenu, baixe o hbmenu novamente.",
"Failed to restore hbmenu, using sphaira instead": "Falha ao restaurar hbmenu, usando sphaira.",
"Restored hbmenu, closing sphaira": "hbmenu restaurado, fechando sphaira.",
"Restored hbmenu": "hbmenu restaurado.",
"Delete Selected files?": "Excluir os arquivos selecionados?",
"Completely remove ": "Remover completamente ",
"Delete successfull!": "Exclusão foi concluída com sucesso.",
"Delete failed!": "Exclusão falhou",
"Are you sure you want to delete ": "Você tem certeza que quer excluir ",
"Are you sure you wish to cancel?": "Você tem certeza que quer cancelar?",
"Audio disabled due to suspended game": "Áudio desativado devido ao software suspenso.",
"If this message appears repeatedly, please open an issue.": "Se esta mensagem aparecer repetidamente, abra um issue."
}

View File

@@ -66,6 +66,7 @@
"Select Theme": "Выберите тему",
"Shuffle": "Перетасовать",
"Music": "Музыка",
"12 Hour Time": "",
"Network": "Сеть",
"Network Options": "Параметры сети",
"Ftp": "FTP",
@@ -251,5 +252,6 @@
"Completely remove ": "",
"Are you sure you want to delete ": "Вы уверены, что хотите удалить ",
"Are you sure you wish to cancel?": "",
"Audio disabled due to suspended game": "",
"If this message appears repeatedly, please open an issue.": ""
}

View File

@@ -66,6 +66,7 @@
"Select Theme": "Välj tema",
"Shuffle": "Blanda",
"Music": "Musik",
"12 Hour Time": "",
"Network": "Nätverk",
"Network Options": "Nätverksalternativ",
"Ftp": "FTP",
@@ -251,5 +252,6 @@
"Completely remove ": "Ta bort helt ",
"Are you sure you want to delete ": "Är du säker på att du vill radera ",
"Are you sure you wish to cancel?": "Är du säker på att du vill avbryta?",
"Audio disabled due to suspended game": "Ljud är avstängt på grund av bakgrundsprogram",
"If this message appears repeatedly, please open an issue.": "Om detta meddelande visas upprepade gånger, vänligen öppna en felanmälan.",
}
}

326
assets/romfs/i18n/uk.json Normal file
View File

@@ -0,0 +1,326 @@
{
"[Applet Mode]": "[Режим Аплету]",
"No Internet": "Без інтернету",
"Files": "Файли",
"Apps": "Програми",
"Store": "Магазин",
"Menu": "Меню",
"Options": "Налаштування",
"OK": "ОК",
"Back": "Назад",
"Select": "Вибрати",
"Open": "Відкрити",
"Launch": "Запустити",
"Info": "Інфо",
"Install": "Встановити",
"Delete": "Видалити",
"Restart": "Перезапустити",
"Changelog": "Журнал змін",
"Details": "Деталі",
"Update": "Оновити",
"Remove": "Видалити",
"Restore": "Відновити",
"Download": "Завантажити",
"Next": "Наступний",
"Prev": "Попередній",
"Next Page": "Наступна сторінка",
"Prev Page": "Попередня сторінка",
"Unstar": "Прибрати з обраного",
"Star": "Позначити зіркою",
"System memory": "Пам'ять консолі",
"microSD card": "SD-карта",
"Sd": "SD-карта",
"Image System memory": "Фото | Пам'ять консолі",
"Image microSD card": "Фото | SD-карта",
"Slow": "Повільно",
"Normal": "Нормально",
"Fast": "Швидко",
"Yes": "Так",
"No": "Ні",
"On": "Увімк.",
"Off": "Вимк.",
"Enable": "Увімк.",
"Enabled": "Увімк.",
"Disabled": "Вимк.",
"Sort By": "Сортувати за",
"Sort Options": "Опції сортування",
"Filter": "Фільтр",
"Sort": "Сортування",
"Order": "Порядок",
"Search": "Пошук",
"Updated": "Оновлено",
"Updated (Star)": "Оновлено (Зірка)",
"Downloads": "Завантаження",
"Size": "Розмір",
"Size (Star)": "Розмір (Зірка)",
"Alphabetical": "За алфавітом",
"Alphabetical (Star)": "За алфавітом (Зірка)",
"Likes": "Вподобання",
"ID": "ID",
"Descending": "За спаданням",
"Descending (down)": "За спаданням (вниз)",
"Desc": "Спад.",
"Ascending": "За зростанням",
"Ascending (Up)": "За зростанням (вгору)",
"Asc": "Зрост.",
"Menu Options": "Опції меню",
"Theme": "Тема",
"Theme Options": "Опції теми",
"Select Theme": "Вибрати тему",
"Shuffle": "Перемішати",
"Music": "Музика",
"12 Hour Time": "12-годинний формат часу",
"Download Default Music": "Завантажити музику за замовчуванням",
"Network": "Мережа",
"Network Options": "Опції мережі",
"Ftp": "FTP",
"Mtp": "MTP",
"Nxlink": "Nxlink",
"Nxlink Connected": "Nxlink підключено",
"Nxlink Upload": "Nxlink | Завантаження",
"Nxlink Finished": "Nxlink | Завершено",
"Switch-Handheld!": "Switch - Портатив!",
"Switch-Docked!": "Switch - Докований!",
"Language": "Мова",
"Auto": "Автоматично",
"English": "English",
"Japanese": "日本語",
"French": "Français",
"German": "Deutsch",
"Italian": "Italiano",
"Spanish": "Español",
"Chinese": "中文",
"Korean": "한국어",
"Dutch": "Nederlands",
"Portuguese": "Português",
"Russian": "Русский",
"Swedish": "Svenska",
"Vietnamese": "Tiếng Việt",
"Logging": "Логування",
"Replace hbmenu on exit": "Заміна hbmenu при виході",
"Misc": "Різне",
"Misc Options": "Опції різного",
"Web": "Веб",
"Install forwarders": "Встановити форвардери",
"Install location": "Місце встановлення",
"Show install warning": "Попередж. при встанов.",
"Text scroll speed": "Швидк. прокрутки",
"Set right-side menu": "Праве меню",
"FileBrowser": "Файловий менеджер",
"%zd files": "%zd файл(и)",
"%zd dirs": "%zd тек(и)",
"File Options": "Опції файлів",
"Show Hidden": "Показати приховані",
"Folders First": "Теки спочатку",
"Hidden Last": "Приховані в кінці",
"Cut": "Вирізати",
"Copy": "Копіювати",
"Paste": "Вставити",
"Paste ": "Вставити: ",
" file(s)?": " файл(и)?",
"Rename": "Перейменувати",
"Compress to zip": "Стиснути в zip",
"Set New File Name": "Введіть нове ім'я файлу",
"Advanced": "Додатково",
"Advanced Options": "Додаткові опції",
"Create File": "Створити файл",
"Set File Name": "Введіть ім'я файлу",
"Create Folder": "Створити теку",
"Set Folder Name": "Введіть ім'я теки",
"View as text (unfinished)": "Переглянути як текст (незавершено)",
"Ignore read only": "Ігнорувати лише читання",
"Mount": "Монтувати",
"Empty...": "Пусто...",
"Open with DayBreak?": "Відкрити за допомогою DayBreak?",
"Launch ": "Запустити ",
"Launch option for: ": "Опція запуску для: ",
"Select launcher for: ": "Виберіть лаунчер для: ",
"Homebrew": "Домашні програми",
"Homebrew Options": "Опції домашніх програм",
"Hide Sphaira": "Приховати Sphaira",
"Install Forwarder": "Встановити форвардер",
"WARNING: Installing forwarders will lead to a ban!": "УВАГА: Встановлення форвардерів може призвести до бану!",
"Installing Forwarder": "Встановлення форвардера",
"Creating Program": "Створення програми",
"Creating Control": "Створення контролера",
"Creating Meta": "Створення метаданих",
"Writing Nca": "Запис NCA",
"Updating ncm databse": "Оновлення бази даних NCM",
"Pushing application record": "Запис даних програми",
"Installed!": "Встановлено!",
"Failed to install forwarder": "Не вдалося встановити форвардер",
"Unstarred ": "Знято зірку з ",
"Starred ": "Позначено зіркою ",
"AppStore": "Магазин програм",
"Filter: %s | Sort: %s | Order: %s": "Фільтр: %s | Сорт.: %s | Порядок: %s",
"AppStore Options": "Опції магазину програм",
"All": "Всі",
"Games": "Ігри",
"Emulators": "Емулятори",
"Tools": "Інструменти",
"Themes": "Теми",
"Legacy": "Доступні оновлення",
"version: %s": "версія: %s",
"updated: %s": "оновлено: %s",
"category: %s": "категорія: %s",
"extracted: %.2f MiB": "розмір: %.2f MiB",
"app_dls: %s": "завантажень: %s",
"More by Author": "Більше від автора",
"Leave Feedback": "Залишити відгук",
"Game Options": "Опції ігор",
"Launch random game": "Запустити випадкову гру",
"List meta records": "Список метаданих записів",
"Entries": "Записи",
"Delete entity": "Видалити сутність",
"Hide forwarders": "Приховати форвардери",
"Irs": "ІЧ-сенсор",
"IRS (Infrared Joycon Camera)": "ІЧ (Інфрачервона камера Joycon)",
"Ambient Noise Level: ": "Рівень навколишнього шуму: ",
"Controller": "Контролер",
"Pad ": "Геймпад ",
" (Available)": " (Доступно)",
" (Unsupported)": " (Не підтримується)",
" (Unconnected)": " (Не підключено)",
"HandHeld": "Портативний режим",
"Rotation": "Обертання",
"0 (Sideways)": "0° (Збоку)",
"90 (Flat)": "90° (Плоско)",
"180 (-Sideways)": "180° (-Збоку)",
"270 (Upside down)": "270° (Догори дном)",
"Colour": "Колір",
"Grey": "Сірий",
"Ironbow": "Ironbow",
"Green": "Зелений",
"Red": "Червоний",
"Blue": "Синій",
"Light Target": "Ціль освітлення",
"All leds": "Всі світлодіоди",
"Bright group": "Яскрава група",
"Dim group": "Тьмяна група",
"None": "Немає",
"Gain": "Підсилення",
"Negative Image": "Негативне зображення",
"Normal image": "Нормальне зображення",
"Trimming Format": "Формат обрізки",
"External Light Filter": "Фільтр зовнішнього освітлення",
"Load Default": "Завантажити типові",
"Format": "Формат",
"320x240": "320×240",
"160x120": "160×120",
"80x60": "80×60",
"40x30": "40×30",
"20x15": "20×15",
"Themezer": "Themezer",
"Themezer Options": "Опції Themezer",
"Nsfw": "NSFW",
"Page": "Сторінка",
"Page %zu / %zu": "Сторінка %zu / %zu",
"Enter Page Number": "Введіть номер сторінки",
"Bad Page": "Неправильна сторінка",
"Download theme?": "Завантажити тему?",
"GitHub": "GitHub",
"Downloading json": "Завантаження JSON",
"Select asset to download for ": "Виберіть ресурс для завантаження для ",
"Install Options": "Опції встановлення",
"Install options": "Опції встановлення",
"Boost CPU clock": "Розігнати CPU",
"Allow downgrade": "Дозволити відкат",
"Skip if already installed": "Пропуск, якщо встановл.",
"Ticket only": "Тільки тікет",
"Patch ticket": "Змінити тікет",
"Skip base": "Пропустити базу",
"Skip patch": "Пропустити патч",
"Skip dlc": "Пропустити DLC",
"Skip data patch": "Пропустити патч даних",
"Skip ticket": "Пропустити тікет",
"skip NCA hash verify": "Пропуск перевірку хешу NCA",
"Skip RSA header verify": "Пропуск перевірку заголовка RSA",
"Skip RSA NPDM verify": "Пропуск перевірку NPDM RSA",
"Ignore distribution bit": "Ігнорувати біт розподілу",
"Convert to standard crypto": "Конвертувати у стандартне шифрування",
"Lower master key": "Знизити майстер-ключ",
"Lower system version": "Знизити версію системи",
"Install Selected files?": "Встановити вибрані файли?",
"Installed": "Встановлено",
"Installed ": "Встановлено ",
"FTP Install": "Встановлення через FTP",
"USB Install": "Встановлення через USB",
"GameCard Install": "Встановлення з картриджа",
"FTP Install (EXPERIMENTAL)": "Встановлення через FTP (ЕКСПЕРИМЕНТАЛЬНО)",
"USB": "USB",
"GameCard": "Картридж",
"Disable MTP for usb install": "Вимкнути MTP для встановлення через USB",
"Re-enabled MTP": "MTP знову увімкнено",
"Waiting for connection...": "Очікування підключення...",
"Transferring data...": "Передача даних...",
"Ftp install success!": "Встановлення через FTP успішно завершено.",
"Ftp install failed!": "Встановлення через FTP не вдалося.",
"Usb install success!": "Встановлення через USB успішно завершено.",
"Usb install failed!": "Встановлення через USB не вдалося.",
"Gc install success!": "Встановлення з картриджа успішно завершено.",
"Gc install failed!": "Встановлення з картриджа не вдалося.",
"Installed via usb": "Встановлено через USB",
"Failed to install via FTP, press B to exit...": "Не вдалося встановити через FTP, натисніть B для виходу...",
"Failed to init usb, press B to exit...": "Не вдалося ініціалізувати USB, натисніть B для виходу...",
"Press B to exit...": "Натисніть B для виходу...",
"Connection Type: WiFi | Strength:": "Тип підключення: WiFi | Сила сигналу:",
"Connection Type: WiFi | Strength: ": "Тип підключення: WiFi | Сила сигналу: ",
"Connection Type: Ethernet": "Тип підключення: Ethernet",
"Connection Type: None": "Тип підключення: Немає",
"Host:": "Хост:",
"Port:": "Порт:",
"Username:": "Ім'я користувача:",
"Password:": "Пароль:",
"SSID:": "SSID:",
"Passphrase:": "Кодова фраза:",
"Installing ": "Встановлення ",
"Uninstalling ": "Видалення ",
"Deleting ": "Видалення ",
"Deleting": "Видалення",
"Pasting ": "Вставлення ",
"Pasting": "Вставлення",
"Removing ": "Видалення ",
"Scanning ": "Сканування ",
"Creating ": "Створення ",
"Copying ": "Копіювання ",
"Trying to load ": "Спроба завантажити ",
"Downloading ": "Завантаження ",
"Downloaded ": "Завантажено ",
"Removed ": "Видалено ",
"Checking MD5": "Перевірка MD5",
"Loading...": "Завантаження...",
"Loading": "Завантаження",
"Empty!": "Пусто!",
"Not Ready...": "Не готово...",
"Error loading page!": "Помилка завантаження сторінки!",
"Update avaliable: ": "Доступне оновлення: ",
"Download update: ": "Завантажити оновлення: ",
"Updated to ": "Оновлено до ",
"Press OK to restart Sphaira": "Натисніть OK для перезапуску Sphaira",
"Restart Sphaira?": "Перезапустити Sphaira?",
"Failed to download update": "Не вдалося завантажити оновлення",
"Restore hbmenu?": "Відновити hbmenu?",
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "Не вдалося знайти /switch/hbmenu.nro\nВикористовуйте Магазин програм для перевстановлення hbmenu",
"Failed to restore hbmenu, please re-download hbmenu": "Не вдалося відновити hbmenu, будь ласка, завантажте hbmenu знову",
"Failed to restore hbmenu, using sphaira instead": "Не вдалося відновити hbmenu, замість цього використовується Sphaira",
"Restored hbmenu, closing sphaira": "hbmenu відновлено, закриття Sphaira",
"Restored hbmenu": "hbmenu відновлено",
"Delete Selected files?": "Видалити вибрані файли?",
"Completely remove ": "Повністю видалити ",
"Are you sure you want to delete ": "Ви впевнені, що хочете видалити ",
"Are you sure you wish to cancel?": "Ви впевнені, що хочете скасувати?",
"Audio disabled due to suspended game": "Аудіо вимкнено через призупинену програму",
"If this message appears repeatedly, please open an issue.": "Якщо це повідомлення з'являється повторно, будь ласка, повідомте про проблему."
}

View File

@@ -66,6 +66,7 @@
"Select Theme": "Chọn Theme",
"Shuffle": "Trộn",
"Music": "Âm nhạc",
"12 Hour Time": "",
"Network": "Mạng",
"Network Options": "Tuỳ chọn mạng",
"Ftp": "FTP",
@@ -251,5 +252,6 @@
"Completely remove ": "Đã gỡ thành công ",
"Are you sure you want to delete ": "Bạn có muốn xoá ",
"Are you sure you wish to cancel?": "Bạn có chắn muốn huỷ không?",
"Audio disabled due to suspended game": "",
"If this message appears repeatedly, please open an issue.": "Nếu thấy tin nhắn này, hãy báo lỗi."
}
}

View File

@@ -21,6 +21,8 @@
"Remove": "删除",
"Restore": "恢复",
"Download": "下载",
"Next": "下一项",
"Prev": "上一项",
"Next Page": "下一页",
"Prev Page": "上一页",
"Unstar": "取消星标",
@@ -30,9 +32,9 @@
"Sd": "SD卡",
"Image System memory": "主机内存图像",
"Image microSD card": "SD卡图像",
"Slow": "",
"Normal": "",
"Fast": "",
"Slow": "",
"Normal": "正常",
"Fast": "",
"Yes": "是",
"No": "否",
"Enabled": "启用",
@@ -66,6 +68,8 @@
"Select Theme": "选择主题",
"Shuffle": "随机播放",
"Music": "音乐",
"12 Hour Time": "12小时制时间",
"Download Default Music": "下载默认音乐",
"Network": "网络",
"Network Options": "网络选项",
"Ftp": "FTP",
@@ -99,7 +103,7 @@
"Install forwarders": "允许安装前端应用",
"Install location": "安装位置",
"Show install warning": "显示安装警告",
"Text scroll speed": "",
"Text scroll speed": "文本滚动速度",
"FileBrowser": "文件浏览",
"%zd files": "%zd 个文件",
@@ -114,6 +118,7 @@
"Paste ": "粘贴 ",
" file(s)?": "个文件(夹)",
"Rename": "重命名",
"Compress to zip": "压缩到zip",
"Set New File Name": "输入新命名",
"Advanced": "高级",
"Advanced Options": "高级选项",
@@ -164,12 +169,19 @@
"More by Author": "作者更多作品",
"Leave Feedback": "留言反馈",
"Game Options": "游戏选项",
"Launch random game": "开启随机游戏",
"List meta records": "列出元数据记录",
"Entries": "条目",
"Delete entity": "删除整体",
"Hide forwarders": "隐藏前端启动",
"Irs": "红外成像",
"Ambient Noise Level: ": "环境噪声等级:",
"Controller": "控制器",
"Pad ": "手柄 ",
" (Available)": " (可用的)",
" (Unsupported)": "",
" (Unsupported)": " (不支持的)",
" (Unconnected)": " (未连接)",
"HandHeld": "掌机模式",
"Rotation": "旋转",
@@ -215,6 +227,58 @@
"Downloading json": "正在下载 json",
"Select asset to download for ": "选择要下载的资源用于 ",
"Install Options": "安装选项",
"Install options": "安装选项",
"Boost CPU clock": "提升 CPU 频率",
"Allow downgrade": "允许降级",
"Skip if already installed": "若已安装则跳过",
"Ticket only": "仅安装票据",
"Patch ticket": "修补票据",
"Skip base": "跳过基础部分",
"Skip patch": "跳过补丁",
"Skip dlc": "跳过 DLC可下载内容",
"Skip data patch": "跳过数据补丁",
"Skip ticket": "跳过票据",
"skip NCA hash verify": "跳过 NCA 哈希验证",
"Skip RSA header verify": "跳过 RSA 头部验证",
"Skip RSA NPDM verify": "跳过 RSA NPDM 验证",
"Ignore distribution bit": "忽略分布位",
"Convert to standard crypto": "转换为标准加密方式",
"Lower master key": "降低主密钥",
"Lower system version": "降低系统版本",
"Install Selected files?": "安装所选文件?",
"Installed": "已安装",
"FTP Install": "通过 FTP 安装",
"USB Install": "通过 USB 安装",
"GameCard Install": "卡带安装",
"FTP Install (EXPERIMENTAL)": "通过 FTP 安装(实验性)",
"USB": "USB",
"GameCard": "卡带",
"Disable MTP for usb install": "暂时禁用 USB 安装的 MTP 功能",
"Re-enabled MTP": "重新启用 MTP",
"Waiting for connection...": "等待连接中...",
"Transferring data...": "正在传输数据...",
"Ftp install success!": "通过 FTP 安装成功。",
"Ftp install failed!": "通过 FTP 安装失败。",
"Usb install success!": "通过 USB 安装成功。",
"Usb install failed!": "通过 USB 安装失败。",
"Gc install success!": "游戏安装成功。",
"Gc install failed!": "游戏安装失败。",
"Installed via usb": "通过 USB 安装",
"Failed to install via FTP, press B to exit...": "通过 FTP 安装失败,按 B 键退出...",
"Failed to init usb, press B to exit...": "USB 初始化失败,按 B 键退出...",
"Press B to exit...": "按 B 键退出...",
"Connection Type: WiFi | Strength:": "连接类型WiFi | 信号强度:",
"Connection Type: Ethernet": "连接类型:以太网",
"Connection Type: None": "连接类型:无",
"Host:": "主机:",
"Port:": "端口:",
"Username:": "用户名:",
"Password:": "密码:",
"SSID:": "网络名称:",
"Passphrase:": "密码:",
"Installing ": "正在安装 ",
"Uninstalling ": "正在卸载 ",
"Deleting ": "正在删除 ",
@@ -238,7 +302,7 @@
"Update avaliable: ": "有可用更新!",
"Download update: ": "下载更新:",
"Updated to ": "更新至 ",
"Press OK to restart Sphaira": "",
"Press OK to restart Sphaira": "按OK键以重启shphaira菜单",
"Restart Sphaira?": "重启 Sphaira",
"Failed to download update": "更新下载失败",
"Restore hbmenu?": "恢复 hbmenu",
@@ -251,5 +315,6 @@
"Completely remove ": "彻底删除 ",
"Are you sure you want to delete ": "您确定要删除吗 ",
"Are you sure you wish to cancel?": "您确定要取消吗?",
"If this message appears repeatedly, please open an issue.": "如果此消息反复出现,请提交一个 issue。"
"Audio disabled due to suspended game": "由于游戏暂停,音频已禁用",
"If this message appears repeatedly, please open an issue.": "若此消息反复出现,请提交问题报告。"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@@ -0,0 +1,15 @@
[meta]
name=Black alt-icons-SP
author=spkatsi
version=1.0.0
inherit=romfs:/themes/base_black_theme.ini
[theme]
icon_audio = romfs:/theme/icons-sp/icon_SP_audio.png
icon_video = romfs:/theme/icons-sp/icon_SP_video.png
icon_image = romfs:/theme/icons-sp/icon_SP_image.png
icon_file = romfs:/theme/icons-sp/icon_SP_file.png
icon_folder = romfs:/theme/icons-sp/icon_SP_folder.png
icon_zip = romfs:/theme/icons-sp/icon_SP_zip.png
icon_nro = romfs:/theme/icons-sp/icon_SP_nro.png

View File

@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.13)
set(sphaira_VERSION 0.6.0)
set(sphaira_VERSION 0.10.3)
project(sphaira
VERSION ${sphaira_VERSION}
@@ -46,6 +46,11 @@ add_executable(sphaira
source/ui/menus/menu_base.cpp
source/ui/menus/themezer.cpp
source/ui/menus/ghdl.cpp
source/ui/menus/usb_menu.cpp
source/ui/menus/ftp_menu.cpp
source/ui/menus/gc_menu.cpp
source/ui/menus/game_menu.cpp
source/ui/menus/grid_menu_base.cpp
source/ui/error_box.cpp
source/ui/notification.cpp
@@ -57,7 +62,7 @@ add_executable(sphaira
source/ui/sidebar.cpp
source/ui/widget.cpp
source/ui/list.cpp
source/ui/bubbles.cpp
source/ui/scrolling_text.cpp
source/app.cpp
source/download.cpp
@@ -65,20 +70,42 @@ add_executable(sphaira
source/evman.cpp
source/fs.cpp
source/image.cpp
source/location.cpp
source/log.cpp
source/main.cpp
source/nro.cpp
source/nxlink.cpp
source/owo.cpp
source/swkbd.cpp
source/web.cpp
source/i18n.cpp
source/ftpsrv_helper.cpp
source/usb/base.cpp
source/usb/usbds.cpp
source/usb/usbhs.cpp
source/usb/usb_uploader.cpp
source/yati/yati.cpp
source/yati/container/nsp.cpp
source/yati/container/xci.cpp
source/yati/source/file.cpp
source/yati/source/stdio.cpp
source/yati/source/usb.cpp
source/yati/source/stream.cpp
source/yati/source/stream_file.cpp
source/yati/nx/es.cpp
source/yati/nx/keys.cpp
source/yati/nx/nca.cpp
source/yati/nx/ncm.cpp
source/yati/nx/ns.cpp
source/yati/nx/nxdumptool_rsa.c
)
target_compile_definitions(sphaira PRIVATE
-DAPP_VERSION="${sphaira_VERSION}"
-DAPP_VERSION_HASH="${sphaira_VERSION_HASH}"
-DCURL_NO_OLDIES=1
)
target_compile_options(sphaira PRIVATE
@@ -127,13 +154,14 @@ set(FETCHCONTENT_QUIET FALSE)
FetchContent_Declare(ftpsrv
GIT_REPOSITORY https://github.com/ITotalJustice/ftpsrv.git
GIT_TAG 1.2.2
# GIT_TAG 1.2.2
GIT_TAG f8a30fd
SOURCE_SUBDIR NONE
)
FetchContent_Declare(libhaze
GIT_REPOSITORY https://github.com/ITotalJustice/libhaze.git
GIT_TAG 3244b9e
GIT_TAG 04f1526
)
FetchContent_Declare(libpulsar
@@ -161,6 +189,24 @@ FetchContent_Declare(minIni
GIT_TAG 11cac8b
)
FetchContent_Declare(zstd
GIT_REPOSITORY https://github.com/facebook/zstd.git
GIT_TAG v1.5.7
SOURCE_SUBDIR build/cmake
)
set(USE_NEW_ZSTD ON)
set(ZSTD_BUILD_STATIC ON)
set(ZSTD_BUILD_SHARED OFF)
set(ZSTD_BUILD_COMPRESSION OFF)
set(ZSTD_BUILD_DECOMPRESSION ON)
set(ZSTD_BUILD_DICTBUILDER OFF)
set(ZSTD_LEGACY_SUPPORT OFF)
set(ZSTD_MULTITHREAD_SUPPORT OFF)
set(ZSTD_BUILD_PROGRAMS OFF)
set(ZSTD_BUILD_TESTS OFF)
set(MININI_LIB_NAME minIni)
set(MININI_USE_STDIO ON)
set(MININI_USE_NX ON)
@@ -195,6 +241,7 @@ FetchContent_MakeAvailable(
stb
minIni
yyjson
zstd
)
set(FTPSRV_LIB_BUILD TRUE)
@@ -238,31 +285,6 @@ if (USE_VFS_GC)
)
endif()
# todo: upstream cmake
add_library(libhaze
${libhaze_SOURCE_DIR}/source/async_usb_server.cpp
${libhaze_SOURCE_DIR}/source/device_properties.cpp
${libhaze_SOURCE_DIR}/source/event_reactor.cpp
${libhaze_SOURCE_DIR}/source/haze.cpp
${libhaze_SOURCE_DIR}/source/ptp_object_database.cpp
${libhaze_SOURCE_DIR}/source/ptp_object_heap.cpp
${libhaze_SOURCE_DIR}/source/ptp_responder_android_operations.cpp
${libhaze_SOURCE_DIR}/source/ptp_responder_mtp_operations.cpp
${libhaze_SOURCE_DIR}/source/ptp_responder_ptp_operations.cpp
${libhaze_SOURCE_DIR}/source/ptp_responder.cpp
${libhaze_SOURCE_DIR}/source/usb_session.cpp
)
target_include_directories(libhaze PUBLIC ${libhaze_SOURCE_DIR}/include)
set_target_properties(libhaze PROPERTIES
C_STANDARD 11
C_EXTENSIONS ON
CXX_STANDARD 20
CXX_EXTENSIONS ON
# force optimisations in debug mode as otherwise vapor errors
# due to force_inline attribute failing...
COMPILE_OPTIONS "$<$<CONFIG:Debug>:-Os>"
)
add_library(stb INTERFACE)
target_include_directories(stb INTERFACE ${stb_SOURCE_DIR})
@@ -274,6 +296,11 @@ find_package(CURL REQUIRED)
find_path(mbedtls_inc mbedtls REQUIRED)
find_library(mbedcrypto_lib mbedcrypto REQUIRED)
if (NOT USE_NEW_ZSTD)
find_path(zstd_inc zstd.h REQUIRED)
find_library(zstd_lib zstd REQUIRED)
endif()
set_target_properties(sphaira PROPERTIES
C_STANDARD 11
C_EXTENSIONS ON
@@ -296,6 +323,15 @@ target_link_libraries(sphaira PRIVATE
${mbedcrypto_lib}
)
if (USE_NEW_ZSTD)
message(STATUS "USING UPSTREAM ZSTD")
target_link_libraries(sphaira PRIVATE libzstd_static)
else()
message(STATUS "USING LOCAL ZSTD")
target_link_libraries(sphaira PRIVATE ${zstd_lib})
target_include_directories(sphaira PRIVATE ${zstd_inc})
endif()
target_include_directories(sphaira PRIVATE
include
${minizip_inc}
@@ -330,7 +366,7 @@ nx_generate_nacp(
OUTPUT sphaira.nacp
NAME ${CMAKE_PROJECT_NAME}
AUTHOR TotalJustice
VERSION ${CMAKE_PROJECT_VERSION}
VERSION ${sphaira_VERSION}
)
# create nro

View File

@@ -8,6 +8,7 @@
#include "owo.hpp"
#include "option.hpp"
#include "fs.hpp"
#include "log.hpp"
#include <switch.h>
#include <vector>
@@ -44,6 +45,8 @@ public:
~App();
void Loop();
static App* GetApp();
static void Exit();
static void ExitRestart();
static auto GetVg() -> NVGcontext*;
@@ -62,7 +65,7 @@ public:
static void SetTheme(s64 theme_index);
static auto GetThemeIndex() -> s64;
static auto GetDefaultImage(int* w = nullptr, int* h = nullptr) -> int;
static auto GetDefaultImage() -> int;
// returns argv[0]
static auto GetExePath() -> fs::FsPath;
@@ -75,10 +78,12 @@ public:
static auto GetLogEnable() -> bool;
static auto GetReplaceHbmenuEnable() -> bool;
static auto GetInstallEnable() -> bool;
static auto GetInstallSysmmcEnable() -> bool;
static auto GetInstallEmummcEnable() -> bool;
static auto GetInstallSdEnable() -> bool;
static auto GetInstallPrompt() -> bool;
static auto GetThemeShuffleEnable() -> bool;
static auto GetThemeMusicEnable() -> bool;
static auto Get12HourTimeEnable() -> bool;
static auto GetLanguage() -> long;
static auto GetTextScrollSpeed() -> long;
@@ -87,11 +92,12 @@ public:
static void SetNxlinkEnable(bool enable);
static void SetLogEnable(bool enable);
static void SetReplaceHbmenuEnable(bool enable);
static void SetInstallEnable(bool enable);
static void SetInstallSysmmcEnable(bool enable);
static void SetInstallEmummcEnable(bool enable);
static void SetInstallSdEnable(bool enable);
static void SetInstallPrompt(bool enable);
static void SetThemeShuffleEnable(bool enable);
static void SetThemeMusicEnable(bool enable);
static void Set12HourTimeEnable(bool enable);
static void SetLanguage(long index);
static void SetTextScrollSpeed(long index);
@@ -100,6 +106,13 @@ public:
static void PlaySoundEffect(SoundEffect effect);
static void DisplayThemeOptions(bool left_side = true);
// todo:
static void DisplayNetworkOptions(bool left_side = true);
static void DisplayMiscOptions(bool left_side = true);
static void DisplayAdvancedOptions(bool left_side = true);
static void DisplayInstallOptions(bool left_side = true);
void Draw();
void Update();
void Poll();
@@ -114,11 +127,45 @@ public:
void ScanThemes(const std::string& path);
void ScanThemeEntries();
// helper that converts 1.2.3 to a u32 used for comparisons.
static auto GetVersionFromString(const char* str) -> u32;
static auto IsVersionNewer(const char* current, const char* new_version) -> u32;
static auto IsApplication() -> bool {
const auto type = appletGetAppletType();
return type == AppletType_Application || type == AppletType_SystemApplication;
}
static auto IsApplet() -> bool {
return !IsApplication();
}
// returns true if launched in applet mode with a title suspended in the background.
static auto IsAppletWithSuspendedApp() -> bool {
R_UNLESS(IsApplet(), false);
R_TRY_RESULT(pmdmntInitialize(), false);
ON_SCOPE_EXIT(pmdmntExit());
u64 pid;
return R_SUCCEEDED(pmdmntGetApplicationProcessId(&pid));
}
static auto IsEmunand() -> bool {
alignas(0x1000) struct EmummcPaths {
char unk[0x80];
char nintendo[0x80];
} paths{};
SecmonArgs args{};
args.X[0] = 0xF0000404; /* smcAmsGetEmunandConfig */
args.X[1] = 0; /* EXO_EMUMMC_MMC_NAND*/
args.X[2] = (u64)&paths; /* out path */
svcCallSecureMonitor(&args);
return (paths.unk[0] != '\0') || (paths.nintendo[0] != '\0');
}
// private:
static constexpr inline auto CONFIG_PATH = "/config/sphaira/config.ini";
static constexpr inline auto PLAYLOG_PATH = "/config/sphaira/playlog.ini";
@@ -157,12 +204,33 @@ public:
option::OptionBool m_ftp_enabled{INI_SECTION, "ftp_enabled", false};
option::OptionBool m_log_enabled{INI_SECTION, "log_enabled", false};
option::OptionBool m_replace_hbmenu{INI_SECTION, "replace_hbmenu", false};
option::OptionBool m_install{INI_SECTION, "install", false};
option::OptionBool m_theme_music{INI_SECTION, "theme_music", true};
option::OptionBool m_12hour_time{INI_SECTION, "12hour_time", false};
option::OptionLong m_language{INI_SECTION, "language", 0}; // auto
option::OptionString m_right_side_menu{INI_SECTION, "right_side_menu", "Appstore"};
// install options
option::OptionBool m_install_sysmmc{INI_SECTION, "install_sysmmc", false};
option::OptionBool m_install_emummc{INI_SECTION, "install_emummc", false};
option::OptionBool m_install_sd{INI_SECTION, "install_sd", true};
option::OptionLong m_install_prompt{INI_SECTION, "install_prompt", true};
option::OptionBool m_theme_shuffle{INI_SECTION, "theme_shuffle", false};
option::OptionBool m_theme_music{INI_SECTION, "theme_music", true};
option::OptionLong m_language{INI_SECTION, "language", 0}; // auto
option::OptionLong m_boost_mode{INI_SECTION, "boost_mode", false};
option::OptionBool m_allow_downgrade{INI_SECTION, "allow_downgrade", false};
option::OptionBool m_skip_if_already_installed{INI_SECTION, "skip_if_already_installed", true};
option::OptionBool m_ticket_only{INI_SECTION, "ticket_only", false};
option::OptionBool m_skip_base{INI_SECTION, "skip_base", false};
option::OptionBool m_skip_patch{INI_SECTION, "skip_patch", false};
option::OptionBool m_skip_addon{INI_SECTION, "skip_addon", false};
option::OptionBool m_skip_data_patch{INI_SECTION, "skip_data_patch", false};
option::OptionBool m_skip_ticket{INI_SECTION, "skip_ticket", false};
option::OptionBool m_skip_nca_hash_verify{INI_SECTION, "skip_nca_hash_verify", false};
option::OptionBool m_skip_rsa_header_fixed_key_verify{INI_SECTION, "skip_rsa_header_fixed_key_verify", false};
option::OptionBool m_skip_rsa_npdm_fixed_key_verify{INI_SECTION, "skip_rsa_npdm_fixed_key_verify", false};
option::OptionBool m_ignore_distribution_bit{INI_SECTION, "ignore_distribution_bit", false};
option::OptionBool m_convert_to_standard_crypto{INI_SECTION, "convert_to_standard_crypto", false};
option::OptionBool m_lower_master_key{INI_SECTION, "lower_master_key", false};
option::OptionBool m_lower_system_version{INI_SECTION, "lower_system_version", false};
// todo: move this into it's own menu
option::OptionLong m_text_scroll_speed{"accessibility", "text_scroll_speed", 1}; // normal

View File

@@ -13,10 +13,14 @@ namespace sphaira::curl {
enum {
Flag_None = 0,
// requests to download send etag in the header.
// the received etag is then saved on success.
// this api is only available on downloading to file.
Flag_Cache = 1 << 0,
// sets CURLOPT_NOBODY.
Flag_NoBody = 1 << 1,
};
enum class Priority {
@@ -29,7 +33,9 @@ struct ApiResult;
using Path = fs::FsPath;
using OnComplete = std::function<void(ApiResult& result)>;
using OnProgress = std::function<bool(u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow)>;
using OnProgress = std::function<bool(s64 dltotal, s64 dlnow, s64 ultotal, s64 ulnow)>;
using OnUploadCallback = std::function<size_t(void *ptr, size_t size)>;
using OnUploadSeek = std::function<bool(s64 offset)>;
using StopToken = std::stop_token;
struct Url {
@@ -62,6 +68,55 @@ struct Flags {
u32 m_flags{Flag_None};
};
struct Port {
Port() = default;
Port(u16 port) : m_port{port} {}
u16 m_port{};
};
struct CustomRequest {
CustomRequest() = default;
CustomRequest(const std::string& str) : m_str{str} {}
std::string m_str;
};
struct UserPass {
UserPass() = default;
UserPass(const std::string& user) : m_user{user} {}
UserPass(const std::string& user, const std::string& pass) : m_user{user}, m_pass{pass} {}
std::string m_user;
std::string m_pass;
};
struct UploadInfo {
UploadInfo() = default;
UploadInfo(const std::string& name) : m_name{name} {}
UploadInfo(const std::string& name, s64 size, OnUploadCallback cb) : m_name{name}, m_size{size}, m_callback{cb} {}
UploadInfo(const std::string& name, const std::vector<u8>& data) : m_name{name}, m_data{data} {}
std::string m_name{};
std::vector<u8> m_data{};
s64 m_size{};
OnUploadCallback m_callback{};
};
struct Bearer {
Bearer() = default;
Bearer(const std::string& str) : m_str{str} {}
std::string m_str;
};
struct PubKey {
PubKey() = default;
PubKey(const std::string& str) : m_str{str} {}
std::string m_str;
};
struct PrivKey {
PrivKey() = default;
PrivKey(const std::string& str) : m_str{str} {}
std::string m_str;
};
struct ApiResult {
bool success;
long code;
@@ -76,16 +131,32 @@ struct DownloadEventData {
StopToken stoken;
};
// helper that generates the api using an location.
#define CURL_LOCATION_TO_API(loc) \
curl::Url{loc.url}, \
curl::UserPass{loc.user, loc.pass}, \
curl::Bearer{loc.bearer}, \
curl::PubKey{loc.pub_key}, \
curl::PrivKey{loc.priv_key}, \
curl::Port(loc.port)
auto Init() -> bool;
void Exit();
// sync functions
auto ToMemory(const Api& e) -> ApiResult;
auto ToFile(const Api& e) -> ApiResult;
auto FromMemory(const Api& e) -> ApiResult;
auto FromFile(const Api& e) -> ApiResult;
// async functions
auto ToMemoryAsync(const Api& e) -> bool;
auto ToFileAsync(const Api& e) -> bool;
auto FromMemoryAsync(const Api& e) -> bool;
auto FromFileAsync(const Api& e) -> bool;
// uses curl to convert string to their %XX
auto EscapeString(const std::string& str) -> std::string;
struct Api {
Api() = default;
@@ -104,6 +175,15 @@ struct Api {
}
}
template <typename... Ts>
auto From(Ts&&... ts) {
if constexpr(std::disjunction_v<std::is_same<Path, Ts>...>) {
return FromFile(std::forward<Ts>(ts)...);
} else {
return FromMemory(std::forward<Ts>(ts)...);
}
}
template <typename... Ts>
auto ToAsync(Ts&&... ts) {
if constexpr(std::disjunction_v<std::is_same<Path, Ts>...>) {
@@ -113,6 +193,15 @@ struct Api {
}
}
template <typename... Ts>
auto FromAsync(Ts&&... ts) {
if constexpr(std::disjunction_v<std::is_same<Path, Ts>...>) {
return FromFileAsync(std::forward<Ts>(ts)...);
} else {
return FromMemoryAsync(std::forward<Ts>(ts)...);
}
}
template <typename... Ts>
auto ToMemory(Ts&&... ts) {
static_assert(std::disjunction_v<std::is_same<Url, Ts>...>, "Url must be specified");
@@ -122,6 +211,16 @@ struct Api {
return curl::ToMemory(*this);
}
template <typename... Ts>
auto FromMemory(Ts&&... ts) {
static_assert(std::disjunction_v<std::is_same<Url, Ts>...>, "Url must be specified");
static_assert(std::disjunction_v<std::is_same<UploadInfo, Ts>...>, "UploadInfo must be specified");
static_assert(!std::disjunction_v<std::is_same<Path, Ts>...>, "Path must not valid for memory");
static_assert(!std::disjunction_v<std::is_same<OnComplete, Ts>...>, "OnComplete must not be specified");
Api::set_option(std::forward<Ts>(ts)...);
return curl::FromMemory(*this);
}
template <typename... Ts>
auto ToFile(Ts&&... ts) {
static_assert(std::disjunction_v<std::is_same<Url, Ts>...>, "Url must be specified");
@@ -131,6 +230,16 @@ struct Api {
return curl::ToFile(*this);
}
template <typename... Ts>
auto FromFile(Ts&&... ts) {
static_assert(std::disjunction_v<std::is_same<Url, Ts>...>, "Url must be specified");
static_assert(std::disjunction_v<std::is_same<Path, Ts>...>, "Path must be specified");
static_assert(std::disjunction_v<std::is_same<UploadInfo, Ts>...>, "UploadInfo must be specified");
static_assert(!std::disjunction_v<std::is_same<OnComplete, Ts>...>, "OnComplete must not be specified");
Api::set_option(std::forward<Ts>(ts)...);
return curl::FromFile(*this);
}
template <typename... Ts>
auto ToMemoryAsync(Ts&&... ts) {
static_assert(std::disjunction_v<std::is_same<Url, Ts>...>, "Url must be specified");
@@ -141,6 +250,17 @@ struct Api {
return curl::ToMemoryAsync(*this);
}
template <typename... Ts>
auto FromMemoryAsync(Ts&&... ts) {
static_assert(std::disjunction_v<std::is_same<Url, Ts>...>, "Url must be specified");
static_assert(std::disjunction_v<std::is_same<UploadInfo, Ts>...>, "UploadInfo must be specified");
static_assert(std::disjunction_v<std::is_same<OnComplete, Ts>...>, "OnComplete must be specified");
static_assert(!std::disjunction_v<std::is_same<Path, Ts>...>, "Path must not valid for memory");
static_assert(std::disjunction_v<std::is_same<StopToken, Ts>...>, "StopToken must be specified");
Api::set_option(std::forward<Ts>(ts)...);
return curl::FromMemoryAsync(*this);
}
template <typename... Ts>
auto ToFileAsync(Ts&&... ts) {
static_assert(std::disjunction_v<std::is_same<Url, Ts>...>, "Url must be specified");
@@ -151,62 +271,55 @@ struct Api {
return curl::ToFileAsync(*this);
}
auto& GetUrl() const {
return m_url.m_str;
}
auto& GetFields() const {
return m_fields.m_str;
}
auto& GetHeader() const {
return m_header;
}
auto& GetFlags() const {
return m_flags.m_flags;
}
auto& GetPath() const {
return m_path;
}
auto& GetOnComplete() const {
return m_on_complete;
}
auto& GetOnProgress() const {
return m_on_progress;
}
auto& GetPriority() const {
return m_prio;
}
auto& GetToken() const {
return m_stoken;
template <typename... Ts>
auto FromFileAsync(Ts&&... ts) {
static_assert(std::disjunction_v<std::is_same<Url, Ts>...>, "Url must be specified");
static_assert(std::disjunction_v<std::is_same<Path, Ts>...>, "Path must be specified");
static_assert(std::disjunction_v<std::is_same<UploadInfo, Ts>...>, "UploadInfo must be specified");
static_assert(std::disjunction_v<std::is_same<OnComplete, Ts>...>, "OnComplete must be specified");
static_assert(std::disjunction_v<std::is_same<StopToken, Ts>...>, "StopToken must be specified");
Api::set_option(std::forward<Ts>(ts)...);
return curl::FromFileAsync(*this);
}
private:
void SetOption(Url&& v) {
m_url = v;
}
void SetOption(Fields&& v) {
m_fields = v;
}
void SetOption(Header&& v) {
m_header = v;
}
void SetOption(Flags&& v) {
m_flags = v;
}
void SetOption(Path&& v) {
m_path = v;
}
void SetOption(OnComplete&& v) {
m_on_complete = v;
}
void SetOption(OnProgress&& v) {
m_on_progress = v;
}
void SetOption(Priority&& v) {
m_prio = v;
}
void SetOption(StopToken&& v) {
m_stoken = v;
}
void SetUpload(bool enable) { m_is_upload = enable; }
auto IsUpload() const { return m_is_upload; }
auto& GetUrl() const { return m_url.m_str; }
auto& GetFields() const { return m_fields.m_str; }
auto& GetHeader() const { return m_header; }
auto& GetFlags() const { return m_flags.m_flags; }
auto& GetPath() const { return m_path; }
auto& GetPort() const { return m_port.m_port; }
auto& GetCustomRequest() const { return m_custom_request.m_str; }
auto& GetUserPass() const { return m_userpass; }
auto& GetBearer() const { return m_bearer.m_str; }
auto& GetPubKey() const { return m_pub_key.m_str; }
auto& GetPrivKey() const { return m_priv_key.m_str; }
auto& GetUploadInfo() const { return m_info; }
auto& GetOnComplete() const { return m_on_complete; }
auto& GetOnProgress() const { return m_on_progress; }
auto& GetOnUploadSeek() const { return m_on_upload_seek; }
auto& GetPriority() const { return m_prio; }
auto& GetToken() const { return m_stoken; }
void SetOption(Url&& v) { m_url = v; }
void SetOption(Fields&& v) { m_fields = v; }
void SetOption(Header&& v) { m_header = v; }
void SetOption(Flags&& v) { m_flags = v; }
void SetOption(Path&& v) { m_path = v; }
void SetOption(Port&& v) { m_port = v; }
void SetOption(CustomRequest&& v) { m_custom_request = v; }
void SetOption(UserPass&& v) { m_userpass = v; }
void SetOption(Bearer&& v) { m_bearer = v; }
void SetOption(PubKey&& v) { m_pub_key = v; }
void SetOption(PrivKey&& v) { m_priv_key = v; }
void SetOption(UploadInfo&& v) { m_info = v; }
void SetOption(OnComplete&& v) { m_on_complete = v; }
void SetOption(OnProgress&& v) { m_on_progress = v; }
void SetOption(OnUploadSeek&& v) { m_on_upload_seek = v; }
void SetOption(Priority&& v) { m_prio = v; }
void SetOption(StopToken&& v) { m_stoken = v; }
template <typename T>
void set_option(T&& t) {
@@ -220,16 +333,25 @@ private:
}
private:
Url m_url;
Url m_url{};
Fields m_fields{};
Header m_header{};
Flags m_flags{};
Path m_path{};
OnComplete m_on_complete{nullptr};
OnProgress m_on_progress{nullptr};
Port m_port{};
CustomRequest m_custom_request{};
UserPass m_userpass{};
Bearer m_bearer{};
PubKey m_pub_key{};
PrivKey m_priv_key{};
UploadInfo m_info{};
OnComplete m_on_complete{};
OnProgress m_on_progress{};
OnUploadSeek m_on_upload_seek{};
Priority m_prio{Priority::High};
std::stop_source m_stop_source{};
StopToken m_stoken{m_stop_source.get_token()};
bool m_is_upload{};
};
} // namespace sphaira::curl

View File

@@ -454,4 +454,10 @@ struct FsNativeContentStorage final : FsNative {
}
};
struct FsNativeGameCard final : FsNative {
FsNativeGameCard(const FsGameCardHandle* handle, FsGameCardPartition partition, bool ignore_read_only = true) : FsNative{ignore_read_only} {
m_open_result = fsOpenGameCardFileSystem(&m_fs, handle, partition);
}
};
} // namespace fs

View File

@@ -1,8 +1,22 @@
#pragma once
#include <functional>
namespace sphaira::ftpsrv {
bool Init();
void Exit();
using OnInstallStart = std::function<bool(void* user, const char* path)>;
using OnInstallWrite = std::function<bool(void* user, const void* buf, size_t size)>;
using OnInstallClose = std::function<void(void* user)>;
void InitInstallMode(void* user, OnInstallStart on_start, OnInstallWrite on_write, OnInstallClose on_close);
void DisableInstallMode();
unsigned GetPort();
bool IsAnon();
const char* GetUser();
const char* GetPass();
} // namespace sphaira::ftpsrv

View File

@@ -1,18 +1,19 @@
#pragma once
#include <string>
#include <string_view>
namespace sphaira::i18n {
bool init(long index);
void exit();
std::string get(const char* str);
std::string get(std::string_view str);
} // namespace sphaira::i18n
inline namespace literals {
std::string operator"" _i18n(const char* str, size_t len);
std::string operator""_i18n(const char* str, size_t len);
} // namespace literals

View File

@@ -0,0 +1,24 @@
#pragma once
#include <string>
#include <vector>
#include <switch.h>
namespace sphaira::location {
struct Entry {
std::string name{};
std::string url{};
std::string user{};
std::string pass{};
std::string bearer{};
std::string pub_key{};
std::string priv_key{};
u16 port{};
};
using Entries = std::vector<Entry>;
auto Load() -> Entries;
void Add(const Entry& e);
} // namespace sphaira::location

View File

@@ -1,24 +1,33 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#define sphaira_USE_LOG 1
#include <cstdarg>
#include <stdarg.h>
#if sphaira_USE_LOG
auto log_file_init() -> bool;
auto log_nxlink_init() -> bool;
bool log_file_init();
bool log_nxlink_init();
void log_file_exit();
void log_nxlink_exit();
void log_write(const char* s, ...) __attribute__ ((format (printf, 1, 2)));
void log_write_arg(const char* s, std::va_list& v);
void log_write_arg(const char* s, va_list* v);
#else
inline auto log_file_init() -> bool {
inline bool log_file_init() {
return true;
}
inline auto log_nxlink_init() -> bool {
inline bool log_nxlink_init() {
return true;
}
#define log_file_exit()
#define log_nxlink_exit()
#define log_write(...)
#define log_write_arg(...)
#endif
#ifdef __cplusplus
}
#endif

View File

@@ -14,8 +14,12 @@ struct OptionBase {
{}
auto Get() -> T;
auto GetOr(const char* name) -> T;
void Set(T value);
private:
auto GetInternal(const char* name) -> T;
private:
const std::string m_section;
const std::string m_name;

View File

@@ -1,10 +0,0 @@
#include "ui/types.hpp"
#include "ui/object.hpp"
namespace sphaira::ui::bubble {
void Init();
void Draw(NVGcontext* vg, Theme* theme);
void Exit();
} // namespace sphaira::ui::bubble

View File

@@ -8,15 +8,14 @@ namespace sphaira::ui {
class ErrorBox final : public Widget {
public:
ErrorBox(Result code, const std::string& message);
ErrorBox(const std::string& message);
auto Update(Controller* controller, TouchInfo* touch) -> void override;
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
private:
Result m_code{};
std::optional<Result> m_code{};
std::string m_message{};
std::string m_module_str{};
std::string m_description_str{};
};
} // namespace sphaira::ui

View File

@@ -5,12 +5,17 @@
namespace sphaira::ui {
struct List final : Object {
enum class Layout {
HOME,
GRID,
};
using Callback = std::function<void(NVGcontext* vg, Theme* theme, Vec4 v, s64 index)>;
using TouchCallback = std::function<void(s64 index)>;
using TouchCallback = std::function<void(bool touch, s64 index)>;
List(s64 row, s64 page, const Vec4& pos, const Vec4& v, const Vec2& pad = {});
void OnUpdate(Controller* controller, TouchInfo* touch, s64 count, TouchCallback callback);
void OnUpdate(Controller* controller, TouchInfo* touch, s64 index, s64 count, TouchCallback callback);
void Draw(NVGcontext* vg, Theme* theme, s64 count, Callback callback) const;
@@ -35,10 +40,36 @@ struct List final : Object {
return m_v.h + m_pad.y;
}
auto GetMaxX() const {
return m_v.w + m_pad.x;
}
auto GetLayout() const {
return m_layout;
}
void SetLayout(Layout layout) {
m_layout = layout;
}
auto GetRow() const {
return m_row;
}
auto GetPage() const {
return m_page;
}
private:
auto Draw(NVGcontext* vg, Theme* theme) -> void override {}
auto ClampX(float x, s64 count) const -> float;
auto ClampY(float y, s64 count) const -> float;
void OnUpdateHome(Controller* controller, TouchInfo* touch, s64 index, s64 count, TouchCallback callback);
void OnUpdateGrid(Controller* controller, TouchInfo* touch, s64 index, s64 count, TouchCallback callback);
void DrawHome(NVGcontext* vg, Theme* theme, s64 count, Callback callback) const;
void DrawGrid(NVGcontext* vg, Theme* theme, s64 count, Callback callback) const;
private:
const s64 m_row;
const s64 m_page;
@@ -52,6 +83,8 @@ private:
float m_yoff{};
// in progress y offset, used when scrolling.
float m_y_prog{};
Layout m_layout{Layout::GRID};
};
} // namespace sphaira::ui

View File

@@ -1,10 +1,11 @@
#pragma once
#include "ui/menus/menu_base.hpp"
#include "ui/menus/grid_menu_base.hpp"
#include "ui/scrollable_text.hpp"
#include "ui/scrolling_text.hpp"
#include "ui/list.hpp"
#include "nro.hpp"
#include "fs.hpp"
#include "option.hpp"
#include <span>
namespace sphaira::ui::menu::appstore {
@@ -73,6 +74,7 @@ struct EntryMenu final : MenuBase {
EntryMenu(Entry& entry, const LazyImage& default_icon, Menu& menu);
~EntryMenu();
auto GetShortTitle() const -> const char* override { return "Entry"; };
void Update(Controller* controller, TouchInfo* touch) override;
void Draw(NVGcontext* vg, Theme* theme) override;
// void OnFocusGained() override;
@@ -134,27 +136,25 @@ enum OrderType {
OrderType_Ascending,
};
struct Menu final : MenuBase {
Menu(const std::vector<NroEntry>& nro_entries);
using LayoutType = grid::LayoutType;
struct Menu final : grid::Menu {
Menu();
~Menu();
auto GetShortTitle() const -> const char* override { return "Store"; };
void Update(Controller* controller, TouchInfo* touch) override;
void Draw(NVGcontext* vg, Theme* theme) override;
void OnFocusGained() override;
void SetIndex(s64 index);
void ScanHomebrew();
void Sort();
void SetFilter(Filter filter);
void SetSort(SortType sort);
void SetOrder(OrderType order);
void SetSearch(const std::string& term);
void SetAuthor();
auto GetEntry(s64 i) -> Entry& {
return m_entries[m_entries_current[i]];
}
auto GetEntry() -> Entry& {
return m_entries[m_entries_current[m_index]];
return GetEntry(m_index);
}
auto SetDirty() {
@@ -162,16 +162,27 @@ struct Menu final : MenuBase {
}
private:
const std::vector<NroEntry>& m_nro_entries;
void SetIndex(s64 index);
void ScanHomebrew();
void Sort();
void SortAndFindLastFile();
void SetFilter();
void SetSearch(const std::string& term);
void OnLayoutChange();
private:
static constexpr inline const char* INI_SECTION = "appstore";
std::vector<Entry> m_entries{};
std::vector<EntryMini> m_entries_index[Filter_MAX]{};
std::vector<EntryMini> m_entries_index_author{};
std::vector<EntryMini> m_entries_index_search{};
std::span<EntryMini> m_entries_current{};
Filter m_filter{Filter::Filter_All};
SortType m_sort{SortType::SortType_Updated};
OrderType m_order{OrderType::OrderType_Descending};
option::OptionLong m_filter{INI_SECTION, "filter", Filter::Filter_All};
option::OptionLong m_sort{INI_SECTION, "sort", SortType::SortType_Updated};
option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Descending};
option::OptionLong m_layout{INI_SECTION, "layout", LayoutType::LayoutType_GridDetail};
s64 m_index{}; // where i am in the array
LazyImage m_default_image{};

View File

@@ -10,6 +10,7 @@ struct Menu final : MenuBase {
Menu(const fs::FsPath& path);
~Menu();
auto GetShortTitle() const -> const char* override { return "File"; };
void Update(Controller* controller, TouchInfo* touch) override;
void Draw(NVGcontext* vg, Theme* theme) override;
void OnFocusGained() override;

View File

@@ -89,6 +89,19 @@ struct FileAssocEntry {
std::string name{}; // ini name
std::vector<std::string> ext{}; // list of ext
std::vector<std::string> database{}; // list of systems
bool use_base_name{}; // if set, uses base name (rom.zip) otherwise uses internal name (rom.gba)
auto IsExtension(std::string_view extension, std::string_view internal_extension) const -> bool {
for (const auto& assoc_ext : ext) {
if (extension.length() == assoc_ext.length() && !strncasecmp(assoc_ext.data(), extension.data(), assoc_ext.length())) {
return true;
}
if (internal_extension.length() == assoc_ext.length() && !strncasecmp(assoc_ext.data(), internal_extension.data(), assoc_ext.length())) {
return true;
}
}
return false;
}
};
struct LastFile {
@@ -111,6 +124,7 @@ struct Menu final : MenuBase {
Menu(const std::vector<NroEntry>& nro_entries);
~Menu();
auto GetShortTitle() const -> const char* override { return "Files"; };
void Update(Controller* controller, TouchInfo* touch) override;
void Draw(NVGcontext* vg, Theme* theme) override;
void OnFocusGained() override;
@@ -122,6 +136,12 @@ struct Menu final : MenuBase {
private:
void SetIndex(s64 index);
void InstallForwarder();
void InstallFiles();
void UnzipFiles(fs::FsPath folder);
void ZipFiles(fs::FsPath zip_path);
void UploadFiles();
auto Scan(const fs::FsPath& new_path, bool is_walk_up = false) -> Result;
void LoadAssocEntriesPath(const fs::FsPath& path);
@@ -141,15 +161,15 @@ private:
}
auto GetSelectedEntries() const -> std::vector<FileEntry> {
if (!m_selected_count) {
return {};
}
std::vector<FileEntry> out;
for (auto&e : m_entries) {
if (e.IsSelected()) {
out.emplace_back(e);
if (!m_selected_count) {
out.emplace_back(GetEntry());
} else {
for (auto&e : m_entries) {
if (e.IsSelected()) {
out.emplace_back(e);
}
}
}
@@ -168,13 +188,6 @@ private:
m_selected_path = m_path;
}
void AddCurrentFileToSelection(SelectedType type) {
m_selected_files.emplace_back(GetEntry());
m_selected_count++;
m_selected_type = type;
m_selected_path = m_path;
}
void ResetSelection() {
m_selected_files.clear();
m_selected_count = 0;

View File

@@ -0,0 +1,61 @@
#pragma once
#include "ui/menus/menu_base.hpp"
#include "yati/source/stream.hpp"
namespace sphaira::ui::menu::ftp {
enum class State {
// not connected.
None,
// just connected, starts the transfer.
Connected,
// set whilst transfer is in progress.
Progress,
// set when the transfer is finished.
Done,
// failed to connect.
Failed,
};
struct StreamFtp final : yati::source::Stream {
StreamFtp(const fs::FsPath& path, std::stop_token token);
Result ReadChunk(void* buf, s64 size, u64* bytes_read) override;
bool Push(const void* buf, s64 size);
void Disable();
// private:
fs::FsPath m_path{};
std::stop_token m_token{};
std::vector<u8> m_buffer{};
Mutex m_mutex{};
bool m_active{};
// bool m_push_exit{};
};
struct Menu final : MenuBase {
Menu();
~Menu();
auto GetShortTitle() const -> const char* override { return "FTP"; };
void Update(Controller* controller, TouchInfo* touch) override;
void Draw(NVGcontext* vg, Theme* theme) override;
void OnFocusGained() override;
// this should be private
// private:
std::shared_ptr<StreamFtp> m_source{};
Thread m_thread{};
Mutex m_mutex{};
// the below are shared across threads, lock with the above mutex!
State m_state{State::None};
const char* m_user{};
const char* m_pass{};
unsigned m_port{};
bool m_anon{};
bool m_was_ftp_enabled{};
};
} // namespace sphaira::ui::menu::ftp

View File

@@ -0,0 +1,153 @@
#pragma once
#include "ui/menus/grid_menu_base.hpp"
#include "ui/list.hpp"
#include "fs.hpp"
#include "option.hpp"
#include <memory>
#include <vector>
namespace sphaira::ui::menu::game {
enum class NacpLoadStatus {
// not yet attempted to be loaded.
None,
// started loading.
Progress,
// loaded, ready to parse.
Loaded,
// failed to load, do not attempt to load again!
Error,
};
struct Entry {
u64 app_id{};
char display_version[0x10]{};
NacpLanguageEntry lang{};
int image{};
bool selected{};
std::shared_ptr<NsApplicationControlData> control{};
u64 control_size{};
NacpLoadStatus status{NacpLoadStatus::None};
auto GetName() const -> const char* {
return lang.name;
}
auto GetAuthor() const -> const char* {
return lang.author;
}
auto GetDisplayVersion() const -> const char* {
return display_version;
}
};
struct ThreadResultData {
u64 id{};
std::shared_ptr<NsApplicationControlData> control{};
u64 control_size{};
char display_version[0x10]{};
NacpLanguageEntry lang{};
NacpLoadStatus status{NacpLoadStatus::None};
};
struct ThreadData {
ThreadData();
auto IsRunning() const -> bool;
void Run();
void Close();
void Push(u64 id);
void Push(std::span<const Entry> entries);
void Pop(std::vector<ThreadResultData>& out);
private:
UEvent m_uevent{};
Mutex m_mutex_id{};
Mutex m_mutex_result{};
// app_ids pushed to the queue, signal uevent when pushed.
std::vector<u64> m_ids{};
// control data pushed to the queue.
std::vector<ThreadResultData> m_result{};
std::atomic_bool m_running{};
};
enum SortType {
SortType_Updated,
};
enum OrderType {
OrderType_Descending,
OrderType_Ascending,
};
using LayoutType = grid::LayoutType;
struct Menu final : grid::Menu {
Menu();
~Menu();
auto GetShortTitle() const -> const char* override { return "Games"; };
void Update(Controller* controller, TouchInfo* touch) override;
void Draw(NVGcontext* vg, Theme* theme) override;
void OnFocusGained() override;
private:
void SetIndex(s64 index);
void ScanHomebrew();
void Sort();
void SortAndFindLastFile(bool scan);
void FreeEntries();
void OnLayoutChange();
auto GetSelectedEntries() const {
std::vector<Entry> out;
for (auto& e : m_entries) {
if (e.selected) {
out.emplace_back(e);
}
}
if (!m_entries.empty() && out.empty()) {
out.emplace_back(m_entries[m_index]);
}
return out;
}
void ClearSelection() {
for (auto& e : m_entries) {
e.selected = false;
}
m_selected_count = 0;
}
void DeleteGames();
void DumpGames(u32 flags);
private:
static constexpr inline const char* INI_SECTION = "games";
static constexpr inline const char* INI_SECTION_DUMP = "dump";
std::vector<Entry> m_entries{};
s64 m_index{}; // where i am in the array
s64 m_selected_count{};
std::unique_ptr<List> m_list{};
bool m_is_reversed{};
bool m_dirty{};
ThreadData m_thread_data{};
Thread m_thread{};
option::OptionLong m_sort{INI_SECTION, "sort", SortType::SortType_Updated};
option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Descending};
option::OptionLong m_layout{INI_SECTION, "layout", LayoutType::LayoutType_GridDetail};
option::OptionBool m_hide_forwarders{INI_SECTION, "hide_forwarders", false};
};
} // namespace sphaira::ui::menu::game

View File

@@ -0,0 +1,82 @@
#pragma once
#include "ui/menus/menu_base.hpp"
#include "yati/container/base.hpp"
#include "yati/source/base.hpp"
#include "ui/list.hpp"
#include <span>
#include <memory>
namespace sphaira::ui::menu::gc {
struct GcCollection : yati::container::CollectionEntry {
GcCollection(const char* _name, s64 _size, u8 _type, u8 _id_offset) {
name = _name;
size = _size;
type = _type;
id_offset = _id_offset;
}
// NcmContentType
u8 type{};
u8 id_offset{};
};
using GcCollections = std::vector<GcCollection>;
struct ApplicationEntry {
u64 app_id{};
u32 version{};
u8 key_gen{};
std::vector<GcCollections> application{};
std::vector<GcCollections> patch{};
std::vector<GcCollections> add_on{};
std::vector<GcCollections> data_patch{};
yati::container::Collections tickets{};
auto GetSize() const -> s64;
auto GetSize(const std::vector<GcCollections>& entries) const -> s64;
};
struct Menu final : MenuBase {
Menu();
~Menu();
auto GetShortTitle() const -> const char* override { return "GC"; };
void Update(Controller* controller, TouchInfo* touch) override;
void Draw(NVGcontext* vg, Theme* theme) override;
void OnFocusGained() override;
private:
Result GcMount();
void GcUnmount();
Result GcPoll(bool* inserted);
Result GcOnEvent();
Result UpdateStorageSize();
void FreeImage();
void OnChangeIndex(s64 new_index);
private:
FsDeviceOperator m_dev_op{};
FsGameCardHandle m_handle{};
std::unique_ptr<fs::FsNativeGameCard> m_fs{};
FsEventNotifier m_event_notifier{};
Event m_event{};
std::vector<ApplicationEntry> m_entries{};
std::unique_ptr<List> m_list{};
s64 m_entry_index{};
s64 m_option_index{};
s64 m_size_free_sd{};
s64 m_size_total_sd{};
s64 m_size_free_nand{};
s64 m_size_total_nand{};
NacpLanguageEntry m_lang_entry{};
int m_icon{};
bool m_mounted{};
};
} // namespace sphaira::ui::menu::gc

View File

@@ -45,6 +45,7 @@ struct Menu final : MenuBase {
Menu();
~Menu();
auto GetShortTitle() const -> const char* override { return "GitHub"; };
void Update(Controller* controller, TouchInfo* touch) override;
void Draw(NVGcontext* vg, Theme* theme) override;
void OnFocusGained() override;

View File

@@ -0,0 +1,35 @@
#pragma once
#include "ui/menus/menu_base.hpp"
#include "ui/scrolling_text.hpp"
#include "ui/list.hpp"
#include <string>
#include <memory>
namespace sphaira::ui::menu::grid {
enum LayoutType {
LayoutType_List,
LayoutType_Grid,
LayoutType_GridDetail,
};
struct Menu : MenuBase {
using MenuBase::MenuBase;
protected:
void OnLayoutChange(std::unique_ptr<List>& list, int layout);
void DrawEntry(NVGcontext* vg, Theme* theme, int layout, const Vec4& v, bool selected, int image, const char* name, const char* author, const char* version);
// same as above but doesn't draw image and returns image dimension.
Vec4 DrawEntryNoImage(NVGcontext* vg, Theme* theme, int layout, const Vec4& v, bool selected, const char* name, const char* author, const char* version);
private:
Vec4 DrawEntry(NVGcontext* vg, Theme* theme, bool draw_image, int layout, const Vec4& v, bool selected, int image, const char* name, const char* author, const char* version);
private:
ScrollingText m_scroll_name{};
ScrollingText m_scroll_author{};
ScrollingText m_scroll_version{};
};
} // namespace sphaira::ui::menu::grid

View File

@@ -1,6 +1,6 @@
#pragma once
#include "ui/menus/menu_base.hpp"
#include "ui/menus/grid_menu_base.hpp"
#include "ui/list.hpp"
#include "nro.hpp"
#include "fs.hpp"
@@ -22,31 +22,37 @@ enum OrderType {
OrderType_Ascending,
};
struct Menu final : MenuBase {
using LayoutType = grid::LayoutType;
struct Menu final : grid::Menu {
Menu();
~Menu();
auto GetShortTitle() const -> const char* override { return "Apps"; };
void Update(Controller* controller, TouchInfo* touch) override;
void Draw(NVGcontext* vg, Theme* theme) override;
void OnFocusGained() override;
void SetIndex(s64 index);
void InstallHomebrew();
void ScanHomebrew();
void Sort();
void SortAndFindLastFile();
auto GetHomebrewList() const -> const std::vector<NroEntry>& {
return m_entries;
}
static Result InstallHomebrew(const fs::FsPath& path, const NacpStruct& nacp, const std::vector<u8>& icon);
static Result InstallHomebrewFromPath(const fs::FsPath& path);
private:
void SetIndex(s64 index);
void InstallHomebrew();
void ScanHomebrew();
void Sort();
void SortAndFindLastFile();
void FreeEntries();
void OnLayoutChange();
auto IsStarEnabled() -> bool {
return m_sort.Get() >= SortType_UpdatedStar;
}
static Result InstallHomebrew(const fs::FsPath& path, const NacpStruct& nacp, const std::vector<u8>& icon);
static Result InstallHomebrewFromPath(const fs::FsPath& path);
private:
static constexpr inline const char* INI_SECTION = "homebrew";
@@ -56,6 +62,7 @@ private:
option::OptionLong m_sort{INI_SECTION, "sort", SortType::SortType_AlphabeticalStar};
option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Descending};
option::OptionLong m_layout{INI_SECTION, "layout", LayoutType::LayoutType_GridDetail};
option::OptionBool m_hide_sphaira{INI_SECTION, "hide_sphaira", false};
};

View File

@@ -30,16 +30,19 @@ struct Menu final : MenuBase {
Menu();
~Menu();
auto GetShortTitle() const -> const char* override { return "IRS"; };
void Update(Controller* controller, TouchInfo* touch) override;
void Draw(NVGcontext* vg, Theme* theme) override;
void OnFocusGained() override;
private:
void PollCameraStatus(bool statup = false);
void LoadDefaultConfig();
void UpdateConfig(const IrsImageTransferProcessorExConfig* config);
void ResetImage();
void UpdateImage();
void updateColourArray();
auto GetEntryName(s64 i) -> std::string;
private:
Result m_init_rc{};

View File

@@ -3,7 +3,6 @@
#include "ui/widget.hpp"
#include "ui/menus/homebrew.hpp"
#include "ui/menus/filebrowser.hpp"
#include "ui/menus/appstore.hpp"
namespace sphaira::ui::menu::main {
@@ -18,6 +17,32 @@ enum class UpdateState {
Error,
};
using MiscMenuFunction = std::function<std::shared_ptr<ui::menu::MenuBase>(void)>;
enum MiscMenuFlag : u8 {
// can be set as the rightside menu.
MiscMenuFlag_Shortcut = 1 << 0,
// needs install option to be enabled.
MiscMenuFlag_Install = 1 << 1,
};
struct MiscMenuEntry {
const char* name;
const char* title;
MiscMenuFunction func;
u8 flag;
auto IsShortcut() const -> bool {
return flag & MiscMenuFlag_Shortcut;
}
auto IsInstall() const -> bool {
return flag & MiscMenuFlag_Install;
}
};
auto GetMiscMenuEntries() -> std::span<const MiscMenuEntry>;
// this holds 2 menus and allows for switching between them
struct MainMenu final : Widget {
MainMenu();
@@ -39,7 +64,7 @@ private:
private:
std::shared_ptr<homebrew::Menu> m_homebrew_menu{};
std::shared_ptr<filebrowser::Menu> m_filebrowser_menu{};
std::shared_ptr<appstore::Menu> m_app_store_menu{};
std::shared_ptr<MenuBase> m_right_side_menu{};
std::shared_ptr<MenuBase> m_current_menu{};
std::string m_update_url{};

View File

@@ -1,7 +1,6 @@
#pragma once
#include "ui/widget.hpp"
#include "nro.hpp"
#include <string>
namespace sphaira::ui::menu {
@@ -10,6 +9,7 @@ struct MenuBase : Widget {
MenuBase(std::string title);
virtual ~MenuBase();
virtual auto GetShortTitle() const -> const char* = 0;
virtual void Update(Controller* controller, TouchInfo* touch);
virtual void Draw(NVGcontext* vg, Theme* theme);
@@ -21,6 +21,10 @@ struct MenuBase : Widget {
void SetTitleSubHeading(std::string sub_heading);
void SetSubHeading(std::string sub_heading);
auto GetTitle() const {
return m_title;
}
private:
void UpdateVars();
@@ -29,6 +33,7 @@ private:
std::string m_title_sub_heading{};
std::string m_sub_heading{};
protected:
struct tm m_tm{};
TimeStamp m_poll_timestamp{};
u32 m_battery_percetange{};

View File

@@ -2,6 +2,7 @@
#include "ui/menus/menu_base.hpp"
#include "ui/scrollable_text.hpp"
#include "ui/scrolling_text.hpp"
#include "ui/list.hpp"
#include "option.hpp"
#include <span>
@@ -132,6 +133,7 @@ struct Menu final : MenuBase {
Menu();
~Menu();
auto GetShortTitle() const -> const char* override { return "Themezer"; };
void Update(Controller* controller, TouchInfo* touch) override;
void Draw(NVGcontext* vg, Theme* theme) override;
void OnFocusGained() override;
@@ -160,6 +162,9 @@ private:
s64 m_index{}; // where i am in the array
std::unique_ptr<List> m_list{};
ScrollingText m_scroll_name{};
ScrollingText m_scroll_author{};
// options
option::OptionLong m_sort{INI_SECTION, "sort", 0};
option::OptionLong m_order{INI_SECTION, "order", 0};

View File

@@ -0,0 +1,45 @@
#pragma once
#include "ui/menus/menu_base.hpp"
#include "yati/source/usb.hpp"
namespace sphaira::ui::menu::usb {
enum class State {
// not connected.
None,
// just connected, waiting for file list.
Connected_WaitForFileList,
// just connected, starts the transfer.
Connected_StartingTransfer,
// set whilst transfer is in progress.
Progress,
// set when the transfer is finished.
Done,
// failed to connect.
Failed,
};
struct Menu final : MenuBase {
Menu();
~Menu();
auto GetShortTitle() const -> const char* override { return "USB"; };
void Update(Controller* controller, TouchInfo* touch) override;
void Draw(NVGcontext* vg, Theme* theme) override;
void OnFocusGained() override;
// this should be private
// private:
std::shared_ptr<yati::source::Usb> m_usb_source{};
bool m_was_mtp_enabled{};
Thread m_thread{};
Mutex m_mutex{};
// the below are shared across threads, lock with the above mutex!
State m_state{State::None};
std::vector<std::string> m_names{};
bool m_usb_has_connection{};
};
} // namespace sphaira::ui::menu::usb

View File

@@ -2,24 +2,26 @@
#include "nanovg.h"
#include "ui/types.hpp"
#include "ui/scrolling_text.hpp"
namespace sphaira::ui::gfx {
void drawImage(NVGcontext*, float x, float y, float w, float h, int texture);
void drawImage(NVGcontext*, const Vec4& v, int texture);
void drawImageRounded(NVGcontext*, float x, float y, float w, float h, int texture);
void drawImageRounded(NVGcontext*, const Vec4& v, int texture);
void drawImage(NVGcontext*, float x, float y, float w, float h, int texture, float rounded = 0.F);
void drawImage(NVGcontext*, const Vec4& v, int texture, float rounded = 0.F);
void dimBackground(NVGcontext*);
void drawRect(NVGcontext*, float x, float y, float w, float h, const NVGcolor& c, bool rounded = false);
void drawRect(NVGcontext*, const Vec4& v, const NVGcolor& c, bool rounded = false);
void drawRect(NVGcontext*, float x, float y, float w, float h, const NVGpaint& p, bool rounded = false);
void drawRect(NVGcontext*, const Vec4& v, const NVGpaint& p, bool rounded = false);
void drawRect(NVGcontext*, float x, float y, float w, float h, const NVGcolor& c, float rounding = 0.F);
void drawRect(NVGcontext*, const Vec4& v, const NVGcolor& c, float rounding = 0.F);
void drawRect(NVGcontext*, float x, float y, float w, float h, const NVGpaint& p, float rounding = 0.F);
void drawRect(NVGcontext*, const Vec4& v, const NVGpaint& p, float rounding = 0.F);
void drawRectOutline(NVGcontext*, const Theme*, float size, float x, float y, float w, float h);
void drawRectOutline(NVGcontext*, const Theme*, float size, const Vec4& v);
void drawTriangle(NVGcontext*, float aX, float aY, float bX, float bY, float cX, float cY, const NVGcolor& c);
void drawTriangle(NVGcontext*, float aX, float aY, float bX, float bY, float cX, float cY, const NVGpaint& p);
void drawText(NVGcontext*, float x, float y, float size, const char* str, const char* end, int align, const NVGcolor& c);
void drawText(NVGcontext*, float x, float y, float size, const NVGcolor& c, const char* str, int align = NVG_ALIGN_LEFT | NVG_ALIGN_TOP, const char* end = nullptr);
void drawText(NVGcontext*, const Vec2& v, float size, const char* str, const char* end, int align, const NVGcolor& c);
@@ -37,6 +39,8 @@ void drawScrollbar(NVGcontext*, const Theme*, float x, float y, float h, u32 ind
void drawScrollbar2(NVGcontext*, const Theme*, float x, float y, float h, s64 index_off, s64 count, s64 row, s64 page);
void drawScrollbar2(NVGcontext*, const Theme*, s64 index_off, s64 count, s64 row, s64 page);
void drawAppLable(NVGcontext* vg, const Theme*, ScrollingText& st, float x, float y, float w, const char* name);
void updateHighlightAnimation();
void getHighlightAnimation(float* gradientX, float* gradientY, float* color);

View File

@@ -32,11 +32,9 @@ public:
using Options = std::vector<Option>;
public:
OptionBox(const std::string& message, const Option& a, Callback cb = [](auto){}); // confirm
OptionBox(const std::string& message, const Option& a, const Option& b, Callback cb); // yesno
OptionBox(const std::string& message, const Option& a, const Option& b, s64 index, Callback cb); // yesno
OptionBox(const std::string& message, const Option& a, const Option& b, const Option& c, Callback cb); // tri
OptionBox(const std::string& message, const Option& a, const Option& b, const Option& c, s64 index, Callback cb); // tri
OptionBox(const std::string& message, const Option& a, Callback cb = [](auto){}, int image = 0); // confirm
OptionBox(const std::string& message, const Option& a, const Option& b, Callback cb, int image = 0); // yesno
OptionBox(const std::string& message, const Option& a, const Option& b, s64 index, Callback cb, int image = 0); // yesno
auto Update(Controller* controller, TouchInfo* touch) -> void override;
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
@@ -50,6 +48,7 @@ private:
private:
std::string m_message{};
Callback m_callback{};
int m_image{};
Vec4 m_spacer_line{};

View File

@@ -1,6 +1,7 @@
#pragma once
#include "ui/widget.hpp"
#include "ui/scrolling_text.hpp"
#include "ui/list.hpp"
#include <optional>
@@ -36,8 +37,10 @@ private:
Items m_items{};
Callback m_callback{};
s64 m_index{}; // index in list array
s64 m_starting_index{};
std::unique_ptr<List> m_list{};
ScrollingText m_scroll_text{};
float m_yoff{};
float m_line_top{};

View File

@@ -3,6 +3,7 @@
#include "widget.hpp"
#include "fs.hpp"
#include <functional>
#include <span>
namespace sphaira::ui {
@@ -12,17 +13,23 @@ using ProgressBoxDoneCallback = std::function<void(bool success)>;
struct ProgressBox final : Widget {
ProgressBox(
int image,
const std::string& action,
const std::string& title,
ProgressBoxCallback callback, ProgressBoxDoneCallback done = [](bool success){},
int cpuid = 1, int prio = 0x2C, int stack_size = 1024*1024
int cpuid = 1, int prio = 0x2C, int stack_size = 1024*128
);
~ProgressBox();
auto Update(Controller* controller, TouchInfo* touch) -> void override;
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
auto SetTitle(const std::string& title) -> ProgressBox&;
auto NewTransfer(const std::string& transfer) -> ProgressBox&;
auto UpdateTransfer(s64 offset, s64 size) -> ProgressBox&;
// not const in order to avoid copy by using std::swap
auto SetImageData(std::vector<u8>& data) -> ProgressBox&;
auto SetImageDataConst(std::span<const u8> data) -> ProgressBox&;
void RequestExit();
auto ShouldExit() -> bool;
@@ -31,15 +38,24 @@ struct ProgressBox final : Widget {
void Yield();
auto OnDownloadProgressCallback() {
return [this](u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow){
return [this](s64 dltotal, s64 dlnow, s64 ultotal, s64 ulnow){
if (this->ShouldExit()) {
return false;
}
this->UpdateTransfer(dlnow, dltotal);
if (dltotal) {
this->UpdateTransfer(dlnow, dltotal);
} else {
this->UpdateTransfer(ulnow, ultotal);
}
return true;
};
}
private:
void FreeImage();
public:
struct ThreadData {
ProgressBox* pbox{};
@@ -51,12 +67,22 @@ private:
Mutex m_mutex{};
Thread m_thread{};
ThreadData m_thread_data{};
ProgressBoxDoneCallback m_done{};
// shared data start.
std::string m_action{};
std::string m_title{};
std::string m_transfer{};
s64 m_size{};
s64 m_offset{};
s64 m_last_offset{};
s64 m_speed{};
TimeStamp m_timestamp{};
std::vector<u8> m_image_data{};
// shared data end.
int m_image{};
bool m_own_image{};
};
// this is a helper function that does many things.

View File

@@ -0,0 +1,20 @@
#pragma once
#include "ui/widget.hpp"
#include <string>
namespace sphaira::ui {
struct ScrollingText final {
public:
void Draw(NVGcontext*, bool focus, float x, float y, float w, float size, int align, const NVGcolor& colour, const std::string& text_entry);
void DrawArgs(NVGcontext*, bool focus, float x, float y, float w, float size, int align, const NVGcolor& colour, const char* s, ...) __attribute__ ((format (printf, 10, 11)));
void Reset(const std::string& text_entry = "");
private:
std::string m_str;
s64 m_tick;
float m_text_xoff;
};
} // namespace sphaira::ui

View File

@@ -14,7 +14,7 @@ namespace sphaira {
#define SCREEN_WIDTH 1280.f
#define SCREEN_HEIGHT 720.f
struct [[nodiscard]] Vec2 {
struct Vec2 {
constexpr Vec2() = default;
constexpr Vec2(float _x, float _y) : x{_x}, y{_y} {}
@@ -53,7 +53,7 @@ struct [[nodiscard]] Vec2 {
float x{}, y{};
};
struct [[nodiscard]] Vec4 {
struct Vec4 {
constexpr Vec4() = default;
constexpr Vec4(float _x, float _y, float _w, float _h) : x{_x}, y{_y}, w{_w}, h{_h} {}
constexpr Vec4(Vec2 vec0, Vec2 vec1) : x{vec0.x}, y{vec0.y}, w{vec1.x}, h{vec1.y} {}
@@ -226,7 +226,6 @@ struct ThemeMeta {
struct Theme {
ThemeMeta meta;
PLSR_BFSTM music;
ElementEntry elements[ThemeEntryID_MAX];
auto GetColour(ThemeEntryID id) const {

View File

@@ -0,0 +1,101 @@
#pragma once
#include <vector>
#include <string>
#include <new>
#include <switch.h>
namespace sphaira::usb {
struct Base {
enum { USBModule = 523 };
enum : Result {
Result_Cancelled = MAKERESULT(USBModule, 100),
};
Base(u64 transfer_timeout);
// sets up usb.
virtual Result Init() = 0;
// returns 0 if usb is connected to a device.
virtual Result IsUsbConnected(u64 timeout) = 0;
// transfers a chunk of data, check out_size_transferred for how much was transferred.
Result TransferPacketImpl(bool read, void *page, u32 size, u32 *out_size_transferred, u64 timeout);
Result TransferPacketImpl(bool read, void *page, u32 size, u32 *out_size_transferred) {
return TransferPacketImpl(read, page, size, out_size_transferred, m_transfer_timeout);
}
// transfers all data.
Result TransferAll(bool read, void *data, u32 size, u64 timeout);
Result TransferAll(bool read, void *data, u32 size) {
return TransferAll(read, data, size, m_transfer_timeout);
}
// returns the cancel event.
auto GetCancelEvent() {
return &m_uevent;
}
// cancels an in progress transfer.
void Cancel() {
ueventSignal(GetCancelEvent());
}
auto& GetTransferBuffer() {
return m_aligned;
}
auto GetTransferTimeout() const {
return m_transfer_timeout;
}
public:
// custom allocator for std::vector that respects alignment.
// https://en.cppreference.com/w/cpp/named_req/Allocator
template <typename T, std::size_t Align>
struct CustomVectorAllocator {
public:
// https://en.cppreference.com/w/cpp/memory/new/operator_new
auto allocate(std::size_t n) -> T* {
n = (n + (Align - 1)) &~ (Align - 1);
return new(align) T[n];
}
// https://en.cppreference.com/w/cpp/memory/new/operator_delete
auto deallocate(T* p, std::size_t n) noexcept -> void {
// ::operator delete[] (p, n, align);
::operator delete[] (p, align);
}
private:
static constexpr inline std::align_val_t align{Align};
};
template <typename T>
struct PageAllocator : CustomVectorAllocator<T, 0x1000> {
using value_type = T; // used by std::vector
};
using PageAlignedVector = std::vector<u8, PageAllocator<u8>>;
protected:
enum UsbSessionEndpoint {
UsbSessionEndpoint_In = 0,
UsbSessionEndpoint_Out = 1,
};
virtual Event *GetCompletionEvent(UsbSessionEndpoint ep) = 0;
virtual Result WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) = 0;
virtual Result TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 size, u32 *out_xfer_id) = 0;
virtual Result GetTransferResult(UsbSessionEndpoint ep, u32 xfer_id, u32 *out_requested_size, u32 *out_transferred_size) = 0;
private:
u64 m_transfer_timeout{};
UEvent m_uevent{};
PageAlignedVector m_aligned{};
};
} // namespace sphaira::usb

View File

@@ -0,0 +1,47 @@
#pragma once
#include <switch.h>
namespace sphaira::usb::tinfoil {
enum Magic : u32 {
Magic_List0 = 0x304C5554, // TUL0 (Tinfoil Usb List 0)
Magic_Command0 = 0x30435554, // TUC0 (Tinfoil USB Command 0)
};
enum USBCmdType : u8 {
REQUEST = 0,
RESPONSE = 1
};
enum USBCmdId : u32 {
EXIT = 0,
FILE_RANGE = 1
};
struct TUSHeader {
u32 magic; // TUL0 (Tinfoil Usb List 0)
u32 nspListSize;
u64 padding;
};
struct NX_PACKED USBCmdHeader {
u32 magic; // TUC0 (Tinfoil USB Command 0)
USBCmdType type;
u8 padding[0x3];
u32 cmdId;
u64 dataSize;
u8 reserved[0xC];
};
struct FileRangeCmdHeader {
u64 size;
u64 offset;
u64 nspNameLen;
u64 padding;
};
static_assert(sizeof(TUSHeader) == 0x10, "TUSHeader must be 0x10!");
static_assert(sizeof(USBCmdHeader) == 0x20, "USBCmdHeader must be 0x20!");
} // namespace sphaira::usb::tinfoil

View File

@@ -0,0 +1,47 @@
#pragma once
#include "usb/usbhs.hpp"
#include <string>
#include <memory>
#include <span>
#include <switch.h>
namespace sphaira::usb::upload {
struct Usb {
enum { USBModule = 523 };
enum : Result {
Result_BadMagic = MAKERESULT(USBModule, 0),
Result_Exit = MAKERESULT(USBModule, 1),
Result_BadCount = MAKERESULT(USBModule, 2),
Result_BadTransferSize = MAKERESULT(USBModule, 3),
Result_BadTotalSize = MAKERESULT(USBModule, 4),
Result_BadCommand = MAKERESULT(USBModule, 4),
};
Usb(u64 transfer_timeout);
virtual ~Usb();
virtual Result Read(const std::string& path, void* buf, s64 off, s64 size, u64* bytes_read) = 0;
Result IsUsbConnected(u64 timeout) {
return m_usb->IsUsbConnected(timeout);
}
// waits for connection and then sends file list.
Result WaitForConnection(u64 timeout, std::span<const std::string> names);
// polls for command, executes transfer if possible.
// will return Result_Exit if exit command is recieved.
Result PollCommands();
private:
Result FileRangeCmd(u64 data_size);
private:
std::unique_ptr<usb::UsbHs> m_usb;
};
} // namespace sphaira::usb::upload

View File

@@ -0,0 +1,27 @@
#pragma once
#include "base.hpp"
namespace sphaira::usb {
// Device Host
struct UsbDs final : Base {
using Base::Base;
~UsbDs();
Result Init() override;
Result IsUsbConnected(u64 timeout) override;
Result GetSpeed(UsbDeviceSpeed* out);
private:
Event *GetCompletionEvent(UsbSessionEndpoint ep) override;
Result WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) override;
Result TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 size, u32 *out_urb_id) override;
Result GetTransferResult(UsbSessionEndpoint ep, u32 urb_id, u32 *out_requested_size, u32 *out_transferred_size) override;
private:
UsbDsInterface* m_interface{};
UsbDsEndpoint* m_endpoints[2]{};
};
} // namespace sphaira::usb

View File

@@ -0,0 +1,33 @@
#pragma once
#include "base.hpp"
namespace sphaira::usb {
struct UsbHs final : Base {
UsbHs(u8 index, const UsbHsInterfaceFilter& filter, u64 transfer_timeout);
~UsbHs();
Result Init() override;
Result IsUsbConnected(u64 timeout) override;
private:
Event *GetCompletionEvent(UsbSessionEndpoint ep) override;
Result WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) override;
Result TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 size, u32 *out_xfer_id) override;
Result GetTransferResult(UsbSessionEndpoint ep, u32 xfer_id, u32 *out_requested_size, u32 *out_transferred_size) override;
Result Connect();
void Close();
private:
u8 m_index{};
UsbHsInterfaceFilter m_filter{};
UsbHsInterface m_interface{};
UsbHsClientIfSession m_s{};
UsbHsClientEpSession m_endpoints[2]{};
Event m_event{};
bool m_connected{};
};
} // namespace sphaira::usb

View File

@@ -1,13 +0,0 @@
#pragma once
#include <switch.h>
#include <string>
namespace sphaira {
// if show_error = true, it will display popup error box on
// faliure. set this to false if you want to handle errors
// from the caller.
auto WebShow(const std::string& url, bool show_error = true) -> Result;
} // namespace sphaira

View File

@@ -0,0 +1,43 @@
#pragma once
#include "yati/source/base.hpp"
#include <vector>
#include <string>
#include <memory>
#include <switch.h>
namespace sphaira::yati::container {
enum class CollectionType {
CollectionType_NCA,
CollectionType_NCZ,
CollectionType_TIK,
CollectionType_CERT,
};
struct CollectionEntry {
// collection name within file.
std::string name{};
// collection offset within file.
s64 offset{};
// collection size within file, may be compressed size.
s64 size{};
};
using Collections = std::vector<CollectionEntry>;
struct Base {
using Source = source::Base;
Base(std::shared_ptr<Source> source) : m_source{source} { }
virtual ~Base() = default;
virtual Result GetCollections(Collections& out) = 0;
auto GetSource() const {
return m_source;
}
protected:
std::shared_ptr<Source> m_source;
};
} // namespace sphaira::yati::container

View File

@@ -0,0 +1,17 @@
#pragma once
#include "base.hpp"
#include <switch.h>
#include <span>
namespace sphaira::yati::container {
struct Nsp final : Base {
using Base::Base;
Result GetCollections(Collections& out) override;
// builds nsp meta data and the size of the entier nsp.
static auto Build(std::span<CollectionEntry> collections, s64& size) -> std::vector<u8>;
};
} // namespace sphaira::yati::container

View File

@@ -0,0 +1,15 @@
#pragma once
#include "base.hpp"
#include <vector>
#include <memory>
#include <switch.h>
namespace sphaira::yati::container {
struct Xci final : Base {
using Base::Base;
Result GetCollections(Collections& out) override;
};
} // namespace sphaira::yati::container

View File

@@ -0,0 +1,57 @@
#pragma once
#include <switch.h>
namespace sphaira::crypto {
struct Aes128 {
Aes128(const void *key, bool is_encryptor) {
m_is_encryptor = is_encryptor;
aes128ContextCreate(&m_ctx, key, is_encryptor);
}
void Run(void *dst, const void *src) {
if (m_is_encryptor) {
aes128EncryptBlock(&m_ctx, dst, src);
} else {
aes128DecryptBlock(&m_ctx, dst, src);
}
}
private:
Aes128Context m_ctx;
bool m_is_encryptor;
};
struct Aes128Xts {
Aes128Xts(const u8 *key, bool is_encryptor) : Aes128Xts{key, key + 0x10, is_encryptor} { }
Aes128Xts(const void *key0, const void *key1, bool is_encryptor) {
m_is_encryptor = is_encryptor;
aes128XtsContextCreate(&m_ctx, key0, key1, is_encryptor);
}
void Run(void *dst, const void *src, u64 sector, u64 sector_size, u64 data_size) {
for (u64 pos = 0; pos < data_size; pos += sector_size) {
aes128XtsContextResetSector(&m_ctx, sector++, true);
if (m_is_encryptor) {
aes128XtsEncrypt(&m_ctx, static_cast<u8*>(dst) + pos, static_cast<const u8*>(src) + pos, sector_size);
} else {
aes128XtsDecrypt(&m_ctx, static_cast<u8*>(dst) + pos, static_cast<const u8*>(src) + pos, sector_size);
}
}
}
private:
Aes128XtsContext m_ctx;
bool m_is_encryptor;
};
static inline void cryptoAes128(const void *in, void *out, const void* key, bool is_encryptor) {
Aes128(key, is_encryptor).Run(out, in);
}
static inline void cryptoAes128Xts(const void* in, void* out, const u8* key, u64 sector, u64 sector_size, u64 data_size, bool is_encryptor) {
Aes128Xts(key, is_encryptor).Run(out, in, sector, sector_size, data_size);
}
} // namespace sphaira::crypto

View File

@@ -0,0 +1,101 @@
#pragma once
#include <switch.h>
#include <span>
#include "ncm.hpp"
#include "keys.hpp"
namespace sphaira::es {
enum { TicketModule = 522 };
enum : Result {
// found ticket has missmatching rights_id from it's name.
Result_InvalidTicketBadRightsId = MAKERESULT(TicketModule, 71),
Result_InvalidTicketVersion = MAKERESULT(TicketModule, 72),
Result_InvalidTicketKeyType = MAKERESULT(TicketModule, 73),
Result_InvalidTicketKeyRevision = MAKERESULT(TicketModule, 74),
};
enum TicketSigantureType {
TicketSigantureType_RSA_4096_SHA1 = 0x010000,
TicketSigantureType_RSA_2048_SHA1 = 0x010001,
TicketSigantureType_ECDSA_SHA1 = 0x010002,
TicketSigantureType_RSA_4096_SHA256 = 0x010003,
TicketSigantureType_RSA_2048_SHA256 = 0x010004,
TicketSigantureType_ECDSA_SHA256 = 0x010005,
TicketSigantureType_HMAC_SHA1_160 = 0x010006,
};
enum TicketTitleKeyType {
TicketTitleKeyType_Common = 0,
TicketTitleKeyType_Personalized = 1,
};
enum TicketPropertiesBitfield {
TicketPropertiesBitfield_None = 0,
// temporary ticket, removed on restart
TicketPropertiesBitfield_Temporary = 1 << 4,
};
struct TicketData {
u8 issuer[0x40];
u8 title_key_block[0x100];
u8 ticket_version1;
u8 title_key_type;
u16 ticket_version2;
u8 license_type;
u8 master_key_revision;
u16 properties_bitfield;
u8 _0x148[0x8];
u64 ticket_id;
u64 device_id;
FsRightsId rights_id;
u32 account_id;
u8 _0x174[0xC];
u8 _0x180[0x140];
};
static_assert(sizeof(TicketData) == 0x2C0);
struct EticketRsaDeviceKey {
u8 ctr[AES_128_KEY_SIZE];
u8 private_exponent[0x100];
u8 modulus[0x100];
u32 public_exponent; ///< Stored using big endian byte order. Must match ETICKET_RSA_DEVICE_KEY_PUBLIC_EXPONENT.
u8 padding[0x14];
u64 device_id;
u8 ghash[0x10];
};
static_assert(sizeof(EticketRsaDeviceKey) == 0x240);
// es functions.
Result Initialize();
void Exit();
Service* GetServiceSession();
// todo: find the ipc that gets personalised tickets.
// todo: if ipc doesn't exist, manually parse es personalised save.
// todo: add personalised -> common ticket conversion.
// todo: make the above an option for both dump and install.
Result ImportTicket(const void* tik_buf, u64 tik_size, const void* cert_buf, u64 cert_size);
Result CountCommonTicket(s32* count);
Result CountPersonalizedTicket(s32* count);
Result ListCommonTicket(s32 *out_entries_written, FsRightsId* out_ids, s32 count);
Result ListPersonalizedTicket(s32 *out_entries_written, FsRightsId* out_ids, s32 count);
Result ListMissingPersonalizedTicket(s32 *out_entries_written, FsRightsId* out_ids, s32 count); // untested
Result GetCommonTicketSize(u64 *size_out, const FsRightsId* rightsId);
Result GetCommonTicketData(u64 *size_out, void *tik_data, u64 tik_size, const FsRightsId* rightsId);
Result GetCommonTicketAndCertificateSize(u64 *tik_size_out, u64 *cert_size_out, const FsRightsId* rightsId); // [4.0.0+]
Result GetCommonTicketAndCertificateData(u64 *tik_size_out, u64 *cert_size_out, void* tik_buf, u64 tik_size, void* cert_buf, u64 cert_size, const FsRightsId* rightsId); // [4.0.0+]
// ticket functions.
Result GetTicketDataOffset(std::span<const u8> ticket, u64& out);
Result GetTicketData(std::span<const u8> ticket, es::TicketData* out);
Result SetTicketData(std::span<u8> ticket, const es::TicketData* in);
Result GetTitleKey(keys::KeyEntry& out, const TicketData& data, const keys::Keys& keys);
Result DecryptTitleKey(keys::KeyEntry& out, u8 key_gen, const keys::Keys& keys);
Result PatchTicket(std::span<u8> ticket, const keys::Keys& keys);
} // namespace sphaira::es

View File

@@ -0,0 +1,70 @@
#pragma once
#include <switch.h>
#include <array>
#include <cstring>
#include "defines.hpp"
namespace sphaira::keys {
struct KeyEntry {
u8 key[AES_128_KEY_SIZE]{};
auto IsValid() const -> bool {
const KeyEntry empty{};
return std::memcmp(key, &empty, sizeof(key));
}
};
using KeySection = std::array<KeyEntry, 0x20>;
struct Keys {
u8 header_key[0x20]{};
// the below are only found if read_from_file=true
KeySection key_area_key[0x3]{}; // index
KeySection titlekek{};
KeySection master_key{};
KeyEntry eticket_rsa_kek{};
SetCalRsa2048DeviceKey eticket_device_key{};
static auto FixKey(u8 key) -> u8 {
if (key) {
return key - 1;
}
return key;
}
auto HasNcaKeyArea(u8 key, u8 index) const -> bool {
return key_area_key[index][FixKey(key)].IsValid();
}
auto HasTitleKek(u8 key) const -> bool {
return titlekek[FixKey(key)].IsValid();
}
auto HasMasterKey(u8 key) const -> bool {
return master_key[FixKey(key)].IsValid();
}
auto GetNcaKeyArea(KeyEntry* out, u8 key, u8 index) const -> Result {
R_UNLESS(HasNcaKeyArea(key, index), 0x1);
*out = key_area_key[index][FixKey(key)];
R_SUCCEED();
}
auto GetTitleKek(KeyEntry* out, u8 key) const -> Result {
R_UNLESS(HasTitleKek(key), 0x1);
*out = titlekek[FixKey(key)];
R_SUCCEED();
}
auto GetMasterKey(KeyEntry* out, u8 key) const -> Result {
R_UNLESS(HasMasterKey(key), 0x1);
*out = master_key[FixKey(key)];
R_SUCCEED();
}
};
void parse_hex_key(void* key, const char* hex);
Result parse_keys(Keys& out, bool read_from_file);
} // namespace sphaira::keys

View File

@@ -0,0 +1,228 @@
#pragma once
#include "fs.hpp"
#include "keys.hpp"
#include "ncm.hpp"
#include <switch.h>
#include <vector>
namespace sphaira::nca {
#define NCA0_MAGIC 0x3041434E
#define NCA2_MAGIC 0x3241434E
#define NCA3_MAGIC 0x3341434E
#define NCA_SECTOR_SIZE 0x200
#define NCA_XTS_SECTION_SIZE 0xC00
#define NCA_SECTION_TOTAL 0x4
#define NCA_MEDIA_REAL(x)((x * 0x200))
#define NCA_PROGRAM_LOGO_OFFSET 0x8000
#define NCA_META_CNMT_OFFSET 0xC20
enum KeyGenerationOld {
KeyGenerationOld_100 = 0x0,
KeyGenerationOld_Unused = 0x1,
KeyGenerationOld_300 = 0x2,
};
enum KeyGeneration {
KeyGeneration_301 = 0x3,
KeyGeneration_400 = 0x4,
KeyGeneration_500 = 0x5,
KeyGeneration_600 = 0x6,
KeyGeneration_620 = 0x7,
KeyGeneration_700 = 0x8,
KeyGeneration_810 = 0x9,
KeyGeneration_900 = 0x0A,
KeyGeneration_910 = 0x0B,
KeyGeneration_1210 = 0x0C,
KeyGeneration_1300 = 0x0D,
KeyGeneration_1400 = 0x0E,
KeyGeneration_1500 = 0x0F,
KeyGeneration_1600 = 0x10,
KeyGeneration_1700 = 0x11,
KeyGeneration_1800 = 0x12,
KeyGeneration_1900 = 0x13,
KeyGeneration_2000 = 0x14,
KeyGeneration_Invalid = 0xFF,
};
enum KeyAreaEncryptionKeyIndex {
KeyAreaEncryptionKeyIndex_Application = 0x0,
KeyAreaEncryptionKeyIndex_Ocean = 0x1,
KeyAreaEncryptionKeyIndex_System = 0x2
};
enum DistributionType {
DistributionType_System = 0x0,
DistributionType_GameCard = 0x1
};
enum ContentType {
ContentType_Program = 0x0,
ContentType_Meta = 0x1,
ContentType_Control = 0x2,
ContentType_Manual = 0x3,
ContentType_Data = 0x4,
ContentType_PublicData = 0x5,
};
enum FileSystemType {
FileSystemType_RomFS = 0x0,
FileSystemType_PFS0 = 0x1
};
enum HashType {
HashType_Auto = 0x0,
HashType_HierarchicalSha256 = 0x2,
HashType_HierarchicalIntegrity = 0x3
};
enum EncryptionType {
EncryptionType_Auto = 0x0,
EncryptionType_None = 0x1,
EncryptionType_AesXts = 0x2,
EncryptionType_AesCtr = 0x3,
EncryptionType_AesCtrEx = 0x4,
EncryptionType_AesCtrSkipLayerHash = 0x5, // [14.0.0+]
EncryptionType_AesCtrExSkipLayerHash = 0x6, // [14.0.0+]
};
struct SectionTableEntry {
u32 media_start_offset; // divided by 0x200.
u32 media_end_offset; // divided by 0x200.
u8 _0x8[0x4]; // unknown.
u8 _0xC[0x4]; // unknown.
};
struct LayerRegion {
u64 offset;
u64 size;
};
struct HierarchicalSha256Data {
u8 master_hash[0x20];
u32 block_size;
u32 layer_count;
LayerRegion hash_layer;
LayerRegion pfs0_layer;
LayerRegion unused_layers[3];
u8 _0x78[0x80];
};
#pragma pack(push, 1)
struct HierarchicalIntegrityVerificationLevelInformation {
u64 logical_offset;
u64 hash_data_size;
u32 block_size; // log2
u32 _0x14; // reserved
};
#pragma pack(pop)
struct InfoLevelHash {
u32 max_layers;
HierarchicalIntegrityVerificationLevelInformation levels[6];
u8 signature_salt[0x20];
};
struct IntegrityMetaInfo {
u32 magic; // IVFC
u32 version;
u32 master_hash_size;
InfoLevelHash info_level_hash;
u8 master_hash[0x20];
u8 _0xE0[0x18];
};
static_assert(sizeof(HierarchicalSha256Data) == 0xF8);
static_assert(sizeof(IntegrityMetaInfo) == 0xF8);
static_assert(sizeof(HierarchicalSha256Data) == sizeof(IntegrityMetaInfo));
struct FsHeader {
u16 version; // always 2.
u8 fs_type; // see FileSystemType.
u8 hash_type; // see HashType.
u8 encryption_type; // see EncryptionType.
u8 metadata_hash_type;
u8 _0x6[0x2]; // empty.
union {
HierarchicalSha256Data hierarchical_sha256_data;
IntegrityMetaInfo integrity_meta_info; // used for romfs
} hash_data;
u8 patch_info[0x40];
u64 section_ctr;
u8 spares_info[0x30];
u8 compression_info[0x28];
u8 meta_data_hash_data_info[0x30];
u8 reserved[0x30];
};
static_assert(sizeof(FsHeader) == 0x200);
static_assert(sizeof(FsHeader::hash_data) == 0xF8);
struct SectionHeaderHash {
u8 sha256[0x20];
};
struct KeyArea {
u8 area[0x10];
};
struct Header {
u8 rsa_fixed_key[0x100];
u8 rsa_npdm[0x100]; // key from npdm.
u32 magic;
u8 distribution_type; // see DistributionType.
u8 content_type; // see ContentType.
u8 old_key_gen; // see KeyGenerationOld.
u8 kaek_index; // see KeyAreaEncryptionKeyIndex.
u64 size;
u64 program_id;
u32 context_id;
u32 sdk_version;
u8 key_gen; // see KeyGeneration.
u8 sig_key_gen;
u8 _0x222[0xE]; // empty.
FsRightsId rights_id;
SectionTableEntry fs_table[NCA_SECTION_TOTAL];
SectionHeaderHash fs_header_hash[NCA_SECTION_TOTAL];
KeyArea key_area[NCA_SECTION_TOTAL];
u8 _0x340[0xC0]; // empty.
FsHeader fs_header[NCA_SECTION_TOTAL];
auto GetKeyGeneration() const -> u8 {
if (old_key_gen < key_gen) {
return key_gen;
} else {
return old_key_gen;
}
}
void SetKeyGeneration(u8 key_generation) {
if (key_generation <= 0x2) {
old_key_gen = key_generation;
key_gen = 0;
} else {
old_key_gen = 0x2;
key_gen = key_generation;
}
}
};
static_assert(sizeof(Header) == 0xC00);
Result DecryptKeak(const keys::Keys& keys, Header& header);
Result EncryptKeak(const keys::Keys& keys, Header& header, u8 key_generation);
Result VerifyFixedKey(const Header& header);
// helpers that parse an nca.
Result ParseCnmt(const fs::FsPath& path, u64 program_id, ncm::PackagedContentMeta& header, std::vector<u8>& extended_header, std::vector<NcmPackagedContentInfo>& infos);
Result ParseControl(const fs::FsPath& path, u64 program_id, void* nacp_out = nullptr, s64 nacp_size = 0, std::vector<u8>* icon_out = nullptr);
auto GetKeyGenStr(u8 key_gen) -> const char*;
} // namespace sphaira::nca

View File

@@ -0,0 +1,47 @@
#pragma once
#include <switch.h>
namespace sphaira::ncm {
struct PackagedContentMeta {
u64 title_id;
u32 title_version;
u8 meta_type; // NcmContentMetaType
u8 content_meta_platform; // [17.0.0+]
NcmContentMetaHeader meta_header;
u8 install_type; // NcmContentInstallType
u8 _0x17;
u32 required_sys_version;
u8 _0x1C[0x4];
};
static_assert(sizeof(PackagedContentMeta) == 0x20);
struct ContentStorageRecord {
NcmContentMetaKey key;
u8 storage_id; // NcmStorageId
u8 padding[0x7];
};
union ExtendedHeader {
NcmApplicationMetaExtendedHeader application;
NcmPatchMetaExtendedHeader patch;
NcmAddOnContentMetaExtendedHeader addon;
NcmLegacyAddOnContentMetaExtendedHeader addon_legacy;
NcmDataPatchMetaExtendedHeader data_patch;
};
auto GetMetaTypeStr(u8 meta_type) -> const char*;
auto GetStorageIdStr(u8 storage_id) -> const char*;
auto GetMetaTypeShortStr(u8 meta_type) -> const char*;
auto GetAppId(u8 meta_type, u64 id) -> u64;
auto GetAppId(const NcmContentMetaKey& key) -> u64;
auto GetAppId(const PackagedContentMeta& meta) -> u64;
auto GetContentIdFromStr(const char* str) -> NcmContentId;
Result Delete(NcmContentStorage* cs, const NcmContentId *content_id);
Result Register(NcmContentStorage* cs, const NcmContentId *content_id, const NcmPlaceHolderId *placeholder_id);
} // namespace sphaira::ncm

View File

@@ -0,0 +1,54 @@
#pragma once
#include <switch.h>
namespace sphaira::ncz {
#define NCZ_SECTION_MAGIC 0x4E544345535A434EUL
// todo: byteswap this
#define NCZ_BLOCK_MAGIC std::byteswap(0x4E435A424C4F434BUL)
#define NCZ_SECTION_OFFSET (0x4000 + sizeof(ncz::Header))
struct Header {
u64 magic; // NCZ_SECTION_MAGIC
u64 total_sections;
};
struct BlockHeader {
u64 magic; // NCZ_BLOCK_MAGIC
u8 version;
u8 type;
u8 padding;
u8 block_size_exponent;
u32 total_blocks;
u64 decompressed_size;
};
struct Block {
u32 size;
};
struct BlockInfo {
u64 offset; // compressed offset.
u64 size; // compressed size.
auto InRange(u64 off) const -> bool {
return off < offset + size && off >= offset;
}
};
struct Section {
u64 offset;
u64 size;
u64 crypto_type;
u64 padding;
u8 key[0x10];
u8 counter[0x10];
auto InRange(u64 off) const -> bool {
return off < offset + size && off >= offset;
}
};
} // namespace sphaira::ncz

View File

@@ -0,0 +1,62 @@
#pragma once
#include <switch.h>
namespace sphaira::npdm {
struct Meta {
u32 magic; // "META"
u32 signature_key_generation; // +9.0.0
u32 _0x8;
u8 flags;
u8 _0xD;
u8 main_thread_priority;
u8 main_thread_core_num;
u32 _0x10;
u32 sys_resource_size; // +3.0.0
u32 version;
u32 main_thread_stack_size;
char title_name[0x10];
char product_code[0x10];
u8 _0x40[0x30];
u32 aci0_offset;
u32 aci0_size;
u32 acid_offset;
u32 acid_size;
};
struct Acid {
u8 rsa_sig[0x100];
u8 rsa_pub[0x100];
u32 magic; // "ACID"
u32 size;
u8 version;
u8 _0x209[0x1];
u8 _0x20A[0x2];
u32 flags;
u64 program_id_min;
u64 program_id_max;
u32 fac_offset;
u32 fac_size;
u32 sac_offset;
u32 sac_size;
u32 kac_offset;
u32 kac_size;
u8 _0x238[0x8];
};
struct Aci0 {
u32 magic; // "ACI0"
u8 _0x4[0xC];
u64 program_id;
u8 _0x18[0x8];
u32 fac_offset;
u32 fac_size;
u32 sac_offset;
u32 sac_size;
u32 kac_offset;
u32 kac_size;
u8 _0x38[0x8];
};
} // namespace sphaira::npdm

View File

@@ -0,0 +1,22 @@
#pragma once
#include <switch.h>
#include "ncm.hpp"
namespace sphaira::ns {
enum ApplicationRecordType {
// installed
ApplicationRecordType_Installed = 0x3,
// application is gamecard, but gamecard isn't insterted
ApplicationRecordType_GamecardMissing = 0x5,
// archived
ApplicationRecordType_Archived = 0xB,
};
Result PushApplicationRecord(Service* srv, u64 tid, const ncm::ContentStorageRecord* records, u32 count);
Result ListApplicationRecordContentMeta(Service* srv, u64 offset, u64 tid, ncm::ContentStorageRecord* out_records, u32 count, s32* entries_read);
Result DeleteApplicationRecord(Service* srv, u64 tid);
Result InvalidateApplicationControlCache(Service* srv, u64 tid);
} // namespace sphaira::ns

View File

@@ -0,0 +1,62 @@
/*
* rsa.c
*
* Copyright (c) 2018-2019, SciresM.
* Copyright (c) 2020-2024, DarkMatterCore <pabloacurielz@gmail.com>.
*
* This file is part of nxdumptool (https://github.com/DarkMatterCore/nxdumptool).
*
* nxdumptool is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* nxdumptool is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#ifndef __RSA_H__
#define __RSA_H__
#ifdef __cplusplus
extern "C" {
#endif
#include <stdbool.h>
#include <stddef.h>
#define RSA2048_BYTES 0x100
#define RSA2048_BITS (RSA2048_BYTES * 8)
#define RSA2048_SIG_SIZE RSA2048_BYTES
#define RSA2048_PUBKEY_SIZE RSA2048_BYTES
/// Verifies a RSA-2048-PSS with SHA-256 signature.
/// Suitable for NCA and NPDM signatures.
/// The provided signature and modulus must have sizes of at least RSA2048_SIG_SIZE and RSA2048_PUBKEY_SIZE, respectively.
bool rsa2048VerifySha256BasedPssSignature(const void *data, size_t data_size, const void *signature, const void *modulus, const void *public_exponent, size_t public_exponent_size);
/// Verifies a RSA-2048-PKCS#1 v1.5 with SHA-256 signature.
/// Suitable for ticket and certificate chain signatures.
/// The provided signature and modulus must have sizes of at least RSA2048_SIG_SIZE and RSA2048_PUBKEY_SIZE, respectively.
bool rsa2048VerifySha256BasedPkcs1v15Signature(const void *data, size_t data_size, const void *signature, const void *modulus, const void *public_exponent, size_t public_exponent_size);
/// Performs RSA-2048-OAEP decryption.
/// Suitable to decrypt the titlekey block from personalized tickets.
/// The provided signature and modulus must have sizes of at least RSA2048_SIG_SIZE and RSA2048_PUBKEY_SIZE, respectively.
/// 'label' and 'label_size' arguments are optional -- if not needed, these may be set to NULL and 0, respectively.
bool rsa2048OaepDecrypt(void *dst, size_t dst_size, const void *signature, const void *modulus, const void *public_exponent, size_t public_exponent_size, const void *private_exponent, \
size_t private_exponent_size, const void *label, size_t label_size, size_t *out_size);
#ifdef __cplusplus
}
#endif
#endif /* __RSA_H__ */

View File

@@ -0,0 +1,56 @@
#pragma once
#include <switch/types.h>
#include <switch/result.h>
#include <switch/kernel/mutex.h>
#include <switch/sf/service.h>
#include <switch/services/sm.h>
typedef struct ServiceGuard {
Mutex mutex;
u32 refCount;
} ServiceGuard;
NX_INLINE bool serviceGuardBeginInit(ServiceGuard* g)
{
mutexLock(&g->mutex);
return (g->refCount++) == 0;
}
NX_INLINE Result serviceGuardEndInit(ServiceGuard* g, Result rc, void (*cleanupFunc)(void))
{
if (R_FAILED(rc)) {
cleanupFunc();
--g->refCount;
}
mutexUnlock(&g->mutex);
return rc;
}
NX_INLINE void serviceGuardExit(ServiceGuard* g, void (*cleanupFunc)(void))
{
mutexLock(&g->mutex);
if (g->refCount && (--g->refCount) == 0)
cleanupFunc();
mutexUnlock(&g->mutex);
}
#define NX_GENERATE_SERVICE_GUARD_PARAMS(name, _paramdecl, _parampass) \
\
static ServiceGuard g_##name##Guard; \
NX_INLINE Result _##name##Initialize _paramdecl; \
static void _##name##Cleanup(void); \
\
Result name##Initialize _paramdecl \
{ \
Result rc = 0; \
if (serviceGuardBeginInit(&g_##name##Guard)) \
rc = _##name##Initialize _parampass; \
return serviceGuardEndInit(&g_##name##Guard, rc, _##name##Cleanup); \
} \
\
void name##Exit(void) \
{ \
serviceGuardExit(&g_##name##Guard, _##name##Cleanup); \
}
#define NX_GENERATE_SERVICE_GUARD(name) NX_GENERATE_SERVICE_GUARD_PARAMS(name, (void), ())

View File

@@ -0,0 +1,29 @@
#pragma once
#include <vector>
#include <switch.h>
namespace sphaira::yati::source {
struct Base {
virtual ~Base() = default;
// virtual Result Read(void* buf, s64 off, s64 size, u64* bytes_read) = 0;
virtual Result Read(void* buf, s64 off, s64 size, u64* bytes_read) = 0;
virtual bool IsStream() const {
return false;
}
virtual void SignalCancel() {
}
Result GetOpenResult() const {
return m_open_result;
}
protected:
Result m_open_result{};
};
} // namespace sphaira::yati::source

View File

@@ -0,0 +1,19 @@
#pragma once
#include "base.hpp"
#include "fs.hpp"
#include <switch.h>
namespace sphaira::yati::source {
struct File final : Base {
File(FsFileSystem* fs, const fs::FsPath& path);
~File();
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override;
private:
FsFile m_file{};
};
} // namespace sphaira::yati::source

View File

@@ -0,0 +1,20 @@
#pragma once
#include "base.hpp"
#include "fs.hpp"
#include <cstdio>
#include <switch.h>
namespace sphaira::yati::source {
struct Stdio final : Base {
Stdio(const fs::FsPath& path);
~Stdio();
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override;
private:
std::FILE* m_file{};
};
} // namespace sphaira::yati::source

View File

@@ -0,0 +1,28 @@
#pragma once
#include "base.hpp"
#include <vector>
#include <switch.h>
namespace sphaira::yati::source {
// streams are for data that do not allow for random access,
// such as FTP or MTP.
struct Stream : Base {
virtual ~Stream() = default;
virtual Result ReadChunk(void* buf, s64 size, u64* bytes_read) = 0;
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override;
bool IsStream() const override {
return true;
}
protected:
Result m_open_result{};
private:
s64 m_offset{};
};
} // namespace sphaira::yati::source

View File

@@ -0,0 +1,22 @@
// this is used for testing that streams work, this code isn't used in normal
// release builds as it is slower / less feature complete than normal.
#pragma once
#include "stream.hpp"
#include "fs.hpp"
#include <switch.h>
namespace sphaira::yati::source {
struct StreamFile final : Stream {
StreamFile(FsFileSystem* fs, const fs::FsPath& path);
~StreamFile();
Result ReadChunk(void* buf, s64 size, u64* bytes_read) override;
private:
FsFile m_file{};
s64 m_offset{};
};
} // namespace sphaira::yati::source

View File

@@ -0,0 +1,50 @@
#pragma once
#include "base.hpp"
#include "fs.hpp"
#include "usb/usbds.hpp"
#include <string>
#include <memory>
#include <switch.h>
namespace sphaira::yati::source {
struct Usb final : Base {
enum { USBModule = 523 };
enum : Result {
Result_BadMagic = MAKERESULT(USBModule, 0),
Result_BadVersion = MAKERESULT(USBModule, 1),
Result_BadCount = MAKERESULT(USBModule, 2),
Result_BadTransferSize = MAKERESULT(USBModule, 3),
Result_BadTotalSize = MAKERESULT(USBModule, 4),
};
Usb(u64 transfer_timeout);
~Usb();
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override;
Result Finished(u64 timeout);
Result IsUsbConnected(u64 timeout) {
return m_usb->IsUsbConnected(timeout);
}
Result WaitForConnection(u64 timeout, std::vector<std::string>& out_names);
void SetFileNameForTranfser(const std::string& name);
void SignalCancel() override {
m_usb->Cancel();
}
private:
Result SendCmdHeader(u32 cmdId, size_t dataSize, u64 timeout);
Result SendFileRangeCmd(u64 offset, u64 size, u64 timeout);
private:
std::unique_ptr<usb::UsbDs> m_usb;
std::string m_transfer_file_name{};
};
} // namespace sphaira::yati::source

View File

@@ -0,0 +1,138 @@
/*
* Notes:
* - nca's that use title key encryption are decrypted using Tegra SE, whereas
* standard crypto uses software decryption.
* The latter is almost always (slightly) faster, and removed the need for es patch.
*/
#pragma once
#include "fs.hpp"
#include "source/base.hpp"
#include "container/base.hpp"
#include "ui/progress_box.hpp"
#include <memory>
#include <optional>
namespace sphaira::yati {
enum { YatiModule = 521 };
enum : Result {
// unkown container for the source provided.
Result_ContainerNotFound = MAKERESULT(YatiModule, 10),
Result_Cancelled = MAKERESULT(YatiModule, 11),
// nca required by the cnmt but not found in collection.
Result_NcaNotFound = MAKERESULT(YatiModule, 30),
Result_InvalidNcaReadSize = MAKERESULT(YatiModule, 31),
Result_InvalidNcaSigKeyGen = MAKERESULT(YatiModule, 32),
Result_InvalidNcaMagic = MAKERESULT(YatiModule, 33),
Result_InvalidNcaSignature0 = MAKERESULT(YatiModule, 34),
Result_InvalidNcaSignature1 = MAKERESULT(YatiModule, 35),
// invalid sha256 over the entire nca.
Result_InvalidNcaSha256 = MAKERESULT(YatiModule, 36),
// section could not be found.
Result_NczSectionNotFound = MAKERESULT(YatiModule, 50),
// section count == 0.
Result_InvalidNczSectionCount = MAKERESULT(YatiModule, 51),
// block could not be found.
Result_NczBlockNotFound = MAKERESULT(YatiModule, 52),
// block version != 2.
Result_InvalidNczBlockVersion = MAKERESULT(YatiModule, 53),
// block type != 1.
Result_InvalidNczBlockType = MAKERESULT(YatiModule, 54),
// block count == 0.
Result_InvalidNczBlockTotal = MAKERESULT(YatiModule, 55),
// block size exponent < 14 || > 32.
Result_InvalidNczBlockSizeExponent = MAKERESULT(YatiModule, 56),
// zstd error while decompressing ncz.
Result_InvalidNczZstdError = MAKERESULT(YatiModule, 57),
// nca has rights_id but matching ticket wasn't found.
Result_TicketNotFound = MAKERESULT(YatiModule, 70),
// found ticket has missmatching rights_id from it's name.
Result_InvalidTicketBadRightsId = MAKERESULT(YatiModule, 71),
Result_InvalidTicketVersion = MAKERESULT(YatiModule, 72),
Result_InvalidTicketKeyType = MAKERESULT(YatiModule, 73),
Result_InvalidTicketKeyRevision = MAKERESULT(YatiModule, 74),
// cert not found for the ticket.
Result_CertNotFound = MAKERESULT(YatiModule, 90),
// unable to fetch header from ncm database.
Result_NcmDbCorruptHeader = MAKERESULT(YatiModule, 110),
// unable to total infos from ncm database.
Result_NcmDbCorruptInfos = MAKERESULT(YatiModule, 111),
};
struct Config {
bool sd_card_install{};
// sets the performance mode to FastLoad which boosts the CPU clock
// and lowers the GPU clock.
bool boost_mode{};
// enables downgrading patch / data patch (dlc) version.
bool allow_downgrade{};
// ignores the install if already installed.
// checks that every nca is available.
bool skip_if_already_installed{};
// installs tickets only.
bool ticket_only{};
// flags to enable / disable install of specific types.
bool skip_base{};
bool skip_patch{};
bool skip_addon{};
bool skip_data_patch{};
bool skip_ticket{};
// enables the option to skip sha256 verification.
bool skip_nca_hash_verify{};
// enables the option to skip rsa nca fixed key verification.
bool skip_rsa_header_fixed_key_verify{};
// enables the option to skip rsa npdm fixed key verification.
bool skip_rsa_npdm_fixed_key_verify{};
// if set, it will ignore the distribution bit in the nca header.
bool ignore_distribution_bit{};
// converts titlekey to standard crypto, also known as "ticketless".
// this will not work with addon (dlc), so, addon tickets will be installed.
bool convert_to_standard_crypto{};
// encrypts the keak with master key 0, this allows the game to be launched on every fw.
// implicitly performs standard crypto.
bool lower_master_key{};
// sets the system_firmware field in the cnmt extended header.
// if mkey is higher than fw version, the game still won't launch
// as the fw won't have the key to decrypt keak.
bool lower_system_version{};
};
// overridable options, set to avoid
struct ConfigOverride {
std::optional<bool> sd_card_install{};
std::optional<bool> skip_nca_hash_verify{};
std::optional<bool> skip_rsa_header_fixed_key_verify{};
std::optional<bool> skip_rsa_npdm_fixed_key_verify{};
std::optional<bool> ignore_distribution_bit{};
std::optional<bool> convert_to_standard_crypto{};
std::optional<bool> lower_master_key{};
std::optional<bool> lower_system_version{};
};
Result InstallFromFile(ui::ProgressBox* pbox, FsFileSystem* fs, const fs::FsPath& path, const ConfigOverride& override = {});
Result InstallFromStdioFile(ui::ProgressBox* pbox, const fs::FsPath& path, const ConfigOverride& override = {});
Result InstallFromSource(ui::ProgressBox* pbox, std::shared_ptr<source::Base> source, const fs::FsPath& path, const ConfigOverride& override = {});
Result InstallFromContainer(ui::ProgressBox* pbox, std::shared_ptr<container::Base> container, const ConfigOverride& override = {});
Result InstallFromCollections(ui::ProgressBox* pbox, std::shared_ptr<source::Base> source, const container::Collections& collections, const ConfigOverride& override = {});
} // namespace sphaira::yati

View File

@@ -1,7 +1,11 @@
#include "ui/menus/main_menu.hpp"
#include "ui/error_box.hpp"
#include "ui/option_box.hpp"
#include "ui/bubbles.hpp"
#include "ui/sidebar.hpp"
#include "ui/popup_list.hpp"
#include "ui/option_box.hpp"
#include "ui/progress_box.hpp"
#include "ui/error_box.hpp"
#include "ui/menus/main_menu.hpp"
#include "app.hpp"
#include "log.hpp"
@@ -34,8 +38,33 @@ extern "C" {
namespace sphaira {
namespace {
constexpr fs::FsPath DEFAULT_MUSIC_PATH = "/config/sphaira/themes/default_music.bfstm";
constexpr const char* DEFAULT_MUSIC_URL = "https://files.catbox.moe/1ovji1.bfstm";
// constexpr const char* DEFAULT_MUSIC_URL = "https://raw.githubusercontent.com/ITotalJustice/sphaira/refs/heads/master/assets/default_music.bfstm";
void download_default_music() {
App::Push(std::make_shared<ui::ProgressBox>(0, "Downloading "_i18n, "default_music.bfstm", [](auto pbox){
const auto result = curl::Api().ToFile(
curl::Url{DEFAULT_MUSIC_URL},
curl::Path{DEFAULT_MUSIC_PATH},
curl::OnProgress{pbox->OnDownloadProgressCallback()}
);
return result.success;
}, [](bool success){
if (success) {
App::Notify("Downloaded "_i18n + "default_music.bfstm");
App::SetTheme(App::GetThemeIndex());
} else {
App::Push(std::make_shared<ui::ErrorBox>(
"Failed to download default_music.bfstm, please try again"_i18n
));
}
}));
}
struct ThemeData {
fs::FsPath music_path{"/config/sphaira/themes/default_music.bfstm"};
fs::FsPath music_path{DEFAULT_MUSIC_PATH};
std::string elements[ThemeEntryID_MAX]{};
};
@@ -271,30 +300,33 @@ auto GetNroIcon(const std::vector<u8>& nro_icon) -> std::vector<u8> {
auto LoadThemeMeta(const fs::FsPath& path, ThemeMeta& meta) -> bool {
meta = {};
char buf[FS_MAX_PATH]{};
int len{};
len = ini_gets("meta", "name", "", buf, sizeof(buf) - 1, path);
if (len <= 1) {
auto cb = [](const mTCHAR *Section, const mTCHAR *Key, const mTCHAR *Value, void *UserData) -> int {
auto meta = static_cast<ThemeMeta*>(UserData);
if (!std::strcmp(Section, "meta")) {
if (!std::strcmp(Key, "name")) {
meta->name = Value;
} else if (!std::strcmp(Key, "author")) {
meta->author = Value;
} else if (!std::strcmp(Key, "version")) {
meta->version = Value;
} else if (!std::strcmp(Key, "inherit")) {
meta->inherit = Value;
}
return 1;
}
return 0;
};
if (!ini_browse(cb, &meta, path)) {
return false;
}
meta.name = buf;
len = ini_gets("meta", "author", "", buf, sizeof(buf) - 1, path);
if (len <= 1) {
if (meta.name.empty() || meta.author.empty() || meta.version.empty()) {
return false;
}
meta.author = buf;
len = ini_gets("meta", "version", "", buf, sizeof(buf) - 1, path);
if (len <= 1) {
return false;
}
meta.version = buf;
len = ini_gets("meta", "inherit", "", buf, sizeof(buf) - 1, path);
if (len > 1) {
meta.inherit = buf;
}
log_write("loaded meta from: %s\n", path.s);
meta.ini_path = path;
@@ -327,7 +359,7 @@ void LoadThemeInternal(ThemeMeta meta, ThemeData& theme_data, int inherit_level
}
}
static constexpr auto cb = [](const mTCHAR *Section, const mTCHAR *Key, const mTCHAR *Value, void *UserData) -> int {
auto cb = [](const mTCHAR *Section, const mTCHAR *Key, const mTCHAR *Value, void *UserData) -> int {
auto theme_data = static_cast<ThemeData*>(UserData);
if (!std::strcmp(Section, "theme")) {
@@ -544,7 +576,7 @@ auto App::GetThemeIndex() -> s64 {
return g_app->m_theme_index;
}
auto App::GetDefaultImage(int* w, int* h) -> int {
auto App::GetDefaultImage() -> int {
return g_app->m_default_image;
}
@@ -569,7 +601,19 @@ auto App::GetReplaceHbmenuEnable() -> bool {
}
auto App::GetInstallEnable() -> bool {
return g_app->m_install.Get();
if (IsEmunand()) {
return GetInstallEmummcEnable();
} else {
return GetInstallSysmmcEnable();
}
}
auto App::GetInstallSysmmcEnable() -> bool {
return g_app->m_install_sysmmc.GetOr("install");
}
auto App::GetInstallEmummcEnable() -> bool {
return g_app->m_install_emummc.GetOr("install");
}
auto App::GetInstallSdEnable() -> bool {
@@ -580,10 +624,6 @@ auto App::GetInstallPrompt() -> bool {
return g_app->m_install_prompt.Get();
}
auto App::GetThemeShuffleEnable() -> bool {
return g_app->m_theme_shuffle.Get();
}
auto App::GetThemeMusicEnable() -> bool {
return g_app->m_theme_music.Get();
}
@@ -604,6 +644,10 @@ auto App::GetTextScrollSpeed() -> long {
return g_app->m_text_scroll_speed.Get();
}
auto App::Get12HourTimeEnable() -> bool {
return g_app->m_12hour_time.Get();
}
void App::SetNxlinkEnable(bool enable) {
if (App::GetNxlinkEnable() != enable) {
g_app->m_nxlink_enabled.Set(enable);
@@ -675,7 +719,7 @@ void App::SetReplaceHbmenuEnable(bool enable) {
}
if (R_SUCCEEDED(rc) && !std::strcmp(sphaira_nacp.lang[0].name, "sphaira")) {
if (std::strcmp(sphaira_nacp.display_version, hbmenu_nacp.display_version) < 0) {
if (IsVersionNewer(sphaira_nacp.display_version, hbmenu_nacp.display_version)) {
if (R_FAILED(rc = fs.copy_entire_file(sphaira_path, "/hbmenu.nro"))) {
log_write("failed to copy entire file: %s 0x%X module: %u desc: %u\n", sphaira_path.s, rc, R_MODULE(rc), R_DESCRIPTION(rc));
} else {
@@ -725,8 +769,12 @@ void App::SetReplaceHbmenuEnable(bool enable) {
}
}
void App::SetInstallEnable(bool enable) {
g_app->m_install.Set(enable);
void App::SetInstallSysmmcEnable(bool enable) {
g_app->m_install_sysmmc.Set(enable);
}
void App::SetInstallEmummcEnable(bool enable) {
g_app->m_install_emummc.Set(enable);
}
void App::SetInstallSdEnable(bool enable) {
@@ -737,20 +785,20 @@ void App::SetInstallPrompt(bool enable) {
g_app->m_install_prompt.Set(enable);
}
void App::SetThemeShuffleEnable(bool enable) {
g_app->m_theme_shuffle.Set(enable);
}
void App::SetThemeMusicEnable(bool enable) {
g_app->m_theme_music.Set(enable);
PlaySoundEffect(SoundEffect::SoundEffect_Music);
}
void App::Set12HourTimeEnable(bool enable) {
g_app->m_12hour_time.Set(enable);
}
void App::SetMtpEnable(bool enable) {
if (App::GetMtpEnable() != enable) {
g_app->m_mtp_enabled.Set(enable);
if (enable) {
hazeInitialize(haze_callback);
hazeInitialize(haze_callback, 0x2C, 2);
} else {
hazeExit();
}
@@ -1001,13 +1049,16 @@ void App::Draw() {
}
m_notif_manager.Draw(vg, &m_theme);
ui::bubble::Draw(vg, &m_theme);
nvgResetTransform(vg);
nvgEndFrame(this->vg);
this->queue.presentImage(this->swapchain, slot);
}
auto App::GetApp() -> App* {
return g_app;
}
auto App::GetVg() -> NVGcontext* {
return g_app->vg;
}
@@ -1100,7 +1151,6 @@ void App::CloseTheme() {
if (m_sound_ids[SoundEffect_Music]) {
plsrPlayerFree(m_sound_ids[SoundEffect_Music]);
m_sound_ids[SoundEffect_Music] = nullptr;
plsrBFSTMClose(&m_theme.music);
}
for (auto& e : m_theme.elements) {
@@ -1130,10 +1180,12 @@ void App::LoadTheme(const ThemeMeta& meta) {
// load music
if (!theme_data.music_path.empty()) {
if (R_SUCCEEDED(plsrBFSTMOpen(theme_data.music_path, &m_theme.music))) {
if (R_SUCCEEDED(plsrPlayerLoadStream(&m_theme.music, &m_sound_ids[SoundEffect_Music]))) {
PLSR_BFSTM music_stream;
if (R_SUCCEEDED(plsrBFSTMOpen(theme_data.music_path, &music_stream))) {
if (R_SUCCEEDED(plsrPlayerLoadStream(&music_stream, &m_sound_ids[SoundEffect_Music]))) {
PlaySoundEffect(SoundEffect_Music);
}
plsrBFSTMClose(&music_stream);
}
}
}
@@ -1177,11 +1229,14 @@ void App::ScanThemeEntries() {
ScanThemes("romfs:/themes/");
romfsExit();
}
// then load custom entries
ScanThemes("/config/sphaira/themes/");
}
App::App(const char* argv0) {
TimeStamp ts;
g_app = this;
m_start_timestamp = armGetSystemTick();
if (!std::strncmp(argv0, "sdmc:/", 6)) {
@@ -1205,10 +1260,11 @@ App::App(const char* argv0) {
if (App::GetLogEnable()) {
log_file_init();
log_write("hello world\n");
App::Notify("Warning! Logs are enabled, Sphaira will run slowly!"_i18n);
}
if (App::GetMtpEnable()) {
hazeInitialize(haze_callback);
hazeInitialize(haze_callback, 0x2C, 2);
}
if (App::GetFtpEnable()) {
@@ -1282,9 +1338,11 @@ App::App(const char* argv0) {
}
}
// only enable audio in non-applet mode due to audren fatal.
// disable audio in applet mode with a suspended application due to audren fatal.
// see: https://github.com/ITotalJustice/sphaira/issues/92
if (IsApplication()) {
if (IsAppletWithSuspendedApp()) {
App::Notify("Audio disabled due to suspended game"_i18n);
} else {
plsrPlayerInit();
}
@@ -1294,16 +1352,21 @@ App::App(const char* argv0) {
if (R_SUCCEEDED(plsrBFSAROpen("qlaunch:/sound/qlaunch.bfsar", &qlaunch_bfsar))) {
ON_SCOPE_EXIT(plsrBFSARClose(&qlaunch_bfsar));
plsrPlayerLoadSoundByName(&qlaunch_bfsar, "SeGameIconFocus", &m_sound_ids[SoundEffect_Focus]);
plsrPlayerLoadSoundByName(&qlaunch_bfsar, "SeGameIconScroll", &m_sound_ids[SoundEffect_Scroll]);
plsrPlayerLoadSoundByName(&qlaunch_bfsar, "SeGameIconLimit", &m_sound_ids[SoundEffect_Limit]);
plsrPlayerLoadSoundByName(&qlaunch_bfsar, "SeStartupMenu_game", &m_sound_ids[SoundEffect_Startup]);
plsrPlayerLoadSoundByName(&qlaunch_bfsar, "SeGameIconAdd", &m_sound_ids[SoundEffect_Install]);
plsrPlayerLoadSoundByName(&qlaunch_bfsar, "SeInsertError", &m_sound_ids[SoundEffect_Error]);
const auto load_sound = [&](const char* name, u32 id) {
if (R_FAILED(plsrPlayerLoadSoundByName(&qlaunch_bfsar, name, &m_sound_ids[id]))) {
log_write("[PLSR] failed to load sound effect: %s\n", name);
}
};
load_sound("SeGameIconFocus", SoundEffect_Focus);
load_sound("SeGameIconScroll", SoundEffect_Scroll);
load_sound("SeGameIconLimit", SoundEffect_Limit);
load_sound("StartupMenu_Game", SoundEffect_Startup);
load_sound("SeGameIconAdd", SoundEffect_Install);
load_sound("SeInsertError", SoundEffect_Error);
plsrPlayerSetVolume(m_sound_ids[SoundEffect_Limit], 2.0f);
plsrPlayerSetVolume(m_sound_ids[SoundEffect_Focus], 0.5f);
PlaySoundEffect(SoundEffect_Startup);
}
} else {
log_write("failed to mount romfs 0x0100000000001000\n");
@@ -1313,11 +1376,7 @@ App::App(const char* argv0) {
fs::FsPath theme_path{};
constexpr fs::FsPath default_theme_path{"romfs:/themes/abyss_theme.ini"};
if (App::GetThemeShuffleEnable() && m_theme_meta_entries.size()) {
theme_path = m_theme_meta_entries[randomGet64() % m_theme_meta_entries.size()].ini_path;
} else {
ini_gets("config", "theme", default_theme_path, theme_path, sizeof(theme_path), CONFIG_PATH);
}
ini_gets("config", "theme", default_theme_path, theme_path, sizeof(theme_path), CONFIG_PATH);
// try and load previous theme, default to previous version otherwise.
ThemeMeta theme_meta;
@@ -1348,6 +1407,10 @@ App::App(const char* argv0) {
// padInitializeDefault(&m_pad);
padInitializeAny(&m_pad);
// usbHsFsSetFileSystemMountFlags(UsbHsFsMountFlags_ReadOnly);
// usbHsFsSetPopulateCallback();
// usbHsFsInitialize(0);
m_prev_timestamp = ini_getl("paths", "timestamp", 0, App::CONFIG_PATH);
const auto last_launch_path_size = ini_gets("paths", "last_launch_path", "", m_prev_last_launch, sizeof(m_prev_last_launch), App::CONFIG_PATH);
fs::FsPath last_launch_path;
@@ -1372,15 +1435,6 @@ App::App(const char* argv0) {
const long old_launch_count = ini_getl(GetExePath(), "launch_count", 0, App::PLAYLOG_PATH);
ini_putl(GetExePath(), "launch_count", old_launch_count + 1, App::PLAYLOG_PATH);
s64 sd_free_space;
if (R_SUCCEEDED(fs.GetFreeSpace("/", &sd_free_space))) {
log_write("sd_free_space: %zd\n", sd_free_space);
}
s64 sd_total_space;
if (R_SUCCEEDED(fs.GetTotalSpace("/", &sd_total_space))) {
log_write("sd_total_space: %zd\n", sd_total_space);
}
// load default image
if (R_SUCCEEDED(romfsInit())) {
ON_SCOPE_EXIT(romfsExit());
@@ -1390,11 +1444,8 @@ App::App(const char* argv0) {
}
}
// soon (tm)
// ui::bubble::Init();
App::Push(std::make_shared<ui::menu::main::MainMenu>());
log_write("finished app constructor\n");
log_write("finished app constructor, time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs());
}
void App::PlaySoundEffect(SoundEffect effect) {
@@ -1410,14 +1461,225 @@ void App::PlaySoundEffect(SoundEffect effect) {
plsrPlayerPlay(id);
}
void App::DisplayThemeOptions(bool left_side) {
ui::SidebarEntryArray::Items theme_items{};
const auto theme_meta = App::GetThemeMetaList();
for (auto& p : theme_meta) {
theme_items.emplace_back(p.name);
}
auto options = std::make_shared<ui::Sidebar>("Theme Options"_i18n, left_side ? ui::Sidebar::Side::LEFT : ui::Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(options));
options->Add(std::make_shared<ui::SidebarEntryArray>("Select Theme"_i18n, theme_items, [](s64& index_out){
App::SetTheme(index_out);
}, App::GetThemeIndex()));
options->Add(std::make_shared<ui::SidebarEntryBool>("Music"_i18n, App::GetThemeMusicEnable(), [](bool& enable){
App::SetThemeMusicEnable(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("12 Hour Time"_i18n, App::Get12HourTimeEnable(), [](bool& enable){
App::Set12HourTimeEnable(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryCallback>("Download Default Music"_i18n, [](){
// check if we already have music
if (fs::FileExists(DEFAULT_MUSIC_PATH)) {
App::Push(std::make_shared<ui::OptionBox>(
"Overwrite current default music?"_i18n,
"No"_i18n, "Yes"_i18n, 0, [](auto op_index){
if (op_index && *op_index) {
download_default_music();
}
}
));
} else {
download_default_music();
}
}));
}
void App::DisplayNetworkOptions(bool left_side) {
}
void App::DisplayMiscOptions(bool left_side) {
auto options = std::make_shared<ui::Sidebar>("Misc Options"_i18n, left_side ? ui::Sidebar::Side::LEFT : ui::Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(options));
for (auto& e : ui::menu::main::GetMiscMenuEntries()) {
if (e.name == g_app->m_right_side_menu.Get()) {
continue;
}
if (e.IsInstall() && !App::GetInstallEnable()) {
continue;
}
options->Add(std::make_shared<ui::SidebarEntryCallback>(i18n::get(e.title), [e](){
App::Push(e.func());
}));
}
}
void App::DisplayAdvancedOptions(bool left_side) {
auto options = std::make_shared<ui::Sidebar>("Advanced Options"_i18n, left_side ? ui::Sidebar::Side::LEFT : ui::Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(options));
ui::SidebarEntryArray::Items text_scroll_speed_items;
text_scroll_speed_items.push_back("Slow"_i18n);
text_scroll_speed_items.push_back("Normal"_i18n);
text_scroll_speed_items.push_back("Fast"_i18n);
std::vector<const char*> menu_names;
for (auto& e : ui::menu::main::GetMiscMenuEntries()) {
if (!e.IsShortcut()) {
continue;
}
if (e.IsInstall() && !App::GetInstallEnable()) {
continue;
}
menu_names.emplace_back(e.name);
}
ui::SidebarEntryArray::Items right_side_menu_items;
for (auto& str : menu_names) {
right_side_menu_items.push_back(i18n::get(str));
}
const auto it = std::find(menu_names.cbegin(), menu_names.cend(), g_app->m_right_side_menu.Get());
if (it == menu_names.cend()) {
g_app->m_right_side_menu.Set(menu_names[0]);
}
options->Add(std::make_shared<ui::SidebarEntryBool>("Logging"_i18n, App::GetLogEnable(), [](bool& enable){
App::SetLogEnable(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Replace hbmenu on exit"_i18n, App::GetReplaceHbmenuEnable(), [](bool& enable){
App::SetReplaceHbmenuEnable(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryArray>("Text scroll speed"_i18n, text_scroll_speed_items, [](s64& index_out){
App::SetTextScrollSpeed(index_out);
}, App::GetTextScrollSpeed()));
options->Add(std::make_shared<ui::SidebarEntryArray>("Set right-side menu"_i18n, right_side_menu_items, [menu_names](s64& index_out){
const auto e = menu_names[index_out];
if (g_app->m_right_side_menu.Get() != e) {
g_app->m_right_side_menu.Set(e);
App::Push(std::make_shared<ui::OptionBox>(
"Press OK to restart Sphaira"_i18n, "OK"_i18n, [](auto){
App::ExitRestart();
}
));
}
}, i18n::get(g_app->m_right_side_menu.Get())));
options->Add(std::make_shared<ui::SidebarEntryCallback>("Install options"_i18n, [left_side](){
App::DisplayInstallOptions(left_side);
}));
}
void App::DisplayInstallOptions(bool left_side) {
auto options = std::make_shared<ui::Sidebar>("Install Options"_i18n, left_side ? ui::Sidebar::Side::LEFT : ui::Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(options));
ui::SidebarEntryArray::Items install_items;
install_items.push_back("System memory"_i18n);
install_items.push_back("microSD card"_i18n);
options->Add(std::make_shared<ui::SidebarEntryBool>("Enable sysmmc"_i18n, App::GetInstallSysmmcEnable(), [](bool& enable){
App::SetInstallSysmmcEnable(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Enable emummc"_i18n, App::GetInstallEmummcEnable(), [](bool& enable){
App::SetInstallEmummcEnable(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Show install warning"_i18n, App::GetApp()->m_install_prompt.Get(), [](bool& enable){
App::GetApp()->m_install_prompt.Set(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryArray>("Install location"_i18n, install_items, [](s64& index_out){
App::SetInstallSdEnable(index_out);
}, (s64)App::GetInstallSdEnable()));
options->Add(std::make_shared<ui::SidebarEntryBool>("Boost CPU clock"_i18n, App::GetApp()->m_boost_mode.Get(), [](bool& enable){
App::GetApp()->m_boost_mode.Set(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Allow downgrade"_i18n, App::GetApp()->m_allow_downgrade.Get(), [](bool& enable){
App::GetApp()->m_allow_downgrade.Set(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip if already installed"_i18n, App::GetApp()->m_skip_if_already_installed.Get(), [](bool& enable){
App::GetApp()->m_skip_if_already_installed.Set(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Ticket only"_i18n, App::GetApp()->m_ticket_only.Get(), [](bool& enable){
App::GetApp()->m_ticket_only.Set(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip base"_i18n, App::GetApp()->m_skip_base.Get(), [](bool& enable){
App::GetApp()->m_skip_base.Set(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip patch"_i18n, App::GetApp()->m_skip_patch.Get(), [](bool& enable){
App::GetApp()->m_skip_patch.Set(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip dlc"_i18n, App::GetApp()->m_skip_addon.Get(), [](bool& enable){
App::GetApp()->m_skip_addon.Set(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip data patch"_i18n, App::GetApp()->m_skip_data_patch.Get(), [](bool& enable){
App::GetApp()->m_skip_data_patch.Set(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip ticket"_i18n, App::GetApp()->m_skip_ticket.Get(), [](bool& enable){
App::GetApp()->m_skip_ticket.Set(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("skip NCA hash verify"_i18n, App::GetApp()->m_skip_nca_hash_verify.Get(), [](bool& enable){
App::GetApp()->m_skip_nca_hash_verify.Set(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip RSA header verify"_i18n, App::GetApp()->m_skip_rsa_header_fixed_key_verify.Get(), [](bool& enable){
App::GetApp()->m_skip_rsa_header_fixed_key_verify.Set(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip RSA NPDM verify"_i18n, App::GetApp()->m_skip_rsa_npdm_fixed_key_verify.Get(), [](bool& enable){
App::GetApp()->m_skip_rsa_npdm_fixed_key_verify.Set(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Ignore distribution bit"_i18n, App::GetApp()->m_ignore_distribution_bit.Get(), [](bool& enable){
App::GetApp()->m_ignore_distribution_bit.Set(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Convert to standard crypto"_i18n, App::GetApp()->m_convert_to_standard_crypto.Get(), [](bool& enable){
App::GetApp()->m_convert_to_standard_crypto.Set(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Lower master key"_i18n, App::GetApp()->m_lower_master_key.Get(), [](bool& enable){
App::GetApp()->m_lower_master_key.Set(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Lower system version"_i18n, App::GetApp()->m_lower_system_version.Get(), [](bool& enable){
App::GetApp()->m_lower_system_version.Set(enable);
}));
}
App::~App() {
log_write("starting to exit\n");
i18n::exit();
curl::Exit();
ui::bubble::Exit();
// 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.
m_widgets.clear();
@@ -1482,7 +1744,7 @@ App::~App() {
// found sphaira, now lets get compare version
if (R_SUCCEEDED(rc) && !std::strcmp(sphaira_nacp.lang[0].name, "sphaira")) {
if (std::strcmp(hbmenu_nacp.display_version, sphaira_nacp.display_version) < 0) {
if (IsVersionNewer(hbmenu_nacp.display_version, sphaira_nacp.display_version)) {
if (R_FAILED(rc = fs.copy_entire_file(GetExePath(), sphaira_path))) {
log_write("failed to copy entire file: %s 0x%X module: %u desc: %u\n", sphaira_path.s, rc, R_MODULE(rc), R_DESCRIPTION(rc));
} else {
@@ -1520,6 +1782,16 @@ App::~App() {
ini_putl("paths", "timestamp", timestamp, App::CONFIG_PATH);
}
auto App::GetVersionFromString(const char* str) -> u32 {
u32 major{}, minor{}, macro{};
std::sscanf(str, "%u.%u.%u", &major, &minor, &macro);
return MAKEHOSVERSION(major, minor, macro);
}
auto App::IsVersionNewer(const char* current, const char* new_version) -> u32 {
return GetVersionFromString(current) < GetVersionFromString(new_version);
}
void App::createFramebufferResources() {
this->swapchain = nullptr;

View File

@@ -3,12 +3,15 @@
#include "defines.hpp"
#include "evman.hpp"
#include "fs.hpp"
#include <switch.h>
#include <cstring>
#include <cassert>
#include <vector>
#include <deque>
#include <mutex>
#include <algorithm>
#include <ranges>
#include <curl/curl.h>
#include <yyjson.h>
@@ -25,7 +28,7 @@ namespace {
log_write("curl_share_setopt(%s, %s) msg: %s\n", #opt, #v, curl_share_strerror(r)); \
} \
constexpr auto API_AGENT = "ITotalJustice";
constexpr auto API_AGENT = "TotalJustice";
constexpr u64 CHUNK_SIZE = 1024*1024;
constexpr auto MAX_THREADS = 4;
constexpr int THREAD_PRIO = 0x2C;
@@ -33,8 +36,18 @@ constexpr int THREAD_CORE = 1;
std::atomic_bool g_running{};
CURLSH* g_curl_share{};
// this is used for single threaded blocking installs.
// avoids the needed for re-creating the handle each time.
CURL* g_curl_single{};
Mutex g_mutex_share[CURL_LOCK_DATA_LAST]{};
struct UploadStruct {
std::span<const u8> data;
s64 offset{};
s64 size{};
FsFile f{};
};
struct DataStruct {
std::vector<u8> data;
s64 offset{};
@@ -42,6 +55,16 @@ struct DataStruct {
s64 file_offset{};
};
struct SeekCustomData {
OnUploadSeek cb{};
s64 size{};
};
// helper for creating webdav folders as libcurl does not have built-in
// support for it.
// only creates the folders if they don't exist.
auto WebdavCreateFolder(CURL* curl, const Api& e) -> bool;
auto generate_key_from_path(const fs::FsPath& path) -> std::string {
const auto key = crc32Calculate(path.s, path.size());
return std::to_string(key);
@@ -195,6 +218,8 @@ private:
} else {
const auto update_entry = [this, &hash_key](const char* tag, const std::string& value) {
if (value.empty()) {
// workaround for appstore accepting etags but not returning them.
yyjson_mut_obj_remove_str(hash_key, tag);
return true;
} else {
auto key = yyjson_mut_obj_get(hash_key, tag);
@@ -300,7 +325,7 @@ struct ThreadQueue {
threadClose(&m_thread);
}
auto Add(const Api& api) -> bool {
auto Add(const Api& api, bool is_upload = false) -> bool {
if (api.GetUrl().empty() || api.GetPath().empty() || !api.GetOnComplete()) {
return false;
}
@@ -310,10 +335,10 @@ struct ThreadQueue {
switch (api.GetPriority()) {
case Priority::Normal:
m_entries.emplace_back(api);
m_entries.emplace_back(api).api.SetUpload(is_upload);
break;
case Priority::High:
m_entries.emplace_front(api);
m_entries.emplace_front(api).api.SetUpload(is_upload);
break;
}
@@ -364,6 +389,94 @@ auto ProgressCallbackFunc2(void *clientp, curl_off_t dltotal, curl_off_t dlnow,
return 0;
}
auto SeekCallback(void *clientp, curl_off_t offset, int origin) -> int {
if (!g_running) {
return 0;
}
auto data_struct = static_cast<UploadStruct*>(clientp);
if (origin == SEEK_SET) {
offset = offset;
} else if (origin == SEEK_CUR) {
offset = data_struct->offset + offset;
} else if (origin == SEEK_END) {
offset = data_struct->size;
}
if (offset < 0 || offset > data_struct->size) {
return CURL_SEEKFUNC_CANTSEEK;
}
data_struct->offset = offset;
return CURL_SEEKFUNC_OK;
}
auto SeekCustomCallback(void *clientp, curl_off_t offset, int origin) -> int {
if (!g_running) {
return 0;
}
auto data_struct = static_cast<SeekCustomData*>(clientp);
if (origin != SEEK_SET || offset < 0 || offset > data_struct->size) {
return CURL_SEEKFUNC_CANTSEEK;
}
if (!data_struct->cb(offset)) {
return CURL_SEEKFUNC_CANTSEEK;
}
return CURL_SEEKFUNC_OK;
}
auto ReadFileCallback(char *ptr, size_t size, size_t nmemb, void *userp) -> size_t {
if (!g_running) {
return 0;
}
auto data_struct = static_cast<UploadStruct*>(userp);
const auto realsize = size * nmemb;
u64 bytes_read;
if (R_FAILED(fsFileRead(&data_struct->f, data_struct->offset, ptr, realsize, FsReadOption_None, &bytes_read))) {
log_write("reading file error\n");
return 0;
}
data_struct->offset += bytes_read;
svcSleepThread(YieldType_WithoutCoreMigration);
return bytes_read;
}
auto ReadMemoryCallback(char *ptr, size_t size, size_t nmemb, void *userp) -> size_t {
if (!g_running) {
return 0;
}
auto data_struct = static_cast<UploadStruct*>(userp);
auto realsize = size * nmemb;
realsize = std::min(realsize, data_struct->data.size() - data_struct->offset);
std::memcpy(ptr, data_struct->data.data(), realsize);
data_struct->offset += realsize;
svcSleepThread(YieldType_WithoutCoreMigration);
return realsize;
}
auto ReadCustomCallback(char *ptr, size_t size, size_t nmemb, void *userp) -> size_t {
if (!g_running) {
return 0;
}
auto data_struct = static_cast<UploadInfo*>(userp);
auto realsize = size * nmemb;
const auto result = data_struct->m_callback(ptr, realsize);
svcSleepThread(YieldType_WithoutCoreMigration);
return result;
}
auto WriteMemoryCallback(void *contents, size_t size, size_t num_files, void *userp) -> size_t {
if (!g_running) {
return 0;
@@ -379,11 +492,9 @@ auto WriteMemoryCallback(void *contents, size_t size, size_t num_files, void *us
data_struct->data.resize(data_struct->offset + realsize);
std::memcpy(data_struct->data.data() + data_struct->offset, contents, realsize);
data_struct->offset += realsize;
svcSleepThread(YieldType_WithoutCoreMigration);
return realsize;
}
@@ -442,6 +553,121 @@ auto header_callback(char* b, size_t size, size_t nitems, void* userdata) -> siz
return numbytes;
}
auto EscapeString(CURL* curl, const std::string& str) -> std::string {
char* s{};
if (!curl) {
s = curl_escape(str.data(), str.length());
} else {
s = curl_easy_escape(curl, str.data(), str.length());
}
if (!s) {
return str;
}
const std::string result = s;
curl_free(s);
return result;
}
auto EncodeUrl(std::string url) -> std::string {
log_write("[CURL] encoding url\n");
if (url.starts_with("webdav://")) {
log_write("[CURL] updating host\n");
url.replace(0, std::strlen("webdav"), "https");
log_write("[CURL] updated host: %s\n", url.c_str());
}
auto clu = curl_url();
R_UNLESS(clu, url);
ON_SCOPE_EXIT(curl_url_cleanup(clu));
log_write("[CURL] setting url\n");
CURLUcode clu_code;
clu_code = curl_url_set(clu, CURLUPART_URL, url.c_str(), CURLU_URLENCODE);
R_UNLESS(clu_code == CURLUE_OK, url);
log_write("[CURL] set url success\n");
char* encoded_url;
clu_code = curl_url_get(clu, CURLUPART_URL, &encoded_url, 0);
R_UNLESS(clu_code == CURLUE_OK, url);
log_write("[CURL] encoded url: %s [vs]: %s\n", encoded_url, url.c_str());
const std::string out = encoded_url;
curl_free(encoded_url);
return out;
}
void SetCommonCurlOptions(CURL* curl, const Api& e) {
CURL_EASY_SETOPT_LOG(curl, CURLOPT_USERAGENT, API_AGENT);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_FOLLOWLOCATION, 1L);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_SSL_VERIFYPEER, 0L);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_SSL_VERIFYHOST, 0L);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_FAILONERROR, 1L);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_NOPROGRESS, 0L);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_SHARE, g_curl_share);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_BUFFERSIZE, 1024*512);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_UPLOAD_BUFFERSIZE, 1024*512);
// enable all forms of compression supported by libcurl.
CURL_EASY_SETOPT_LOG(curl, CURLOPT_ACCEPT_ENCODING, "");
// for smb / ftp, try and use ssl if possible.
CURL_EASY_SETOPT_LOG(curl, CURLOPT_USE_SSL, (long)CURLUSESSL_TRY);
// in most cases, this will use CURLAUTH_BASIC.
CURL_EASY_SETOPT_LOG(curl, CURLOPT_HTTPAUTH, (long)CURLAUTH_ANY);
// enable TE is server supports it.
CURL_EASY_SETOPT_LOG(curl, CURLOPT_TRANSFER_ENCODING, 1L);
// set flags.
if (e.GetFlags() & Flag_NoBody) {
CURL_EASY_SETOPT_LOG(curl, CURLOPT_NOBODY, 1L);
}
// set custom request.
if (!e.GetCustomRequest().empty()) {
log_write("[CURL] setting custom request: %s\n", e.GetCustomRequest().c_str());
CURL_EASY_SETOPT_LOG(curl, CURLOPT_CUSTOMREQUEST, e.GetCustomRequest().c_str());
}
// set oath2 bearer.
if (!e.GetBearer().empty()) {
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XOAUTH2_BEARER, e.GetBearer().c_str());
}
// set ssh pub/priv key file.
if (!e.GetPubKey().empty()) {
CURL_EASY_SETOPT_LOG(curl, CURLOPT_SSH_PUBLIC_KEYFILE, e.GetPubKey().c_str());
}
if (!e.GetPrivKey().empty()) {
CURL_EASY_SETOPT_LOG(curl, CURLOPT_SSH_PRIVATE_KEYFILE, e.GetPrivKey().c_str());
}
// set auth.
if (!e.GetUserPass().m_user.empty()) {
CURL_EASY_SETOPT_LOG(curl, CURLOPT_USERPWD, e.GetUserPass().m_user.c_str());
}
if (!e.GetUserPass().m_pass.empty()) {
CURL_EASY_SETOPT_LOG(curl, CURLOPT_PASSWORD, e.GetUserPass().m_pass.c_str());
}
// set port, if valid.
if (e.GetPort()) {
CURL_EASY_SETOPT_LOG(curl, CURLOPT_PORT, (long)e.GetPort());
}
// progress calls.
if (e.GetOnProgress()) {
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFODATA, &e);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFOFUNCTION, ProgressCallbackFunc2);
} else {
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFOFUNCTION, ProgressCallbackFunc1);
}
}
auto DownloadInternal(CURL* curl, const Api& e) -> ApiResult {
// check if stop has been requested before starting download
if (e.GetToken().stop_requested()) {
@@ -451,6 +677,7 @@ auto DownloadInternal(CURL* curl, const Api& e) -> ApiResult {
fs::FsPath tmp_buf;
const bool has_file = !e.GetPath().empty() && e.GetPath() != "";
const bool has_post = !e.GetFields().empty() && e.GetFields() != "";
const auto encoded_url = EncodeUrl(e.GetUrl());
DataStruct chunk;
Header header_in = e.GetHeader();
@@ -471,7 +698,8 @@ auto DownloadInternal(CURL* curl, const Api& e) -> ApiResult {
return {};
}
if (e.GetFlags() & Flag_Cache) {
// only add etag if the dst file still exists.
if ((e.GetFlags() & Flag_Cache) && fs::FileExists(&fs.m_fs, e.GetPath())) {
g_cache.get(e.GetPath(), header_in);
}
}
@@ -480,14 +708,9 @@ auto DownloadInternal(CURL* curl, const Api& e) -> ApiResult {
chunk.data.reserve(CHUNK_SIZE);
curl_easy_reset(curl);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_URL, e.GetUrl().c_str());
CURL_EASY_SETOPT_LOG(curl, CURLOPT_USERAGENT, "TotalJustice");
CURL_EASY_SETOPT_LOG(curl, CURLOPT_FOLLOWLOCATION, 1L);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_SSL_VERIFYPEER, 0L);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_SSL_VERIFYHOST, 0L);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_FAILONERROR, 1L);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_SHARE, g_curl_share);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_BUFFERSIZE, 1024*512);
SetCommonCurlOptions(curl, e);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_URL, encoded_url.c_str());
CURL_EASY_SETOPT_LOG(curl, CURLOPT_HEADERFUNCTION, header_callback);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_HEADERDATA, &header_out);
@@ -521,15 +744,6 @@ auto DownloadInternal(CURL* curl, const Api& e) -> ApiResult {
CURL_EASY_SETOPT_LOG(curl, CURLOPT_HTTPHEADER, list);
}
// progress calls.
if (e.GetOnProgress()) {
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFODATA, &e);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFOFUNCTION, ProgressCallbackFunc2);
} else {
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFOFUNCTION, ProgressCallbackFunc1);
}
CURL_EASY_SETOPT_LOG(curl, CURLOPT_NOPROGRESS, 0L);
// write calls.
CURL_EASY_SETOPT_LOG(curl, CURLOPT_WRITEFUNCTION, has_file ? WriteFileCallback : WriteMemoryCallback);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_WRITEDATA, &chunk);
@@ -558,6 +772,15 @@ auto DownloadInternal(CURL* curl, const Api& e) -> ApiResult {
g_cache.set(e.GetPath(), header_out);
}
// enable to log received headers.
#if 0
log_write("\n\nLOGGING HEADER\n");
for (auto [a, b] : header_out.m_map) {
log_write("\t%s: %s\n", a.c_str(), b.c_str());
}
log_write("\n\n");
#endif
fs.DeleteFile(e.GetPath());
fs.CreateDirectoryRecursivelyWithPath(e.GetPath());
if (R_FAILED(fs.RenameFile(tmp_buf, e.GetPath()))) {
@@ -573,18 +796,210 @@ auto DownloadInternal(CURL* curl, const Api& e) -> ApiResult {
}
}
log_write("Downloaded %s %s\n", e.GetUrl().c_str(), curl_easy_strerror(res));
log_write("Downloaded %s code: %ld %s\n", e.GetUrl().c_str(), http_code, curl_easy_strerror(res));
return {success, http_code, header_out, chunk.data, e.GetPath()};
}
auto DownloadInternal(const Api& e) -> ApiResult {
auto curl = curl_easy_init();
if (!curl) {
log_write("curl init failed\n");
auto UploadInternal(CURL* curl, const Api& e) -> ApiResult {
// check if stop has been requested before starting download
if (e.GetToken().stop_requested()) {
return {};
}
ON_SCOPE_EXIT(curl_easy_cleanup(curl));
return DownloadInternal(curl, e);
if (e.GetUrl().starts_with("webdav://")) {
if (!WebdavCreateFolder(curl, e)) {
log_write("[CURL] failed to create webdav folder, aborting\n");
return {};
}
}
const auto& info = e.GetUploadInfo();
const auto url = e.GetUrl() + "/" + info.m_name;
const auto encoded_url = EncodeUrl(url);
const bool has_file = !e.GetPath().empty() && e.GetPath() != "";
UploadStruct chunk{};
DataStruct chunk_out{};
SeekCustomData seek_data{};
Header header_in = e.GetHeader();
Header header_out;
fs::FsNativeSd fs{};
if (has_file) {
if (R_FAILED(fs.OpenFile(e.GetPath(), FsOpenMode_Read, &chunk.f))) {
log_write("failed to open file: %s\n", e.GetPath().s);
return {};
}
fsFileGetSize(&chunk.f, &chunk.size);
log_write("got chunk size: %zd\n", chunk.size);
} else {
if (info.m_callback) {
chunk.size = info.m_size;
log_write("setting upload size: %zu\n", chunk.size);
} else {
chunk.size = info.m_data.size();
chunk.data = info.m_data;
}
}
if (url.starts_with("file://")) {
const auto folder_path = fs::AppendPath("/", url.substr(std::strlen("file://")));
log_write("creating local folder: %s\n", folder_path.s);
// create the folder as libcurl doesn't seem to manually create it.
fs.CreateDirectoryRecursivelyWithPath(folder_path);
// remove the path so that libcurl can upload over it.
fs.DeleteFile(folder_path);
}
// reserve the first chunk
chunk_out.data.reserve(CHUNK_SIZE);
curl_easy_reset(curl);
SetCommonCurlOptions(curl, e);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_URL, encoded_url.c_str());
CURL_EASY_SETOPT_LOG(curl, CURLOPT_HEADERFUNCTION, header_callback);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_HEADERDATA, &header_out);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_UPLOAD, 1L);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_INFILESIZE_LARGE, (curl_off_t)chunk.size);
// instruct libcurl to create ftp folders if they don't yet exist.
CURL_EASY_SETOPT_LOG(curl, CURLOPT_FTP_CREATE_MISSING_DIRS, CURLFTP_CREATE_DIR_RETRY);
struct curl_slist* list = NULL;
ON_SCOPE_EXIT(if (list) { curl_slist_free_all(list); } );
for (const auto& [key, value] : header_in.m_map) {
if (value.empty()) {
continue;
}
// create header key value pair.
const auto header_str = key + ": " + value;
// try to append header chunk.
auto temp = curl_slist_append(list, header_str.c_str());
if (temp) {
log_write("adding header: %s\n", header_str.c_str());
list = temp;
} else {
log_write("failed to append header\n");
}
}
if (list) {
CURL_EASY_SETOPT_LOG(curl, CURLOPT_HTTPHEADER, list);
}
// set callback for reading more data.
if (info.m_callback) {
CURL_EASY_SETOPT_LOG(curl, CURLOPT_READFUNCTION, ReadCustomCallback);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_READDATA, &info);
if (e.GetOnUploadSeek()) {
seek_data.cb = e.GetOnUploadSeek();
seek_data.size = chunk.size;
CURL_EASY_SETOPT_LOG(curl, CURLOPT_SEEKFUNCTION, SeekCustomCallback);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_SEEKDATA, &seek_data);
}
} else {
CURL_EASY_SETOPT_LOG(curl, CURLOPT_READFUNCTION, has_file ? ReadFileCallback : ReadMemoryCallback);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_READDATA, &chunk);
// allow for seeking upon uploads, may be used for ftp and http.
CURL_EASY_SETOPT_LOG(curl, CURLOPT_SEEKFUNCTION, SeekCallback);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_SEEKDATA, &chunk);
}
// write calls.
CURL_EASY_SETOPT_LOG(curl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_WRITEDATA, &chunk_out);
// perform upload and cleanup after and report the result.
const auto res = curl_easy_perform(curl);
bool success = res == CURLE_OK;
long http_code = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
if (has_file) {
fsFileClose(&chunk.f);
}
log_write("Uploaded %s code: %ld %s\n", url.c_str(), http_code, curl_easy_strerror(res));
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) {
@@ -608,10 +1023,12 @@ void ThreadEntry::ThreadFunc(void* p) {
continue;
}
const auto result = DownloadInternal(data->m_curl, data->m_api);
const auto result = data->m_api.IsUpload() ? UploadInternal(data->m_curl, data->m_api) : DownloadInternal(data->m_curl, data->m_api);
if (g_running && data->m_api.GetOnComplete() && !data->m_api.GetToken().stop_requested()) {
const DownloadEventData event_data{data->m_api.GetOnComplete(), result, data->m_api.GetToken()};
evman::push(std::move(event_data), false);
evman::push(
DownloadEventData{data->m_api.GetOnComplete(), result, data->m_api.GetToken()},
false
);
}
data->m_in_progress = false;
@@ -708,6 +1125,11 @@ auto Init() -> bool {
}
}
g_curl_single = curl_easy_init();
if (!g_curl_single) {
log_write("failed to create g_curl_single\n");
}
log_write("finished creating threads\n");
if (!g_cache.init()) {
@@ -722,6 +1144,11 @@ void Exit() {
g_thread_queue.Close();
if (g_curl_single) {
curl_easy_cleanup(g_curl_single);
g_curl_single = nullptr;
}
for (auto& entry : g_threads) {
entry.Close();
}
@@ -739,14 +1166,28 @@ auto ToMemory(const Api& e) -> ApiResult {
if (!e.GetPath().empty()) {
return {};
}
return DownloadInternal(e);
return DownloadInternal(g_curl_single, e);
}
auto ToFile(const Api& e) -> ApiResult {
if (e.GetPath().empty()) {
return {};
}
return DownloadInternal(e);
return DownloadInternal(g_curl_single, e);
}
auto FromMemory(const Api& e) -> ApiResult {
if (!e.GetPath().empty()) {
return {};
}
return UploadInternal(g_curl_single, e);
}
auto FromFile(const Api& e) -> ApiResult {
if (e.GetPath().empty()) {
return {};
}
return UploadInternal(g_curl_single, e);
}
auto ToMemoryAsync(const Api& api) -> bool {
@@ -757,4 +1198,16 @@ auto ToFileAsync(const Api& e) -> bool {
return g_thread_queue.Add(e);
}
auto FromMemoryAsync(const Api& api) -> bool {
return g_thread_queue.Add(api, true);
}
auto FromFileAsync(const Api& e) -> bool {
return g_thread_queue.Add(e, true);
}
auto EscapeString(const std::string& str) -> std::string {
return EscapeString(nullptr, str);
}
} // namespace sphaira::curl

View File

@@ -12,14 +12,30 @@
#include <nx/vfs_nx.h>
#include <nx/utils.h>
namespace sphaira::ftpsrv {
namespace {
struct InstallSharedData {
std::mutex mutex;
std::deque<std::string> queued_files;
void* user;
OnInstallStart on_start;
OnInstallWrite on_write;
OnInstallClose on_close;
bool in_progress;
bool enabled;
};
const char* INI_PATH = "/config/ftpsrv/config.ini";
FtpSrvConfig g_ftpsrv_config = {0};
volatile bool g_should_exit = false;
bool g_is_running{false};
Thread g_thread;
std::mutex g_mutex{};
InstallSharedData g_shared_data{};
void ftp_log_callback(enum FTP_API_LOG_TYPE type, const char* msg) {
sphaira::App::NotifyFlashLed();
@@ -29,6 +45,235 @@ void ftp_progress_callback(void) {
sphaira::App::NotifyFlashLed();
}
const char* SUPPORTED_EXT[] = {
".nsp", ".xci", ".nsz", ".xcz",
};
struct VfsUserData {
char* path;
int valid;
};
// ive given up with good names.
void on_thing() {
log_write("[FTP] doing on_thing\n");
std::scoped_lock lock{g_shared_data.mutex};
log_write("[FTP] locked on_thing\n");
if (!g_shared_data.in_progress) {
if (!g_shared_data.queued_files.empty()) {
log_write("[FTP] pushing new file data\n");
if (!g_shared_data.on_start || !g_shared_data.on_start(g_shared_data.user, g_shared_data.queued_files[0].c_str())) {
g_shared_data.queued_files.clear();
} else {
log_write("[FTP] success on new file push\n");
g_shared_data.in_progress = true;
}
}
}
}
int vfs_install_open(void* user, const char* path, enum FtpVfsOpenMode mode) {
{
std::scoped_lock lock{g_shared_data.mutex};
auto data = static_cast<VfsUserData*>(user);
data->valid = 0;
if (mode != FtpVfsOpenMode_WRITE) {
errno = EACCES;
return -1;
}
if (!g_shared_data.enabled) {
errno = EACCES;
return -1;
}
const char* ext = strrchr(path, '.');
if (!ext) {
errno = EACCES;
return -1;
}
bool found = false;
for (size_t i = 0; i < std::size(SUPPORTED_EXT); i++) {
if (!strcasecmp(ext, SUPPORTED_EXT[i])) {
found = true;
break;
}
}
if (!found) {
errno = EINVAL;
return -1;
}
// check if we already have this file queued.
auto it = std::find(g_shared_data.queued_files.cbegin(), g_shared_data.queued_files.cend(), path);
if (it != g_shared_data.queued_files.cend()) {
errno = EEXIST;
return -1;
}
g_shared_data.queued_files.push_back(path);
data->path = strdup(path);
data->valid = true;
}
on_thing();
log_write("[FTP] got file: %s\n", path);
return 0;
}
int vfs_install_read(void* user, void* buf, size_t size) {
errno = EACCES;
return -1;
}
int vfs_install_write(void* user, const void* buf, size_t size) {
std::scoped_lock lock{g_shared_data.mutex};
if (!g_shared_data.enabled) {
errno = EACCES;
return -1;
}
auto data = static_cast<VfsUserData*>(user);
if (!data->valid) {
errno = EACCES;
return -1;
}
if (!g_shared_data.on_write || !g_shared_data.on_write(g_shared_data.user, buf, size)) {
errno = EIO;
return -1;
}
return size;
}
int vfs_install_seek(void* user, const void* buf, size_t size, size_t off) {
errno = ESPIPE;
return -1;
}
int vfs_install_isfile_open(void* user) {
std::scoped_lock lock{g_shared_data.mutex};
auto data = static_cast<VfsUserData*>(user);
return data->valid;
}
int vfs_install_isfile_ready(void* user) {
std::scoped_lock lock{g_shared_data.mutex};
auto data = static_cast<VfsUserData*>(user);
const auto ready = !g_shared_data.queued_files.empty() && data->path == g_shared_data.queued_files[0];
return ready;
}
int vfs_install_close(void* user) {
{
log_write("[FTP] closing file\n");
std::scoped_lock lock{g_shared_data.mutex};
auto data = static_cast<VfsUserData*>(user);
if (data->valid) {
log_write("[FTP] closing valid file\n");
auto it = std::find(g_shared_data.queued_files.cbegin(), g_shared_data.queued_files.cend(), data->path);
if (it != g_shared_data.queued_files.cend()) {
if (it == g_shared_data.queued_files.cbegin()) {
log_write("[FTP] closing current file\n");
if (g_shared_data.on_close) {
g_shared_data.on_close(g_shared_data.user);
}
g_shared_data.in_progress = false;
} else {
log_write("[FTP] closing other file...\n");
}
g_shared_data.queued_files.erase(it);
} else {
log_write("[FTP] could not find file in queue...\n");
}
if (data->path) {
free(data->path);
}
data->valid = 0;
}
memset(data, 0, sizeof(*data));
}
on_thing();
return 0;
}
int vfs_install_opendir(void* user, const char* path) {
return 0;
}
const char* vfs_install_readdir(void* user, void* user_entry) {
return NULL;
}
int vfs_install_dirlstat(void* user, const void* user_entry, const char* path, struct stat* st) {
st->st_nlink = 1;
st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
return 0;
}
int vfs_install_isdir_open(void* user) {
return 1;
}
int vfs_install_closedir(void* user) {
return 0;
}
int vfs_install_stat(const char* path, struct stat* st) {
st->st_nlink = 1;
st->st_mode = S_IFDIR | S_IWUSR | S_IWGRP | S_IWOTH;
return 0;
}
int vfs_install_mkdir(const char* path) {
return -1;
}
int vfs_install_unlink(const char* path) {
return -1;
}
int vfs_install_rmdir(const char* path) {
return -1;
}
int vfs_install_rename(const char* src, const char* dst) {
return -1;
}
FtpVfs g_vfs_install = {
.open = vfs_install_open,
.read = vfs_install_read,
.write = vfs_install_write,
.seek = vfs_install_seek,
.close = vfs_install_close,
.isfile_open = vfs_install_isfile_open,
.isfile_ready = vfs_install_isfile_ready,
.opendir = vfs_install_opendir,
.readdir = vfs_install_readdir,
.dirlstat = vfs_install_dirlstat,
.closedir = vfs_install_closedir,
.isdir_open = vfs_install_isdir_open,
.stat = vfs_install_stat,
.lstat = vfs_install_stat,
.mkdir = vfs_install_mkdir,
.unlink = vfs_install_unlink,
.rmdir = vfs_install_rmdir,
.rename = vfs_install_rename,
};
void loop(void* arg) {
while (!g_should_exit) {
ftpsrv_init(&g_ftpsrv_config);
@@ -44,8 +289,6 @@ void loop(void* arg) {
} // namespace
namespace sphaira::ftpsrv {
bool Init() {
std::scoped_lock lock{g_mutex};
if (g_is_running) {
@@ -84,6 +327,9 @@ bool Init() {
mount_bis = ini_getbool("Nx-App", "mount_bis", mount_bis, INI_PATH);
save_writable = ini_getbool("Nx-App", "save_writable", save_writable, INI_PATH);
mount_devices = true;
g_ftpsrv_config.timeout = 0;
if (!g_ftpsrv_config.port) {
return false;
}
@@ -93,7 +339,13 @@ bool Init() {
g_ftpsrv_config.anon = true;
}
vfs_nx_init(mount_devices, save_writable, mount_bis);
const VfsNxCustomPath custom = {
.name = "install",
.user = NULL,
.func = &g_vfs_install,
};
vfs_nx_init(&custom, mount_devices, save_writable, mount_bis);
Result rc;
if (R_FAILED(rc = threadCreate(&g_thread, loop, nullptr, nullptr, 1024*16, 0x2C, 2))) {
@@ -123,6 +375,40 @@ void Exit() {
fsdev_wrapUnmountAll();
}
void InitInstallMode(void* user, OnInstallStart on_start, OnInstallWrite on_write, OnInstallClose on_close) {
std::scoped_lock lock{g_shared_data.mutex};
g_shared_data.user = user;
g_shared_data.on_start = on_start;
g_shared_data.on_write = on_write;
g_shared_data.on_close = on_close;
g_shared_data.enabled = true;
}
void DisableInstallMode() {
std::scoped_lock lock{g_shared_data.mutex};
g_shared_data.enabled = false;
}
unsigned GetPort() {
std::scoped_lock lock{g_mutex};
return g_ftpsrv_config.port;
}
bool IsAnon() {
std::scoped_lock lock{g_mutex};
return g_ftpsrv_config.anon;
}
const char* GetUser() {
std::scoped_lock lock{g_mutex};
return g_ftpsrv_config.user;
}
const char* GetPass() {
std::scoped_lock lock{g_mutex};
return g_ftpsrv_config.pass;
}
} // namespace sphaira::ftpsrv
extern "C" {
@@ -132,9 +418,9 @@ void log_file_write(const char* msg) {
}
void log_file_fwrite(const char* fmt, ...) {
std::va_list v{};
va_list v{};
va_start(v, fmt);
log_write_arg(fmt, v);
log_write_arg(fmt, &v);
va_end(v);
}

View File

@@ -13,8 +13,8 @@ yyjson_doc* json;
yyjson_val* root;
std::unordered_map<std::string, std::string> g_tr_cache;
std::string get_internal(const char* str, size_t len) {
const std::string kkey = {str, len};
std::string get_internal(std::string_view str) {
const std::string kkey = {str.data(), str.length()};
if (auto it = g_tr_cache.find(kkey); it != g_tr_cache.end()) {
return it->second;
@@ -28,7 +28,7 @@ std::string get_internal(const char* str, size_t len) {
return kkey;
}
auto key = yyjson_obj_getn(root, str, len);
auto key = yyjson_obj_getn(root, str.data(), str.length());
if (!key) {
log_write("\tfailed to find key: [%s]\n", kkey.c_str());
return kkey;
@@ -78,6 +78,7 @@ bool init(long index) {
case 11: setLanguage = SetLanguage_RU; break; // "Russian"
case 12: lang_name = "se"; break; // "Swedish"
case 13: lang_name = "vi"; break; // "Vietnamese"
case 14: lang_name = "uk"; break; // "Ukrainian"
}
switch (setLanguage) {
@@ -86,7 +87,7 @@ bool init(long index) {
case SetLanguage_DE: lang_name = "de"; break;
case SetLanguage_IT: lang_name = "it"; break;
case SetLanguage_ES: lang_name = "es"; break;
case SetLanguage_ZHCN: lang_name = "zh"; break;
case SetLanguage_ZHCN: lang_name = "zh"; break;
case SetLanguage_KO: lang_name = "ko"; break;
case SetLanguage_NL: lang_name = "nl"; break;
case SetLanguage_PT: lang_name = "pt"; break;
@@ -134,16 +135,16 @@ void exit() {
g_i18n_data.clear();
}
std::string get(const char* str) {
return get_internal(str, std::strlen(str));
std::string get(std::string_view str) {
return get_internal(str);
}
} // namespace sphaira::i18n
namespace literals {
std::string operator"" _i18n(const char* str, size_t len) {
return sphaira::i18n::get_internal(str, len);
std::string operator""_i18n(const char* str, size_t len) {
return sphaira::i18n::get_internal({str, len});
}
} // namespace literals

View File

@@ -0,0 +1,75 @@
#include "location.hpp"
#include "fs.hpp"
#include <cstring>
#include <minIni.h>
namespace sphaira::location {
namespace {
constexpr fs::FsPath location_path{"/config/sphaira/locations.ini"};
} // namespace
void Add(const Entry& e) {
if (e.name.empty() || e.url.empty()) {
return;
}
ini_puts(e.name.c_str(), "url", e.url.c_str(), location_path);
if (!e.user.empty()) {
ini_puts(e.name.c_str(), "user", e.user.c_str(), location_path);
}
if (!e.pass.empty()) {
ini_puts(e.name.c_str(), "pass", e.pass.c_str(), location_path);
}
if (!e.bearer.empty()) {
ini_puts(e.name.c_str(), "bearer", e.bearer.c_str(), location_path);
}
if (!e.pub_key.empty()) {
ini_puts(e.name.c_str(), "pub_key", e.pub_key.c_str(), location_path);
}
if (!e.priv_key.empty()) {
ini_puts(e.name.c_str(), "priv_key", e.priv_key.c_str(), location_path);
}
if (e.port) {
ini_putl(e.name.c_str(), "port", e.port, location_path);
}
}
auto Load() -> Entries {
Entries out{};
auto cb = [](const mTCHAR *Section, const mTCHAR *Key, const mTCHAR *Value, void *UserData) -> int {
auto e = static_cast<Entries*>(UserData);
// add new entry if use section changed.
if (e->empty() || std::strcmp(Section, e->back().name.c_str())) {
e->emplace_back(Section);
}
if (!std::strcmp(Key, "url")) {
e->back().url = Value;
} else if (!std::strcmp(Key, "user")) {
e->back().user = Value;
} else if (!std::strcmp(Key, "pass")) {
e->back().pass = Value;
} else if (!std::strcmp(Key, "bearer")) {
e->back().bearer = Value;
} else if (!std::strcmp(Key, "pub_key")) {
e->back().pub_key = Value;
} else if (!std::strcmp(Key, "priv_key")) {
e->back().priv_key = Value;
} else if (!std::strcmp(Key, "port")) {
e->back().port = std::atoi(Value);
}
return 1;
};
ini_browse(cb, &out, location_path);
return out;
}
} // namespace sphaira::location

View File

@@ -14,18 +14,20 @@ std::FILE* file{};
int nxlink_socket{};
std::mutex mutex{};
void log_write_arg_internal(const char* s, std::va_list& v) {
void log_write_arg_internal(const char* s, std::va_list* v) {
if (file) {
std::vfprintf(file, s, v);
std::vfprintf(file, s, *v);
std::fflush(file);
}
if (nxlink_socket) {
std::vprintf(s, v);
std::vprintf(s, *v);
}
}
} // namespace
extern "C" {
auto log_file_init() -> bool {
std::scoped_lock lock{mutex};
if (file) {
@@ -70,11 +72,11 @@ void log_write(const char* s, ...) {
std::va_list v{};
va_start(v, s);
log_write_arg_internal(s, v);
log_write_arg_internal(s, &v);
va_end(v);
}
void log_write_arg(const char* s, std::va_list& v) {
void log_write_arg(const char* s, va_list* v) {
std::scoped_lock lock{mutex};
if (!file && !nxlink_socket) {
return;
@@ -83,4 +85,6 @@ void log_write_arg(const char* s, std::va_list& v) {
log_write_arg_internal(s, v);
}
} // extern "C"
#endif

View File

@@ -66,6 +66,9 @@ void userAppInit(void) {
if (R_FAILED(rc = ncmInitialize()))
diagAbortWithResult(rc);
// it doesn't matter if this fails.
appletSetScreenShotPermission(AppletScreenShotPermission_Enable);
log_nxlink_init();
}

View File

@@ -57,6 +57,7 @@ auto nro_parse_internal(fs::FsNative& fs, const fs::FsPath& path, NroEntry& entr
// some .nro (vgedit) have bad nacp, fake the nacp
if (asset.magic != NROASSETHEADER_MAGIC || asset.nacp.offset == 0 || asset.nacp.size != sizeof(entry.nacp)) {
std::memset(&asset, 0, sizeof(asset));
std::memset(&entry.nacp, 0, sizeof(entry.nacp));
// get the name without the .nro
@@ -157,6 +158,11 @@ auto nro_scan_internal(const fs::FsPath& path, std::vector<NroEntry>& nros, bool
}
auto nro_get_icon_internal(FsFile* f, u64 size, u64 offset) -> std::vector<u8> {
// protect again really messed up sizes.
if (size > 1024 * 1024) {
return {};
}
std::vector<u8> icon;
u64 bytes_read{};
icon.resize(size);

View File

@@ -6,21 +6,35 @@
namespace sphaira::option {
template<typename T>
auto OptionBase<T>::Get() -> T {
auto OptionBase<T>::GetInternal(const char* name) -> T {
if (!m_value.has_value()) {
if constexpr(std::is_same_v<T, bool>) {
m_value = ini_getbool(m_section.c_str(), m_name.c_str(), m_default_value, App::CONFIG_PATH);
m_value = ini_getbool(m_section.c_str(), name, m_default_value, App::CONFIG_PATH);
} else if constexpr(std::is_same_v<T, long>) {
m_value = ini_getl(m_section.c_str(), m_name.c_str(), m_default_value, App::CONFIG_PATH);
m_value = ini_getl(m_section.c_str(), name, m_default_value, App::CONFIG_PATH);
} else if constexpr(std::is_same_v<T, std::string>) {
char buf[FS_MAX_PATH];
ini_gets(m_section.c_str(), m_name.c_str(), 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;
}
}
return m_value.value();
}
template<typename T>
auto OptionBase<T>::Get() -> T {
return GetInternal(m_name.c_str());
}
template<typename T>
auto OptionBase<T>::GetOr(const char* name) -> T {
if (ini_haskey(m_section.c_str(), m_name.c_str(), App::CONFIG_PATH)) {
return Get();
} else {
return GetInternal(name);
}
}
template<typename T>
void OptionBase<T>::Set(T value) {
m_value = value;

View File

@@ -7,6 +7,14 @@
#include <string_view>
#include <span>
#include "yati/nx/nca.hpp"
#include "yati/nx/ncm.hpp"
#include "yati/nx/npdm.hpp"
#include "yati/nx/ns.hpp"
#include "yati/nx/es.hpp"
#include "yati/nx/keys.hpp"
#include "yati/nx/crypto.hpp"
#include "owo.hpp"
#include "defines.hpp"
#include "app.hpp"
@@ -23,52 +31,9 @@ constexpr u32 PFS0_EXEFS_HASH_BLOCK_SIZE = 0x10000;
constexpr u32 PFS0_LOGO_HASH_BLOCK_SIZE = 0x1000;
constexpr u32 PFS0_META_HASH_BLOCK_SIZE = 0x1000;
constexpr u32 PFS0_PADDING_SIZE = 0x200;
constexpr u32 NCA_SECTION_TOTAL = 0x4;
constexpr u32 ROMFS_ENTRY_EMPTY = 0xFFFFFFFF;
constexpr u32 ROMFS_FILEPARTITION_OFS = 0x200;
enum NcaDistributionType {
NcaDistributionType_System = 0x0,
NcaDistributionType_GameCard = 0x1
};
enum NcaContentType {
NcaContentType_Program = 0x0,
NcaContentType_Meta = 0x1,
NcaContentType_Control = 0x2,
NcaContentType_Manual = 0x3,
NcaContentType_Data = 0x4,
NcaContentType_PublicData = 0x5,
};
enum NcaFileSystemType {
NcaFileSystemType_RomFS = 0x0,
NcaFileSystemType_PFS0 = 0x1
};
enum NcaHashType {
NcaHashType_Auto = 0x0,
NcaHashType_HierarchicalSha256 = 0x2,
NcaHashType_HierarchicalIntegrity = 0x3
};
enum NcaEncryptionType {
NcaEncryptionType_Auto = 0x0,
NcaEncryptionType_None = 0x1,
NcaEncryptionType_AesCtrOld = 0x2,
NcaEncryptionType_AesCtr = 0x3,
NcaEncryptionType_AesCtrEx = 0x4
};
enum NsApplicationRecordType {
// installed
NsApplicationRecordType_Installed = 0x3,
// application is gamecard, but gamecard isn't insterted
NsApplicationRecordType_GamecardMissing = 0x5,
// archived
NsApplicationRecordType_Archived = 0xB,
};
// stdio-like wrapper for std::vector
struct BufHelper {
BufHelper() = default;
@@ -124,12 +89,6 @@ struct CnmtHeader {
};
static_assert(sizeof(CnmtHeader) == 0x20);
struct NcmContentStorageRecord {
NcmContentMetaKey key;
u8 storage_id; //
u8 padding[0x7];
};
struct NcmContentMetaData {
NcmContentMetaHeader header;
NcmApplicationMetaExtendedHeader extended;
@@ -142,7 +101,7 @@ struct NcaMetaEntry {
NcaEntry nca_entry;
NcmContentMetaHeader content_meta_header{};
NcmContentMetaKey content_meta_key{};
NcmContentStorageRecord content_storage_record{};
ncm::ContentStorageRecord content_storage_record{};
NcmContentMetaData content_meta_data{};
};
@@ -171,61 +130,6 @@ struct FileEntry {
using FileEntries = std::vector<FileEntry>;
struct NpdmMeta {
u32 magic; // "META"
u32 signature_key_generation; // +9.0.0
u32 _0x8;
u8 flags;
u8 _0xD;
u8 main_thread_priority;
u8 main_thread_core_num;
u32 _0x10;
u32 sys_resource_size; // +3.0.0
u32 version;
u32 main_thread_stack_size;
char title_name[0x10];
char product_code[0x10];
u8 _0x40[0x30];
u32 aci0_offset;
u32 aci0_size;
u32 acid_offset;
u32 acid_size;
};
struct NpdmAcid {
u8 rsa_sig[0x100];
u8 rsa_pub[0x100];
u32 magic; // "ACID"
u32 size;
u8 version;
u8 _0x209[0x1];
u8 _0x20A[0x2];
u32 flags;
u64 program_id_min;
u64 program_id_max;
u32 fac_offset;
u32 fac_size;
u32 sac_offset;
u32 sac_size;
u32 kac_offset;
u32 kac_size;
u8 _0x238[0x8];
};
struct NpdmAci0 {
u32 magic; // "ACI0"
u8 _0x4[0xC];
u64 program_id;
u8 _0x18[0x8];
u32 fac_offset;
u32 fac_size;
u32 sac_offset;
u32 sac_size;
u32 kac_offset;
u32 kac_size;
u8 _0x38[0x8];
};
struct NpdmPatch {
char title_name[0x10]{"Application"};
char product_code[0x10]{};
@@ -238,56 +142,6 @@ struct NcapPatch {
u64 tid;
};
struct NcaSectionTableEntry {
u32 media_start_offset; // divided by 0x200.
u32 media_end_offset; // divided by 0x200.
u8 _0x8[0x4]; // unknown.
u8 _0xC[0x4]; // unknown.
};
struct LayerRegion {
u64 offset;
u64 size;
};
struct HierarchicalSha256Data {
u8 master_hash[0x20];
u32 block_size;
u32 layer_count;
LayerRegion hash_layer;
LayerRegion pfs0_layer;
LayerRegion unused_layers[3];
u8 _0x78[0x80];
};
#pragma pack(push, 1)
struct HierarchicalIntegrityVerificationLevelInformation {
u64 logical_offset;
u64 hash_data_size;
u32 block_size; // log2
u32 _0x14; // reserved
};
#pragma pack(pop)
struct InfoLevelHash {
u32 max_layers;
HierarchicalIntegrityVerificationLevelInformation levels[6];
u8 signature_salt[0x20];
};
struct IntegrityMetaInfo {
u32 magic; // IVFC
u32 version;
u32 master_hash_size;
InfoLevelHash info_level_hash;
u8 master_hash[0x20];
u8 _0xE0[0x18];
};
static_assert(sizeof(HierarchicalSha256Data) == 0xF8);
static_assert(sizeof(IntegrityMetaInfo) == 0xF8);
static_assert(sizeof(HierarchicalSha256Data) == sizeof(IntegrityMetaInfo));
typedef struct romfs_dirent_ctx {
u32 entry_offset;
struct romfs_dirent_ctx *parent; /* Parent node */
@@ -317,70 +171,6 @@ typedef struct {
u64 file_partition_size;
} romfs_ctx_t;
struct NcaFsHeader {
u16 version; // always 2.
u8 fs_type; // see NcaFileSystemType.
u8 hash_type; // see NcaHashType.
u8 encryption_type; // see NcaEncryptionType.
u8 metadata_hash_type;
u8 _0x6[0x2]; // empty.
union {
HierarchicalSha256Data hierarchical_sha256_data;
IntegrityMetaInfo integrity_meta_info; // used for romfs
} hash_data;
u8 patch_info[0x40];
u64 section_ctr;
u8 spares_info[0x30];
u8 compression_info[0x28];
u8 meta_data_hash_data_info[0x30];
u8 reserved[0x30];
};
struct NcaSectionHeaderHash {
u8 sha256[0x20];
};
struct NcaKeyArea {
u8 area[0x10];
};
struct NcaHeader {
u8 rsa_fixed_key[0x100];
u8 rsa_npdm[0x100]; // key from npdm.
u32 magic;
u8 distribution_type; // see NcaDistributionType.
u8 content_type; // see NcaContentType.
u8 old_key_gen; // see NcaOldKeyGeneration.
u8 kaek_index; // see NcaKeyAreaEncryptionKeyIndex.
u64 size;
u64 title_id;
u32 context_id;
u32 sdk_version;
u8 key_gen; // see NcaKeyGeneration.
u8 header_1_sig_key_gen;
u8 _0x222[0xE]; // empty.
FsRightsId rights_id;
NcaSectionTableEntry fs_table[NCA_SECTION_TOTAL];
NcaSectionHeaderHash fs_header_hash[NCA_SECTION_TOTAL];
NcaKeyArea key_area[NCA_SECTION_TOTAL];
u8 _0x340[0xC0]; // empty.
NcaFsHeader fs_header[NCA_SECTION_TOTAL];
};
constexpr u8 HEADER_KEK_SRC[0x10] = {
0x1F, 0x12, 0x91, 0x3A, 0x4A, 0xCB, 0xF0, 0x0D, 0x4C, 0xDE, 0x3A, 0xF6, 0xD5, 0x23, 0x88, 0x2A
};
constexpr u8 HEADER_KEY_SRC[0x20] = {
0x5A, 0x3E, 0xD8, 0x4F, 0xDE, 0xC0, 0xD8, 0x26, 0x31, 0xF7, 0xE2, 0x5D, 0x19, 0x7B, 0xF5, 0xD0,
0x1C, 0x9B, 0x7B, 0xFA, 0xF6, 0x28, 0x18, 0x3D, 0x71, 0xF6, 0x4D, 0x73, 0xF1, 0x50, 0xB9, 0xD2
};
auto write_padding(BufHelper& buf, u64 off, u64 block) -> u64 {
const u64 size = block - (off % block);
if (size) {
@@ -632,9 +422,9 @@ auto npdm_patch_kc(std::vector<u8>& npdm, u32 off, u32 size, u32 bitmask, u32 va
// todo: manually build npdm
void patch_npdm(std::vector<u8>& npdm, const NpdmPatch& patch) {
NpdmMeta meta{};
NpdmAci0 aci0{};
NpdmAcid acid{};
npdm::Meta meta{};
npdm::Aci0 aci0{};
npdm::Acid acid{};
std::memcpy(&meta, npdm.data(), sizeof(meta));
std::memcpy(&aci0, npdm.data() + meta.aci0_offset, sizeof(aci0));
std::memcpy(&acid, npdm.data() + meta.acid_offset, sizeof(acid));
@@ -655,7 +445,7 @@ void patch_npdm(std::vector<u8>& npdm, const NpdmPatch& patch) {
splGetConfig(SplConfigItem_ExosphereVersion, &ver);
ver >>= 40;
if (ver >= MAKEHOSVERSION(1,7,1)) {
if (ver >= MAKEHOSVERSION(1,8,0)) {
npdm_patch_kc(npdm, meta.aci0_offset + aci0.kac_offset, aci0.kac_size, 16, BIT(19));
npdm_patch_kc(npdm, meta.acid_offset + acid.kac_offset, acid.kac_size, 16, BIT(19));
}
@@ -805,7 +595,7 @@ void write_nca_padding(BufHelper& buf) {
write_padding(buf, buf.tell(), 0x200);
}
void nca_encrypt_header(NcaHeader* header, std::span<const u8> key) {
void nca_encrypt_header(nca::Header* header, std::span<const u8> key) {
Aes128XtsContext ctx{};
aes128XtsContextCreate(&ctx, key.data(), key.data() + 0x10, true);
@@ -816,41 +606,41 @@ void nca_encrypt_header(NcaHeader* header, std::span<const u8> key) {
}
}
void write_nca_section(NcaHeader& nca_header, u8 index, u64 start, u64 end) {
void write_nca_section(nca::Header& nca_header, u8 index, u64 start, u64 end) {
auto& section = nca_header.fs_table[index];
section.media_start_offset = start / 0x200; // 0xC00 / 0x200
section.media_end_offset = end / 0x200; // Section end offset / 200
section._0x8[0] = 0x1; // Always 1
}
void write_nca_fs_header_pfs0(NcaHeader& nca_header, u8 index, const std::vector<u8>& master_hash, u64 hash_table_size, u32 block_size) {
void write_nca_fs_header_pfs0(nca::Header& nca_header, u8 index, const std::vector<u8>& master_hash, u64 hash_table_size, u32 block_size) {
auto& fs_header = nca_header.fs_header[index];
fs_header.hash_type = NcaHashType_HierarchicalSha256;
fs_header.fs_type = NcaFileSystemType_PFS0;
fs_header.hash_type = nca::HashType_HierarchicalSha256;
fs_header.fs_type = nca::FileSystemType_PFS0;
fs_header.version = 0x2; // Always 2
fs_header.hash_data.hierarchical_sha256_data.layer_count = 0x2;
fs_header.hash_data.hierarchical_sha256_data.block_size = block_size;
fs_header.encryption_type = NcaEncryptionType_None;
fs_header.encryption_type = nca::EncryptionType_None;
fs_header.hash_data.hierarchical_sha256_data.hash_layer.size = hash_table_size;
std::memcpy(fs_header.hash_data.hierarchical_sha256_data.master_hash, master_hash.data(), master_hash.size());
sha256CalculateHash(&nca_header.fs_header_hash[index], &fs_header, sizeof(fs_header));
}
void write_nca_fs_header_romfs(NcaHeader& nca_header, u8 index) {
void write_nca_fs_header_romfs(nca::Header& nca_header, u8 index) {
auto& fs_header = nca_header.fs_header[index];
fs_header.hash_type = NcaHashType_HierarchicalIntegrity;
fs_header.fs_type = NcaFileSystemType_RomFS;
fs_header.hash_type = nca::HashType_HierarchicalIntegrity;
fs_header.fs_type = nca::FileSystemType_RomFS;
fs_header.version = 0x2; // Always 2
fs_header.hash_data.integrity_meta_info.magic = 0x43465649;
fs_header.hash_data.integrity_meta_info.version = 0x20000; // Always 0x20000
fs_header.hash_data.integrity_meta_info.master_hash_size = SHA256_HASH_SIZE;
fs_header.hash_data.integrity_meta_info.info_level_hash.max_layers = 0x7;
fs_header.encryption_type = NcaEncryptionType_None;
fs_header.encryption_type = nca::EncryptionType_None;
fs_header.hash_data.integrity_meta_info.info_level_hash.levels[5].block_size = 0x0E; // 0x4000
sha256CalculateHash(&nca_header.fs_header_hash[index], &fs_header, sizeof(fs_header));
}
void write_nca_pfs0(NcaHeader& nca_header, u8 index, const FileEntries& entries, u32 block_size, BufHelper& buf) {
void write_nca_pfs0(nca::Header& nca_header, u8 index, const FileEntries& entries, u32 block_size, BufHelper& buf) {
const auto pfs0 = build_pfs0(entries);
const auto pfs0_hash_table = build_pfs0_hash_table(pfs0, block_size);
const auto pfs0_master_hash = build_pfs0_master_hash(pfs0_hash_table);
@@ -887,7 +677,7 @@ auto ivfc_create_level(const std::vector<u8>& src) -> std::vector<u8> {
return buf.buf;
}
void write_nca_romfs(NcaHeader& nca_header, u8 index, const FileEntries& entries, u32 block_size, BufHelper& buf) {
void write_nca_romfs(nca::Header& nca_header, u8 index, const FileEntries& entries, u32 block_size, BufHelper& buf) {
auto& fs_header = nca_header.fs_header[index];
auto& meta_info = fs_header.hash_data.integrity_meta_info;
auto& info_level_hash = meta_info.info_level_hash;
@@ -921,22 +711,22 @@ void write_nca_romfs(NcaHeader& nca_header, u8 index, const FileEntries& entries
write_nca_fs_header_romfs(nca_header, index);
}
void write_nca_header_encypted(NcaHeader& nca_header, u64 tid, std::span<const u8> key, NcaContentType type, BufHelper& buf) {
nca_header.magic = 0x3341434E;
nca_header.distribution_type = NcaDistributionType_System;
void write_nca_header_encypted(nca::Header& nca_header, u64 tid, const keys::Keys& keys, nca::ContentType type, BufHelper& buf) {
nca_header.magic = NCA3_MAGIC;
nca_header.distribution_type = nca::DistributionType_System;
nca_header.content_type = type;
nca_header.title_id = tid;
nca_header.program_id = tid;
nca_header.sdk_version = 0x000C1100;
nca_header.size = buf.tell();
nca_encrypt_header(&nca_header, key);
nca_encrypt_header(&nca_header, keys.header_key);
buf.seek(0);
buf.write(&nca_header, sizeof(nca_header));
}
auto create_program_nca(u64 tid, std::span<const u8> key, const FileEntries& exefs, const FileEntries& romfs, const FileEntries& logo) -> NcaEntry {
auto create_program_nca(u64 tid, const keys::Keys& keys, const FileEntries& exefs, const FileEntries& romfs, const FileEntries& logo) -> NcaEntry {
BufHelper buf;
NcaHeader nca_header{};
nca::Header nca_header{};
buf.write(&nca_header, sizeof(nca_header));
write_nca_pfs0(nca_header, 0, exefs, PFS0_EXEFS_HASH_BLOCK_SIZE, buf);
@@ -945,23 +735,23 @@ auto create_program_nca(u64 tid, std::span<const u8> key, const FileEntries& exe
if (logo.size() == 2 && !logo[0].data.empty() && !logo[1].data.empty()) {
write_nca_pfs0(nca_header, 2, logo, PFS0_LOGO_HASH_BLOCK_SIZE, buf);
}
write_nca_header_encypted(nca_header, tid, key, NcaContentType_Program, buf);
write_nca_header_encypted(nca_header, tid, keys, nca::ContentType_Program, buf);
return {buf, NcmContentType_Program};
}
auto create_control_nca(u64 tid, std::span<const u8> key, const FileEntries& romfs) -> NcaEntry{
NcaHeader nca_header{};
auto create_control_nca(u64 tid, const keys::Keys& keys, const FileEntries& romfs) -> NcaEntry{
nca::Header nca_header{};
BufHelper buf;
buf.write(&nca_header, sizeof(nca_header));
write_nca_romfs(nca_header, 0, romfs, IVFC_HASH_BLOCK_SIZE, buf);
write_nca_header_encypted(nca_header, tid, key, NcaContentType_Control, buf);
write_nca_header_encypted(nca_header, tid, keys, nca::ContentType_Control, buf);
return {buf, NcmContentType_Control};
}
auto create_meta_nca(u64 tid, std::span<const u8> key, NcmStorageId storage_id, const std::vector<NcaEntry>& ncas) -> NcaMetaEntry {
auto create_meta_nca(u64 tid, const keys::Keys& keys, NcmStorageId storage_id, const std::vector<NcaEntry>& ncas) -> NcaMetaEntry {
CnmtHeader cnmt_header{};
NcmApplicationMetaExtendedHeader cnmt_extended{};
NcmPackagedContentInfo packaged_content_info[2]{};
@@ -997,10 +787,10 @@ auto create_meta_nca(u64 tid, std::span<const u8> key, NcmStorageId storage_id,
std::snprintf(cnmt_name, sizeof(cnmt_name), "Application_%016lX.cnmt", tid);
add_file_entry(cnmt, cnmt_name, cnmt_buf.buf.data(), cnmt_buf.buf.size());
NcaHeader nca_header{};
nca::Header nca_header{};
buf.write(&nca_header, sizeof(nca_header));
write_nca_pfs0(nca_header, 0, cnmt, PFS0_META_HASH_BLOCK_SIZE, buf);
write_nca_header_encypted(nca_header, tid, key, NcaContentType_Meta, buf);
write_nca_header_encypted(nca_header, tid, keys, nca::ContentType_Meta, buf);
// entry
NcaMetaEntry entry{buf, NcmContentType_Meta};
@@ -1040,26 +830,6 @@ auto create_meta_nca(u64 tid, std::span<const u8> key, NcmStorageId storage_id,
return entry;
}
Result nsDeleteApplicationRecord(Service* srv, u64 tid) {
return serviceDispatchIn(srv, 27, tid);
}
Result nsPushApplicationRecord(Service* srv, u64 tid, const NcmContentStorageRecord* records, u32 count) {
const struct {
u8 last_modified_event;
u8 padding[0x7];
u64 tid;
} in = { NsApplicationRecordType_Installed, {0}, tid };
return serviceDispatchIn(srv, 16, in,
.buffer_attrs = { SfBufferAttr_HipcMapAlias | SfBufferAttr_In },
.buffers = { { records, sizeof(NcmContentStorageRecord) * count } });
}
Result nsInvalidateApplicationControlCache(Service* srv, u64 tid) {
return serviceDispatchIn(srv, 404, tid);
}
auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStorageId storage_id) -> Result {
R_UNLESS(!config.nro_path.empty(), OwoError_BadArgs);
// R_UNLESS(!config.icon.empty(), OwoError_BadArgs);
@@ -1073,14 +843,8 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
R_TRY(nsInitialize());
ON_SCOPE_EXIT(nsExit());
// generate header kek
u8 header_kek[0x20];
R_TRY(splCryptoGenerateAesKek(HEADER_KEK_SRC, 0, 0, header_kek));
// gen header key 0
u8 key[0x20];
R_TRY(splCryptoGenerateAesKey(header_kek, HEADER_KEY_SRC, key));
// gen header key 1
R_TRY(splCryptoGenerateAesKey(header_kek, HEADER_KEY_SRC + 0x10, key + 0x10));
keys::Keys keys;
R_TRY(keys::parse_keys(keys, false));
// fix args to include nro path
if (config.args.empty()) {
@@ -1093,7 +857,8 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
u64 hash_data[SHA256_HASH_SIZE / sizeof(u64)];
const auto hash_path = config.nro_path + config.args;
sha256CalculateHash(hash_data, hash_path.data(), hash_path.length());
const u64 tid = 0x0100000000000000 | (hash_data[0] & 0x00FFFFFFFFFFF000);
const u64 old_tid = 0x0100000000000000 | (hash_data[0] & 0x00FFFFFFFFFFF000);
const u64 tid = 0x0500000000000000 | (hash_data[0] & 0x00FFFFFFFFFFF000);
std::vector<NcaEntry> nca_entries;
@@ -1124,7 +889,7 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
patch_npdm(exefs[1].data, npdm_patch);
nca_entries.emplace_back(
create_program_nca(tid, key, exefs, romfs, logo)
create_program_nca(tid, keys, exefs, romfs, logo)
);
} else {
nca_entries.emplace_back(
@@ -1147,18 +912,18 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
add_file_entry(romfs, "/icon_AmericanEnglish.dat", config.icon);
nca_entries.emplace_back(
create_control_nca(tid, key, romfs)
create_control_nca(tid, keys, romfs)
);
}
// create meta
NcmContentMetaHeader content_meta_header;
NcmContentMetaKey content_meta_key;
NcmContentStorageRecord content_storage_record;
ncm::ContentStorageRecord content_storage_record;
NcmContentMetaData content_meta_data;
{
pbox->NewTransfer("Creating Meta"_i18n).UpdateTransfer(2, 8);
const auto meta_entry = create_meta_nca(tid, key, storage_id, nca_entries);
const auto meta_entry = create_meta_nca(tid, keys, storage_id, nca_entries);
nca_entries.emplace_back(meta_entry.nca_entry);
content_meta_header = meta_entry.content_meta_header;
@@ -1216,17 +981,23 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
R_TRY(nsIsAnyApplicationEntityInstalled(tid, &already_installed));
}
// remove old id for forwarders.
const auto rc = nsDeleteApplicationCompletely(old_tid);
if (R_FAILED(rc) && rc != 0x410) { // not found
App::Notify("Failed to remove old forwarder, please manually remove it!");
}
// remove previous application record
if (already_installed || hosversionBefore(2,0,0)) {
const auto rc = nsDeleteApplicationRecord(srv_ptr, tid);
const auto rc = ns::DeleteApplicationRecord(srv_ptr, tid);
R_UNLESS(R_SUCCEEDED(rc) || hosversionBefore(2,0,0), rc);
}
R_TRY(nsPushApplicationRecord(srv_ptr, tid, &content_storage_record, 1));
R_TRY(ns::PushApplicationRecord(srv_ptr, tid, &content_storage_record, 1));
// force flush
if (already_installed || hosversionBefore(2,0,0)) {
const auto rc = nsInvalidateApplicationControlCache(srv_ptr, tid);
const auto rc = ns::InvalidateApplicationControlCache(srv_ptr, tid);
R_UNLESS(R_SUCCEEDED(rc) || hosversionBefore(2,0,0), rc);
}
}
@@ -1241,7 +1012,7 @@ auto install_forwarder(ui::ProgressBox* pbox, OwoConfig& config, NcmStorageId st
}
auto install_forwarder(OwoConfig& config, NcmStorageId storage_id) -> Result {
App::Push(std::make_shared<ui::ProgressBox>("Installing Forwarder"_i18n, [config, storage_id](auto pbox) mutable -> bool {
App::Push(std::make_shared<ui::ProgressBox>(0, "Installing Forwarder"_i18n, config.name, [config, storage_id](auto pbox) mutable -> bool {
return R_SUCCEEDED(install_forwarder(pbox, config, storage_id));
}));
R_SUCCEED();

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