36 Commits

Author SHA1 Message Date
ITotalJustice
8e02538405 fix nvjpg crashing when trying to load a non-jpg image, fix building with nvjpg disable, optimise invalid nro asset loading, bump version for new release 0.13.0 -> 0.13.1 2025-06-18 21:10:15 +01:00
ITotalJustice
928da0cbda bump version for new release 0.12.0 -> 0.13.0 2025-06-18 15:42:50 +01:00
ITotalJustice
267693c6ab Revert "disable mtp install"
This reverts commit 8e67e5f0fc.

While MTP install may not work for most people, i guess it's better to have it as an option still.
Who knows, someone may figure out why it randomly freezes on windows when installing heavily compressed nsz files.
2025-06-18 15:22:53 +01:00
ITotalJustice
3f99afaa38 add personalised -> common convert. patch bad common tickets. fix yati installing nca's if already installed.
- dumped nsp now have the tik/cert at the end of the file table, rather than the beginning.
- dumped nsp patches the ticket if needed (no personalised dumping yet).
- installing titles will now patch the ticket, performing personalised -> common convert if needed, as well as fixing bad common tickets.
- yati no longer tries to install ncas if they already exist.
- ticket only option now actually works.
- fixed some translations.
- removed unused error codes.
2025-06-18 15:07:07 +01:00
ITotalJustice
8e67e5f0fc disable mtp install
see #132
2025-06-17 22:16:54 +01:00
Ny'hrarr
cb1508e6d5 Update pt.json (#178)
* Remove duplicate keys

* Add translation for MTP install

* Resolve conflicts with latest upstream commit

* Update pt.json
2025-06-17 17:32:53 +01:00
ITotalJustice
070be1ff94 fix filebrowser touch (related to f2462cff81).
the fix in commit f2462cff81 broke split screen.
this commit properly fixes the touch and split screen buttons.
2025-06-17 11:46:34 +01:00
ITotalJustice
7730eacea8 fix mtp install (again) due to the next transfer trying to start before the previous one had finished. 2025-06-17 11:23:45 +01:00
ITotalJustice
c5e3373fe1 fix filebrowser mount name not being displayed as the current option if translations are enabled 2025-06-17 11:00:56 +01:00
ITotalJustice
d7ec620173 signal homebrew refresh when downloading an app via ghdl 2025-06-17 10:57:24 +01:00
ITotalJustice
1c72350d4a add mtp install, fix es ticket being the wrong size, fix yati not returning the read fail result, updated haze, updated translations
see #132
2025-06-17 10:48:07 +01:00
sandmaennchen5
4ef15f8b81 German File Update (#177) 2025-06-17 01:59:13 +01:00
Aistra
8fc7b614a0 update Chinese translation and fix typo (#172) 2025-06-17 01:58:39 +01:00
ITotalJustice
0789a69975 delete save file before restoring. always commit fs after every write + close, delete, rename, create. 2025-06-17 01:57:42 +01:00
ITotalJustice
b405a816c9 use actual save timestamp when creating a zip backup 2025-06-17 01:03:18 +01:00
ITotalJustice
99c1db3655 disable hdd->hdd threading. only open log write on write to allow for reads. log fw/ams version.
hdd->hdd threading is disabled due to a bug in libusbhsfs which causes a deadlock if 2 fs calls happen at the same time.
2025-06-17 00:57:46 +01:00
ITotalJustice
6b099de63c fix passing raw string to vsprintf.
fixes #173
2025-06-12 21:50:06 +01:00
ITotalJustice
275707fe27 add mtp custom mount support (image sd, image nand, install, speed test). 2025-06-12 14:47:33 +01:00
ITotalJustice
c535b96b12 bump oss-nvjpg version fdcaba8 -> 16c10a3 (fixes homebrew icon decoding). 2025-06-10 20:23:37 +01:00
ITotalJustice
6b77cbb0c0 enable boost mode as early as possible during init, and exit boost as late as possible during exit. 2025-06-09 12:32:43 +01:00
ITotalJustice
a33d8e1061 fix gc menu button::A not being enabled when a gamecard is not inserted. 2025-06-09 12:20:27 +01:00
ITotalJustice
aaf11211dc add basic error messages for internal sphaira errors. 2025-06-09 12:11:05 +01:00
ITotalJustice
83b2aca942 bump ftpsrv version from 8c18431 -> 8782f6b 2025-06-09 10:26:31 +01:00
impeeza
fbae286dff Spanish file Update (#170)
* Spanish file Update

Making Spanish file update to latest version changes.

Short some strings.
2025-06-08 22:08:46 +01:00
游家小少
ba9b6b54bf Update zh.json (#171)
* Update zh.json

Add relevant strings to the Chinese language file and translate it based on the latest “pt.json” language file.

* Update zh.json
2025-06-08 22:07:43 +01:00
Ny'hrarr
1677514355 Update pt.json (#169)
* Update pt.json

* Update BCAT fields
2025-06-07 20:37:18 +01:00
ITotalJustice
ec1042efa3 rename Bcat -> BCAT, rename bcat folder to BCAT, better impl for stripping leading '/' for zip_add. 2025-06-07 20:27:36 +01:00
ITotalJustice
b03ad4ade3 support for all save types (system, bcat, cache, device). 2025-06-07 17:55:04 +01:00
ITotalJustice
04f6e5d2a8 update homebrew menu when app is installed from the appstore, add same effect to a few other menus (unused). 2025-06-07 13:03:44 +01:00
ITotalJustice
16c074db1a strip leading '/' in fs::AppendPath(). 2025-06-07 11:54:56 +01:00
ITotalJustice
8d958a2d1d in save menu close account service as soon as account data is loaded. 2025-06-07 11:49:50 +01:00
ITotalJustice
74c1cd3be0 add support for backup/restore save to usb 2025-06-07 11:45:06 +01:00
shadow2560
0fd5f348e2 Update french translation and fix a small typing error. (#168)
Signed-off-by: shadow2560 <24191064+shadow2560@users.noreply.github.com>
2025-06-07 08:13:34 +01:00
redraz
0c9433d0d3 Update ru.json (#166) 2025-06-05 08:59:00 +01:00
ITotalJustice
8fb34d42dc fix very rare crash when closing sphaira from the appstore when saves/games menu has also been opened.
i've been trying to track down this bug for a while. i still don't understand why it happens, however i have managed to
reproduce it an narrow down the crash, and thus fix it.

the bug was caused calling nvgDeleteImage() inside ~LazyImage() on image 43. this would only trigger once games/saves menu
had been opened at least once also.

i can only assume that the image fd was still refrenced in deko3d when drawing, as it would only ever crash on the visible images.
destroying fb resources before the calls to nvg delete seems to fix the issue.

maybe the explicit call to waitIdle is what fixes it? or clearing the cmd buf? who knows...
2025-06-05 02:56:13 +01:00
Ny'hrarr
be831eb04a Update pt.json (#165)
* Update pt.json
2025-06-05 02:51:49 +01:00
83 changed files with 3804 additions and 1969 deletions

View File

@@ -98,6 +98,7 @@ The output will be found in `build/MinSizeRel/sphaira.nro`
- [hb-appstore](https://github.com/fortheusers/hb-appstore)
- [haze](https://github.com/Atmosphere-NX/Atmosphere/tree/master/troposphere/haze)
- [nxdumptool](https://github.com/DarkMatterCore/nxdumptool) (for gamecard bin dumping and rsa verify code)
- [Liam0](switch-010editor-templates) (for ticket / cert structs)
- [libusbhsfs](https://github.com/DarkMatterCore/libusbhsfs)
- [libnxtc](https://github.com/DarkMatterCore/libnxtc)
- [oss-nvjpg](https://github.com/averne/oss-nvjpg)

View File

@@ -1,24 +1,24 @@
{
"[Applet Mode]": " | Applet Modus |",
"[Applet Mode]": "[Applet Modus]",
"No Internet": "Kein Internet",
"Switch-Handheld!": "Handheld!",
"Switch-Docked!": "Angedockt!",
"Warning! Logs are enabled, Sphaira will run slowly!": "",
"Switch-Handheld!": "Handheld-Modus!",
"Switch-Docked!": "TV-Modus!",
"Warning! Logs are enabled, Sphaira will run slowly!": "Warnung! Protokollierung ist aktiviert, Sphaira läuft langsam!",
"Audio disabled due to suspended game": "Audio deaktivert wegen Spielabbruch",
"Are you sure you wish to cancel?": "Bist du sicher dass du abbrechen willst?",
"An error occurred": "",
"An error occurred": "Ein Fehler ist aufgetreten.",
"If this message appears repeatedly, please open an issue.": "Bei wiederholtem Auftreten bitte Issue erstellen.",
"Menu Options": " Menü | Optionen",
"Menu Options": "Menü | Optionen",
"Menu": "Menü",
"Theme": "Themes",
"Theme Options": " Themes | Optionen",
"Select Theme": "Theme wählen",
"Theme": "Designs",
"Theme Options": "Designs | Optionen",
"Select Theme": "Design wählen",
"Music": "Musik",
"12 Hour Time": "12-Std Zeitformat",
"Download Default Music": "",
"Failed to download default_music.bfstm, please try again": "",
"Overwrite current default music?": "",
"Download Default Music": "Standardmusik herunterladen",
"Failed to download default_music.bfstm, please try again": "Fehler beim Herunterladen von default_music.bfstm. Bitte versuchen Sie es erneut.",
"Overwrite current default music?": "Aktuelle Standardmusik überschreiben?",
"Network": "Konnektivität",
"Network Options": "Konnektivität | Optionen",
@@ -28,8 +28,8 @@
"Nxlink Connected": "NXLink | Verbunden",
"Nxlink Upload": "NXLink | wird hochgeladen...",
"Nxlink Finished": "NXLink | Hochladen beendet",
"Hdd": "",
"Hdd write protect": "",
"Hdd": "HDD",
"Hdd write protect": "HDD Schreibgeschützt",
"Language": "Sprache",
"Auto": "Systemsprache",
@@ -49,11 +49,11 @@
"Ukrainian": "Українська",
"Misc": "Extras",
"Misc Options": " Extras | Optionen",
"Misc Options": "Extras | Optionen",
"Games": "Spiele",
"Game Options": "",
"Hide forwarders": "",
"Launch random game": "",
"Game Options": "Spiel | Optionen",
"Hide forwarders": "Forwarders ausblenden",
"Launch random game": "Zufälliges Spiel starten",
"List meta records": "",
"Entries": "",
"Failed to list application meta entries": "",
@@ -102,11 +102,10 @@
"GitHub": "GitHub",
"Downloading json": "Lade JSON-File",
"Select asset to download for ": "Wähle Asset für den Download von ",
"Failed to download json": "",
"Failed to download app!": "",
"Failed to download json": "Download von JSON-Datei fehlgeschlagen",
"Failed to download app!": "Download der App fehlgeschlagen",
"FTP Install": "",
"FTP Install (EXPERIMENTAL)": "",
"Connection Type: WiFi | Strength: ": "",
"Connection Type: Ethernet": "",
"Connection Type: None": "",
@@ -116,19 +115,19 @@
"Password:": "",
"SSID:": "",
"Passphrase:": "",
"Failed to install via FTP, press B to exit...": "",
"Ftp install success!": "",
"Ftp install failed!": "",
"Failed to install, press B to exit...": "",
"Install success!": "",
"Install failed!": "",
"USB Install": "",
"USB": "",
"Connected, waiting for file list...": "",
"Connected, starting transfer...": "",
"Connected, waiting for file list...": "Verbunden, warte auf die Dateiliste...",
"Connected, starting transfer...": "Verbunden, Übertragung startet...",
"Failed to init usb, press B to exit...": "",
"Waiting for connection...": "",
"Transferring data...": "",
"Waiting for connection...": "Warten auf Verbindung...",
"Transferring data...": "Datenübertragung...",
"USB connected, sending file list": "",
"Sent file list, waiting for command...": "",
"waiting for usb connection...": "",
"waiting for usb connection...": "Warten auf USB Verbindung...",
"Disable MTP for usb install": "",
"Re-enabled MTP": "",
"Installed via usb": "",
@@ -142,7 +141,7 @@
"microSD card %.1f GB": "",
"Exit": "",
"Install disabled...\nPlease enable installing via the install options.": "",
"No GameCard inserted": "",
"No GameCard inserted": "Keine Spielkarte eingelegt",
"GameCard is already trimmed!": "",
"WARNING: GameCard is already trimmed!": "",
"Continue": "",
@@ -189,7 +188,7 @@
"Enter custom URL": "",
"Enter URL": "",
"Advanced": "Erweitert...",
"Advanced": "Erweitert",
"Advanced Options": " Erweitert | Optionen",
"Logging": "Protokollieren",
"Replace hbmenu on exit": "hbmenu durch sphaira ersetzen",
@@ -244,7 +243,7 @@
"Creating Control": "Erstelle Control",
"Creating Meta": "Erstelle Meta",
"Writing Nca": "Schreibe NCA",
"Updating ncm databse": "Aktualisiere NCM-Datenbank",
"Updating ncm database": "Aktualisiere NCM-Datenbank",
"Pushing application record": "Übertrage Anwendungsdaten",
"Failed to install forwarder": "Fehler beim installieren des Forwarders",
"Unstar": "Kein Favorit",
@@ -254,7 +253,7 @@
"Failed to remove old forwarder, please manually remove it!": "",
"AppStore": "hb-AppStore",
"Appstore": "",
"Appstore": "hb-AppStore",
"Store": "hb-Store",
"Filter: %s | Sort: %s | Order: %s": "Rubrik: %s | Sort.nach.: %s | Ordnung: %s",
"AppStore Options": " hb-AppStore | Optionen",
@@ -262,7 +261,7 @@
"Changelog": "Neuerungen",
"Details": "Details",
"version: %s": "Version: %s",
"updated: %s": "Letztes Update am: %s",
"updated: %s": "Letztes Update: %s",
"category: %s": "Rubrik: %s",
"extracted: %.2f MiB": "Größe: %.2f MiB",
"app_dls: %s": "Anzahl Downloads: %s",
@@ -331,10 +330,10 @@
"Launch ": "Starte ",
"Launch option for: ": "Start Option für: ",
"Select launcher for: ": "Wähle Launcher für: ",
"Close FileBrowser?": "",
"Close FileBrowser?": "Datei-Manager schließen?",
"Sort By": "Sortierung",
"Sort Options": " Sortierung | Optionen",
"Sort Options": "Sortierung | Optionen",
"Filter": "Rubrik",
"All": "Alles anzeigen",
"Emulators": "Emulatoren",
@@ -370,20 +369,20 @@
"Back": "Zurück",
"Select": "Auswählen",
"Open": "Öffne",
"Close": "",
"Close": "Schließe",
"Launch": "Starte",
"Restart": "Neustart",
"Next": "",
"Prev": "",
"Yes": "Ja",
"No": "Nein",
"On": "",
"Off": "",
"On": "An",
"Off": "Aus",
"Install": "Installieren",
"Install Selected files?": "",
"Installing ": "Installiert wird: ",
"Installed ": "",
"Installed ": "Installiert ",
"Installed!": "Installiert!",
"Trying to load ": "Versucht zu laden wird: ",
"Checking MD5": "Checke MD5 Prüfsumme",
@@ -411,13 +410,13 @@
"Updated to ": "Aktualisiert auf: ",
"Failed to download update": "Herunterladen des Updates fehlgeschlagen!",
"%zu hours %zu minutes remaining": "",
"%zu minutes %zu seconds remaining": "",
"%zu seconds remaining": "",
"%zu hours %zu minutes remaining": "%zu Stunden und %zu Minuten verbleibend",
"%zu minutes %zu seconds remaining": "%zu Minuten und %zu Sekunde verbleibend",
"%zu seconds remaining": "%zu Sekunde verbleibend",
"Loading...": "Wird geladen...",
"Loading": "Wird geladen",
"Empty!": "Keine Daten!",
"Not Ready...": "Nicht bereit...",
"Error loading page!": "Ladefehler!"
}
}

View File

@@ -106,7 +106,6 @@
"Failed to download app!": "Failed to download app!",
"FTP Install": "FTP Install",
"FTP Install (EXPERIMENTAL)": "FTP Install (EXPERIMENTAL)",
"Connection Type: WiFi | Strength: ": "Connection Type: WiFi | Strength: ",
"Connection Type: Ethernet": "Connection Type: Ethernet",
"Connection Type: None": "Connection Type: None",
@@ -116,9 +115,9 @@
"Password:": "Password:",
"SSID:": "SSID:",
"Passphrase:": "Passphrase",
"Failed to install via FTP, press B to exit...": "Failed to install via FTP, press  to exit...",
"Ftp install success!": "Ftp install success!",
"Ftp install failed!": "Ftp install failed!",
"Failed to install, press B to exit...": "Failed to install, press  to exit...",
"Install success!": "Install success!",
"Install failed!": "Install failed!",
"USB Install": "USB Install",
"USB": "USB",
"Connected, waiting for file list...": "Connected, waiting for file list...",
@@ -244,7 +243,7 @@
"Creating Control": "Creating Control",
"Creating Meta": "Creating Meta",
"Writing Nca": "Writing Nca",
"Updating ncm databse": "Updating ncm databse",
"Updating ncm database": "Updating ncm database",
"Pushing application record": "Pushing application record",
"Failed to install forwarder": "Failed to install forwarder",
"Unstar": "Unstar",
@@ -420,4 +419,4 @@
"Empty!": "Empty!",
"Not Ready...": "Not Ready...",
"Error loading page!": "Error loading page!"
}
}

View File

@@ -3,10 +3,10 @@
"No Internet": "Sin Internet",
"Switch-Handheld!": "¡Switch-Modo-Portátil!",
"Switch-Docked!": "¡Switch-Modo-TV!",
"Warning! Logs are enabled, Sphaira will run slowly!": "",
"Audio disabled due to suspended game": "",
"Warning! Logs are enabled, Sphaira will run slowly!": "ADVERTENCIA, ¡El registro de eventos está activado, Sphaira correrá más lento!",
"Audio disabled due to suspended game": "Audio deshabilitado debido al juego suspendido",
"Are you sure you wish to cancel?": "¿Estás seguro que deseas cancelar?",
"An error occurred": "",
"An error occurred": "Ha ocurrido un error",
"If this message appears repeatedly, please open an issue.": "Si este mensaje aparece repetidamente, por favor abrir un 'issue'.",
"Menu Options": "Opciones de menú",
@@ -15,10 +15,10 @@
"Theme Options": "Opciones de tema",
"Select Theme": "Seleccionar tema",
"Music": "Música",
"12 Hour Time": "",
"Download Default Music": "",
"Failed to download default_music.bfstm, please try again": "",
"Overwrite current default music?": "",
"12 Hour Time": "Reloj de 12 horas",
"Download Default Music": "Descargar música por defecto",
"Failed to download default_music.bfstm, please try again": "Fallo al descargar archivo default_music.bfstm, intentar nuevamente",
"Overwrite current default music?": "¿Sobreescribir la música por defecto actual?",
"Network": "Red",
"Network Options": "Opciones de red",
@@ -28,8 +28,8 @@
"Nxlink Connected": "NXlink conectado",
"Nxlink Upload": "NXlink subida",
"Nxlink Finished": "NXlink finalizado",
"Hdd": "",
"Hdd write protect": "",
"Hdd": "Disco Duro",
"Hdd write protect": "Disco duro solo lectura",
"Language": "Idioma",
"Auto": "Automático",
@@ -51,44 +51,44 @@
"Misc": "Varios",
"Misc Options": "Opciones varias",
"Games": "Juegos",
"Game Options": "",
"Hide forwarders": "",
"Launch random game": "",
"List meta records": "",
"Entries": "",
"Failed to list application meta entries": "",
"No meta entries found...\n": "",
"Updating application record list": "",
"Dump": "",
"Dump options": "",
"Dump Options": "",
"Select content to dump": "",
"Dump All": "",
"Dump Application": "",
"Dump Patch": "",
"Dump AddOnContent": "",
"Dump DataPatch": "",
"Created nested folder": "",
"Append folder with .xci": "",
"Trim XCI": "",
"Label trimmed XCI": "",
"Multi-threaded USB transfer": "",
"Dump All Bins": "",
"Dump XCI": "",
"Dump Card ID Set": "",
"Dump Card UID": "",
"Dump Certificate": "",
"Dump Initial Data": "",
"Select dump location": "",
"microSD card (/dumps/)": "",
"USB transfer (Switch 2 Switch)": "",
"/dev/null (Speed Test)": "",
"Dumping": "",
"Dump successfull!": "",
"Dump failed!": "",
"Delete successfull!": "",
"Delete failed!": "",
"Success": "",
"Game Options": "Opciones de Juegos",
"Hide forwarders": "Ocultar forwarders",
"Launch random game": "Ejecutar un juego aleatorio",
"List meta records": "Listar registros meta",
"Entries": "Entradas",
"Failed to list application meta entries": "Error al listar las entradas meta del aplicativo",
"No meta entries found...\n": "No se encontró entradas meta...\n",
"Updating application record list": "Actualizando la lista de registros del aplicativo",
"Dump": "Volcar",
"Dump options": "Opciones de volcado",
"Dump Options": "Opciones de volcado",
"Select content to dump": "Seleccione contenido a volcar",
"Dump All": "Volcar todo",
"Dump Application": "Volcar aplicativo",
"Dump Patch": "Volcar parche",
"Dump AddOnContent": "Volcar Contenido adicional",
"Dump DataPatch": "Volcar Datos de parche",
"Created nested folder": "Se creó una carpeta anidadada",
"Append folder with .xci": "Adjuntar carpeta con archivo .XCI",
"Trim XCI": "Archivo XCI recortado",
"Label trimmed XCI": "Archivo XCI con etiqueta removida",
"Multi-threaded USB transfer": "Transferencia USB multi-hilos",
"Dump All Bins": "Volcar todos los binarios",
"Dump XCI": "Volcar archivo XCI",
"Dump Card ID Set": "Volcar conjunto de ID de tarjeta",
"Dump Card UID": "Volcar UID de tarjeta",
"Dump Certificate": "Volcar Certificado",
"Dump Initial Data": "Volcar datos iniciales",
"Select dump location": "Escoja la ubicación para el volcado",
"microSD card (/dumps/)": "Tarjeta microSD (/dumps/)",
"USB transfer (Switch 2 Switch)": "Transferencia USB (de Switch a Switch)",
"/dev/null (Speed Test)": "Medir la velocidad usando dispositivo /dev/null",
"Dumping": "Volcando",
"Dump successfull!": "Volcado satisfactorio",
"Dump failed!": "Error en el volcado",
"Delete successfull!": "Borrado satisfactorio",
"Delete failed!": "Error al borrar",
"Success": "Proceso Exitoso",
"Themezer": "Themezer",
"Themezer Options": "Opciones de Themezer",
@@ -102,56 +102,55 @@
"GitHub": "GitHub",
"Downloading json": "Descargando json",
"Select asset to download for ": "Seleccionar recurso a descargar para ",
"Failed to download json": "",
"Failed to download app!": "",
"Failed to download json": "Error al descargar JSON",
"Failed to download app!": "¡Error al descargar aplicativo!",
"FTP Install": "",
"FTP Install (EXPERIMENTAL)": "",
"Connection Type: WiFi | Strength: ": "",
"Connection Type: Ethernet": "",
"Connection Type: None": "",
"Host:": "",
"Port:": "",
"Username:": "",
"Password:": "",
"SSID:": "",
"Passphrase:": "",
"Failed to install via FTP, press B to exit...": "",
"Ftp install success!": "",
"Ftp install failed!": "",
"USB Install": "",
"USB": "",
"Connected, waiting for file list...": "",
"Connected, starting transfer...": "",
"Failed to init usb, press B to exit...": "",
"Waiting for connection...": "",
"Transferring data...": "",
"USB connected, sending file list": "",
"Sent file list, waiting for command...": "",
"waiting for usb connection...": "",
"Disable MTP for usb install": "",
"Re-enabled MTP": "",
"Installed via usb": "",
"Usb install success!": "",
"Usb install failed!": "",
"Press B to exit...": "",
"GameCard Install": "",
"GameCard": "",
"GC": "",
"System memory %.1f GB": "",
"microSD card %.1f GB": "",
"Exit": "",
"Install disabled...\nPlease enable installing via the install options.": "",
"No GameCard inserted": "",
"GameCard is already trimmed!": "",
"WARNING: GameCard is already trimmed!": "",
"Continue": "",
"Gc install success!": "",
"Gc install failed!": "",
"FTP Install": "Instalación vía FTP",
"Connection Type: WiFi | Strength: ": "Tipo de conexión: WiFi | Fuerza de la señal",
"Connection Type: Ethernet": "Tipo de Conexión: Ethernet",
"Connection Type: None": "Tipo de Conexión: Ninguna",
"Host:": "Host",
"Port:": "Puerto",
"Username:": "Nombre de Usuario",
"Password:": "Contraseña",
"SSID:": "SSID",
"Passphrase:": "Parafrase",
"Failed to install, press B to exit...": "Error al instalar vía FTP, presione B para salir…",
"Install success!": "¡Instalación vía FTP satisfactoria!",
"Install failed!": "¡Error en instalación vía FTP!",
"USB Install": "Instalación vía USB",
"USB": "USB",
"Connected, waiting for file list...": "Conectado, esperando lista de archivos…",
"Connected, starting transfer...": "Conectado, inicando la transferencia…",
"Failed to init usb, press B to exit...": "Error al iniciar USB, presione B para salir…",
"Waiting for connection...": "Esperando por conexión…",
"Transferring data...": "Tranfiriendo datos…",
"USB connected, sending file list": "USB conectado, enviando listado de archivos",
"Sent file list, waiting for command...": "Listado de archivos enviado, esprendo comando…",
"waiting for usb connection...": "Esperando por conexión USB…",
"Disable MTP for usb install": "Instalación vía MTP Deshabilitada",
"Re-enabled MTP": "Instalación vía MTP rehabilitado",
"Installed via usb": "Instalado vía USB",
"Usb install success!": "Instalación USB satisfactoria",
"Usb install failed!": "Error en instalación USB",
"Press B to exit...": "Presione B para salir…",
"GameCard Install": "Instalación de Tarjeta de Juego",
"GameCard": "Tarjeta de Juego",
"GC": "TJ",
"System memory %.1f GB": "Memoria del sistema %.1f GB",
"microSD card %.1f GB": "Tarjeta microSD %.1f GB",
"Exit": "Salir",
"Install disabled...\nPlease enable installing via the install options.": "Instalación deshabilitada…\nPor favor habilite instalar aplicativos en las opciones de instalación",
"No GameCard inserted": "No hay una Tarjeta de Juego insertada",
"GameCard is already trimmed!": "¡La Tarjeta de Juego ya se encuentra recortada!",
"WARNING: GameCard is already trimmed!": "ADVERTENCIA: ¡La Tarjeta de Juego ya se encuentra recortada!",
"Continue": "Continuar",
"Gc install success!": "Instalación de TJ satisfactoria",
"Gc install failed!": "Fallo al instalar TJ",
"IRS (Infrared Joycon Camera)": "",
"IRS": "",
"Irs": "IRS",
"IRS (Infrared Joycon Camera)": "IRS (Cámara infraroja del JoyCon)",
"IRS": "IRS",
"Irs": "Irs",
"Ambient Noise Level: ": "Nivel de Ruido Ambiente",
"Controller": "Control",
"Pad ": "GamePad ",
@@ -184,14 +183,14 @@
"External Light Filter": "Filtro de luz externa",
"Load Default": "Cargar predeterminado",
"Web": "",
"Select URL": "",
"Enter custom URL": "",
"Enter URL": "",
"Web": "Web",
"Select URL": "Seleccione el URL",
"Enter custom URL": "Ingrese un URL personalizado",
"Enter URL": "Escriba el URL",
"Advanced": "Avanzado",
"Advanced Options": "Opciones avanzadas",
"Logging": "Registro",
"Logging": "Bitácora",
"Replace hbmenu on exit": "Reemplazar hbmenu",
"Restore hbmenu?": "¿Restaurar hbmenu?",
"Restore": "Restaurar",
@@ -202,36 +201,36 @@
"Restored hbmenu": "hbmenu restaurado",
"Restart Sphaira?": "¿Reiniciar sphaira?",
"Press OK to restart Sphaira": "Presiona OK para reiniciar sphaira",
"Boost CPU during transfer": "",
"Boost CPU during transfer": "Tranferir con Overclock",
"Text scroll speed": "Velocidad de scroll",
"Slow": "Lento",
"Normal": "Normal",
"Fast": "Rápido",
"Set left-side menu": "",
"Set right-side menu": "",
"Install options": "",
"Install Options": "",
"Enable sysmmc": "",
"Enable emummc": "",
"Set left-side menu": "Menú izquierdo",
"Set right-side menu": "Menú Drecho",
"Install options": "Opciones de Instalación",
"Install Options": "Opciones de Instalación",
"Enable sysmmc": "Habilitar SysMMC",
"Enable emummc": "Habilitar EmuMMC",
"Show install warning": "Precaución de instalación",
"Install location": "Dispositivo de instalación",
"System memory": "Memoria de sistema",
"microSD card": "microSD",
"Allow downgrade": "",
"Skip if already installed": "",
"Ticket only": "",
"Skip base": "",
"Skip patch": "",
"Skip dlc": "",
"Skip data patch": "",
"Skip ticket": "",
"Skip NCA hash verify": "",
"Skip RSA header verify": "",
"Skip RSA NPDM verify": "",
"Ignore distribution bit": "",
"Convert to standard crypto": "",
"Lower master key": "",
"Lower system version": "",
"Allow downgrade": "Permitir instalar versiones anteriores",
"Skip if already installed": "Saltar si ya está instalado",
"Ticket only": "Únicamente tiquetes",
"Skip base": "Saltar base",
"Skip patch": "Saltar parche",
"Skip dlc": "Saltar DLC",
"Skip data patch": "Saltar datos de parche",
"Skip ticket": "Saltar Tiquete",
"Skip NCA hash verify": "Saltar verificación de Hash NCA",
"Skip RSA header verify": "Saltar verificación de encabezado RSA",
"Skip RSA NPDM verify": "Saltar verificación de NPDM RSA",
"Ignore distribution bit": "Ignorar bit de distribución",
"Convert to standard crypto": "Convertir a crypto estándar",
"Lower master key": "Bajar versión de llave maestra",
"Lower system version": "Bajar versión de sistema",
"Homebrew": "Homebrew",
"Apps": "Apps",
@@ -244,17 +243,17 @@
"Creating Control": "Creando Control",
"Creating Meta": "Creando Meta",
"Writing Nca": "Creando NCA",
"Updating ncm databse": "Actualizando base de datos ncm",
"Updating ncm database": "Actualizando base de datos ncm",
"Pushing application record": "Registro de aplicación",
"Failed to install forwarder": "Fallo al instalar forwarder",
"Unstar": "Quitar favorito",
"Star": "Favorito",
"Unstarred ": "Quitar Favorito",
"Starred ": "Favorito",
"Failed to remove old forwarder, please manually remove it!": "",
"Failed to remove old forwarder, please manually remove it!": "Error al remover forwarder anterior, por favor remuévalo manualmente",
"AppStore": "Tienda",
"Appstore": "",
"Appstore": "Tienda",
"Store": "Tienda",
"Filter: %s | Sort: %s | Order: %s": "Filtrar: %s | Clasificar: %s | Orden: %s",
"AppStore Options": "Opciones de la Tienda",
@@ -281,57 +280,57 @@
"Copy": "Copiar",
"Copying ": "Copiando ",
"Paste": "Pegar",
"Paste file(s)?": "",
"Paste file(s)?": "Pegar archivo(s)",
"Pasting ": "Pegando ",
"Pasting": "Pegando",
"Rename": "Renombrar",
"Set New File Name": "Establecer nuevo nombre de archivo",
"Failed to delete directory": "",
"Failed to delete file": "",
"Extract zip": "",
"Extract Options": "",
"Extract here": "",
"Extract to root": "",
"Are you sure you want to extract to root?": "",
"Extract to...": "",
"Enter the path to the folder to extract into": "",
"Extracting ": "",
"Extract success!": "",
"Extract failed!": "",
"Compress to zip": "",
"Compress Options": "",
"Compress": "",
"Compress to...": "",
"Compressing ": "",
"Compress success!": "",
"Compress failed!": "",
"Failed to delete directory": "Error al borrar carpeta",
"Failed to delete file": "Error al borrar archivo",
"Extract zip": "Extraer carpeta comprimida ZIP",
"Extract Options": "Opciones de extracción",
"Extract here": "Extraer aquí",
"Extract to root": "Extraer en la raíz",
"Are you sure you want to extract to root?": "¿Seguro que desea extraerlo en la carpeta raíz?",
"Extract to...": "Extraer en…",
"Enter the path to the folder to extract into": "Ingrese la ruta en la cual extraer",
"Extracting ": "Extrayendo",
"Extract success!": "¡Extracción satisfactoria!",
"Extract failed!": "¡Extracción fallida!",
"Compress to zip": "Comprimir en archivo ZIP",
"Compress Options": "Opciones de compresión",
"Compress": "Comprimir",
"Compress to...": "Comprimir en…",
"Compressing ": "Comprimiendo",
"Compress success!": "¡Compresión satisfactoria!",
"Compress failed!": "¡Error en la descompresión!",
"Create File": "Crear archivo",
"Set File Name": "Establecer nombre de archivo",
"Create Folder": "Crear carpeta",
"Set Folder Name": "Establecer nombre de carpeta",
"Creating ": "Creando ",
"View as text (unfinished)": "Ver como texto (sin terminar)",
"Upload": "",
"Select upload location": "",
"No upload locations set!": "",
"Uploading": "",
"Upload successfull!": "",
"Upload failed!": "",
"Hash": "",
"Hash Options": "",
"Hashing": "",
"Failed to hash file...": "",
"Upload": "Subir",
"Select upload location": "Seleccione la ubicación a la cual subir",
"No upload locations set!": "No se ha definido la ubicación para subir",
"Uploading": "Subiendo",
"Upload successfull!": "¡Subida satisfactoria!",
"Upload failed!": "¡Error en la subida!",
"Hash": "Hash",
"Hash Options": "Opciones de Hash",
"Hashing": "Creando Hash",
"Failed to hash file...": "Error al crear Hash del archivo…",
"Ignore read only": "Ignorar sólo lectura",
"Mount": "Montar",
"Sd": "SD",
"Image System memory": "Imagen memoria interna",
"Image microSD card": "Imagen tarjeta microSD",
"Empty...": "Vacío...",
"Empty...": "Vacío",
"Open with DayBreak?": "¿Abrir con DayBreak?",
"Launch ": "Abrir ",
"Launch option for: ": "Opción de abrir con: ",
"Select launcher for: ": "Seleccionar abrir con: ",
"Close FileBrowser?": "",
"Close FileBrowser?": "¿Cerrar explorador de archivos?",
"Sort By": "Ordenar por",
"Sort Options": "Opciones de clasificación",
@@ -358,32 +357,32 @@
"Ascending": "Ascendente",
"Ascending (Up)": "Ascendente (arriba)",
"Asc": "Ascendente",
"Layout": "",
"List": "",
"Icon": "",
"Grid": "",
"Layout": "Diseño",
"List": "Lista",
"Icon": "Íconos",
"Grid": "Malla",
"Search": "Buscar",
"Options": "Opciones",
"Split": "",
"Split": "Dividir",
"OK": "OK",
"Back": "Atrás",
"Select": "Seleccionar",
"Open": "Abrir",
"Close": "",
"Close": "Cerrar",
"Launch": "Ejecutar",
"Restart": "Reiniciar",
"Next": "",
"Prev": "",
"Next": "Siguiente",
"Prev": "Anterior",
"Yes": "Sí",
"No": "No",
"On": "",
"Off": "",
"On": "Activo",
"Off": "Apagado",
"Install": "Instalar",
"Install Selected files?": "",
"Install Selected files?": "¡Instalar los archivos seleccionados?",
"Installing ": "Instalando ",
"Installed ": "",
"Installed ": "Instalado",
"Installed!": "¡Instalado!",
"Trying to load ": "Intentando cargar ",
"Checking MD5": "Chequeando MD5",
@@ -403,7 +402,7 @@
"Download": "Descargar",
"Downloading ": "Descargando ",
"Downloaded ": "Descargado ",
"Download via the Network options!": "",
"Download via the Network options!": "¡Descargar vía las opciones de red!",
"Update": "Actualizar",
"Update avaliable: ": "Actualización disponible: ",
@@ -411,13 +410,13 @@
"Updated to ": "Actualizado a ",
"Failed to download update": "Fallo al descargar actualización",
"%zu hours %zu minutes remaining": "",
"%zu minutes %zu seconds remaining": "",
"%zu seconds remaining": "",
"%zu hours %zu minutes remaining": "Faltan %zu horas %zu minutos",
"%zu minutes %zu seconds remaining": "Faltan %zu minutos %zu segundos",
"%zu seconds remaining": "Faltan %zu segundos",
"Loading...": "Cargando...",
"Loading...": "Cargando",
"Loading": "Cargando",
"Empty!": "¡Vacío!",
"Not Ready...": "No listo aún...",
"Not Ready...": "No listo aún",
"Error loading page!": "¡Error cargando la página!"
}
}

View File

@@ -9,7 +9,7 @@
"An error occurred": "Une erreur s'est produite",
"If this message appears repeatedly, please open an issue.": "Si ce message apparaît en boucle, veuillez ouvrir une issue sur :",
"Menu Options": "Paramètres",
"Menu Options": "Paramètres du menu",
"Menu": "Paramètres",
"Theme": "Thèmes",
"Theme Options": "Paramètres des Thèmes",
@@ -26,7 +26,7 @@
"Mtp": "MTP",
"Nxlink": "Nxlink",
"Nxlink Connected": "Nxlink Connecté",
"Nxlink Upload": "Téléchargement Nxlink",
"Nxlink Upload": "Téléversement Nxlink",
"Nxlink Finished": "Nxlink terminé",
"Hdd": "Disque Dur",
"Hdd write protect": "Protection en écriture du disque dur",
@@ -73,12 +73,12 @@
"Trim XCI": "Trimmer le XCI",
"Label trimmed XCI": "Étiquette sur le XCI trimmer",
"Multi-threaded USB transfer": "Transfert USB multithread",
"Dump All Bins": "",
"Dump XCI": "",
"Dump Card ID Set": "",
"Dump Card UID": "",
"Dump Certificate": "",
"Dump Initial Data": "",
"Dump All Bins": "Dumper tous les éléments",
"Dump XCI": "Dumper le XCI",
"Dump Card ID Set": "Dumper l'ID de la cartouche",
"Dump Card UID": "Dumper l'UID de la cartouche",
"Dump Certificate": "Dumper le certificat",
"Dump Initial Data": "Dumper l'Initial Data",
"Select dump location": "Sélectionner l'emplacement du dump :",
"microSD card (/dumps/)": "carte microSD (/dumps/)",
"USB transfer (Switch 2 Switch)": "Transfert USB (Switch à Switch",
@@ -100,13 +100,12 @@
"Download theme?": "Voulez-vous télécharger ce thème ?",
"GitHub": "Github",
"Downloading json": "Téléchargement du .json",
"Downloading json": "Téléchargement du json",
"Select asset to download for ": "Sélectionnez l'actif à télécharger pour :",
"Failed to download json": "Échec du téléchargement de .json",
"Failed to download json": "Échec du téléchargement du json",
"Failed to download app!": "Échec du téléchargement de l'application",
"FTP Install": "Installer contenu via FTP",
"FTP Install (EXPERIMENTAL)": "Installation via FTP (EXPERIMENTAL)",
"Connection Type: WiFi | Strength: ": "Type de connexion : WiFi / Force du signal :",
"Connection Type: Ethernet": "Type de connexion : Ethernet",
"Connection Type: None": "Type de connexion : Aucune",
@@ -116,9 +115,9 @@
"Password:": "Mot de passe :",
"SSID:": "SSID :",
"Passphrase:": "Mot de passe :",
"Failed to install via FTP, press B to exit...": "Installation via FTP échouée, appuyer sur B pour quitter...",
"Ftp install success!": "Installation via FTP réussie !",
"Ftp install failed!": "Installation via FTP échouée !",
"Failed to install, press B to exit...": "Installation échouée, appuyer sur B pour quitter...",
"Install success!": "Installation réussie !",
"Install failed!": "Installation échouée !",
"USB Install": "Installer contenu via USB",
"USB": "Installation via USB",
"Connected, waiting for file list...": "Connecté, en attente de la liste des fichiers...",
@@ -136,7 +135,7 @@
"Usb install failed!": "Installation via USB échouée !",
"Press B to exit...": "Appuyer sur B pour quitter...",
"GameCard Install": "Installation de la cartouche du jeu",
"GameCard": "Carte du jeu",
"GameCard": "Cartouche du jeu",
"GC": "GC",
"System memory %.1f GB": "Mémoire système %.1f GB",
"microSD card %.1f GB": "Carte microSD %.1f GB",
@@ -150,7 +149,7 @@
"Gc install failed!": "Installation de la cartouche échouée !",
"IRS (Infrared Joycon Camera)": "IRS (infrarouge de la cam du Joycon",
"IRS": "Infrarouge Camera",
"IRS": "Caméra Infrarouge",
"Irs": "Irs",
"Ambient Noise Level: ": "Niveau de bruit ambiant: ",
"Controller": "Contrôleur",
@@ -187,7 +186,7 @@
"Web": "Web",
"Select URL": "Sélectionnez une adresse",
"Enter custom URL": "Entrez une adresse personnalisée",
"Enter URL": "Enter l'adresse :",
"Enter URL": "Entrer l'adresse :",
"Advanced": "Menu avancé",
"Advanced Options": "Menu avancé",
@@ -212,7 +211,7 @@
"Install options": "Options d'installation des jeux",
"Install Options": "Options d'Installation",
"Enable sysmmc": "Afficher la SYSmmc",
"Enable emummc": "Afficher sur l'EMUmmc",
"Enable emummc": "Afficher l'EMUmmc",
"Show install warning": "Avertissement lors d'installation",
"Install location": "Emplacement d'installation",
"System memory": "Mémoire système",
@@ -244,7 +243,7 @@
"Creating Control": "Création de Control",
"Creating Meta": "Création de Meta",
"Writing Nca": "Écriture NCA",
"Updating ncm databse": "Mise à jour de la base de données ncm",
"Updating ncm database": "Mise à jour de la base de données ncm",
"Pushing application record": "Ajout de l'enregistrement de l'application",
"Failed to install forwarder": "Échec de l'installation du forwarder",
"Unstar": "Retirer des favories",
@@ -257,7 +256,7 @@
"Appstore": "Boutique d'applications",
"Store": "AppStore",
"Filter: %s | Sort : %s | Order: %s" : "Filtre : %s | Tri : %s | Ordre : %s",
"AppStore Options": "Options du AppStore",
"AppStore Options": "Options de l'AppStore",
"Info": "Info.",
"Changelog": "Journal des modifications",
"Details": "Détails",
@@ -281,13 +280,13 @@
"Copy": "Copier",
"Copying ": "Copie en cours...",
"Paste": "Coller",
"Paste file(s)?": "Copier le colis ?",
"Paste file(s)?": "Copier le(s) fichier(s) ?",
"Pasting": "Collage en cours...",
"Pasting": "Collage en cours...",
"Rename": "Renommer",
"Set New File Name": "Nouveau nom du fichier",
"Failed to delete directory": "Échec de la suppression du répertoire",
"Failed to delete file": "Échec de la suppression du répertoire",
"Failed to delete file": "Échec de la suppression du fichier",
"Extract zip": "Extraire le zip",
"Extract Options": "Options d'extraction",
"Extract here": "Extraire ici",
@@ -308,15 +307,15 @@
"Create File": "Créer un fichier",
"Set File Name": "Nommer Le fichier",
"Create Folder": "Créer un dossier",
"Set Folder Name": "Nommer la dossier",
"Set Folder Name": "Nommer le dossier",
"Creating ": "Création ",
"View as text (unfinished)": "Afficher sous forme de texte (inachevé)",
"Upload": "Télécharger",
"Select upload location": "Emplacement du téléchargement :",
"No upload locations set!": "Aucun emplacement configuré !",
"Uploading": "Téléchargement en cours...",
"Upload successfull!": "Téléchargement réussi !",
"Upload failed!": "Téléchargement échoué !",
"Upload": "Téléverser",
"Select upload location": "Emplacement du téléversement :",
"No upload locations set!": "Aucun emplacement de téléversement configuré !",
"Uploading": "Téléversement en cours...",
"Upload successfull!": "Téléversement réussi !",
"Upload failed!": "Téléversement échoué !",
"Hash": "Hash",
"Hash Options": "Options du Hash",
"Hashing": "Hachage",

View File

@@ -106,7 +106,6 @@
"Failed to download app!": "",
"FTP Install": "",
"FTP Install (EXPERIMENTAL)": "",
"Connection Type: WiFi | Strength: ": "",
"Connection Type: Ethernet": "",
"Connection Type: None": "",
@@ -116,9 +115,9 @@
"Password:": "",
"SSID:": "",
"Passphrase:": "",
"Failed to install via FTP, press B to exit...": "",
"Ftp install success!": "",
"Ftp install failed!": "",
"Failed to install, press B to exit...": "",
"Install success!": "",
"Install failed!": "",
"USB Install": "",
"USB": "",
"Connected, waiting for file list...": "",
@@ -244,7 +243,7 @@
"Creating Control": "",
"Creating Meta": "",
"Writing Nca": "",
"Updating ncm databse": "",
"Updating ncm database": "",
"Pushing application record": "",
"Failed to install forwarder": "",
"Unstar": "Rimuovi dai preferiti",
@@ -420,4 +419,4 @@
"Empty!": "Vuoto!",
"Not Ready...": "Non pronto...",
"Error loading page!": "Errore nel caricare la pagina!"
}
}

View File

@@ -106,7 +106,6 @@
"Failed to download app!": "アプリのダウンロードに失敗しました!",
"FTP Install": "FTPでインストール",
"FTP Install (EXPERIMENTAL)": "FTPでインストール(実験機能)",
"Connection Type: WiFi | Strength: ": "接続: WiFi | 強度: ",
"Connection Type: Ethernet": "接続: イーサネット",
"Connection Type: None": "接続: なし",
@@ -116,9 +115,9 @@
"Password:": "暗証番号:",
"SSID:": "SSID:",
"Passphrase:": "WiFi暗証番号:",
"Failed to install via FTP, press B to exit...": "FTP経由でインストールできませんでした、を押して終了します",
"Ftp install success!": "FTPインストール完了!",
"Ftp install failed!": "FTPインストール失敗!",
"Failed to install, press B to exit...": "FTP経由でインストールできませんでした、を押して終了します",
"Install success!": "FTPインストール完了!",
"Install failed!": "FTPインストール失敗!",
"USB Install": "USBインストール",
"USB": "USBインストール",
"Connected, waiting for file list...": "接続されました、ファイルリストを待っています",
@@ -244,7 +243,7 @@
"Creating Control": "コントロール作成中",
"Creating Meta": "メター作成中",
"Writing Nca": "Nca書き取り中",
"Updating ncm databse": "ncmのDBをアップデート中",
"Updating ncm database": "ncmのDBをアップデート中",
"Pushing application record": "アプリの記録をプッシュ中",
"Failed to install forwarder": "Forwarderのインストール失敗",
"Unstar": "お気に入り解除",
@@ -420,4 +419,4 @@
"Empty!": "何も見つかりません",
"Not Ready...": "準備ができていません",
"Error loading page!": "ページのロードエラー"
}
}

View File

@@ -106,7 +106,6 @@
"Failed to download app!": "앱 다운로드 실패!",
"FTP Install": "FTP 설치",
"FTP Install (EXPERIMENTAL)": "FTP 설치 (실험실 기능)",
"Connection Type: WiFi | Strength: ": "상태: WiFi | 신호 세기: ",
"Connection Type: Ethernet": "상태: 이더넷",
"Connection Type: None": "상태: 연결 없음",
@@ -116,9 +115,9 @@
"Password:": "비밀번호:",
"SSID:": "SSID:",
"Passphrase:": "WiFi 암호:",
"Failed to install via FTP, press B to exit...": "FTP 설치 실패함, 종료하려면  버튼을 입력하세요...",
"Ftp install success!": "FTP 설치 완료!",
"Ftp install failed!": "FTP 설치 실패!",
"Failed to install, press B to exit...": "FTP 설치 실패함, 종료하려면  버튼을 입력하세요...",
"Install success!": "FTP 설치 완료!",
"Install failed!": "FTP 설치 실패!",
"USB Install": "USB 설치",
"USB": "USB 설치",
"Connected, waiting for file list...": "연결됨, 파일 목록 대기 중...",
@@ -244,7 +243,7 @@
"Creating Control": "컨트롤 생성",
"Creating Meta": "메타 생성",
"Writing Nca": "Nca 쓰기",
"Updating ncm databse": "Ncm 데이터베이스 업데이트",
"Updating ncm database": "Ncm 데이터베이스 업데이트",
"Pushing application record": "응용 프로그램 기록 푸싱",
"Failed to install forwarder": "바로가기 설치 실패함",
"Unstar": "즐겨찾기 해제",
@@ -420,4 +419,4 @@
"Empty!": "찾을 수 없습니다!",
"Not Ready...": "준비되지 않음...",
"Error loading page!": "페이지 로딩 오류!"
}
}

View File

@@ -106,7 +106,6 @@
"Failed to download app!": "",
"FTP Install": "",
"FTP Install (EXPERIMENTAL)": "",
"Connection Type: WiFi | Strength: ": "",
"Connection Type: Ethernet": "",
"Connection Type: None": "",
@@ -116,9 +115,9 @@
"Password:": "",
"SSID:": "",
"Passphrase:": "",
"Failed to install via FTP, press B to exit...": "",
"Ftp install success!": "",
"Ftp install failed!": "",
"Failed to install, press B to exit...": "",
"Install success!": "",
"Install failed!": "",
"USB Install": "",
"USB": "",
"Connected, waiting for file list...": "",
@@ -244,7 +243,7 @@
"Creating Control": "",
"Creating Meta": "",
"Writing Nca": "",
"Updating ncm databse": "",
"Updating ncm database": "",
"Pushing application record": "",
"Failed to install forwarder": "",
"Unstar": "",
@@ -420,4 +419,4 @@
"Empty!": "",
"Not Ready...": "",
"Error loading page!": ""
}
}

View File

@@ -3,7 +3,7 @@
"No Internet": "Sem internet",
"Switch-Handheld!": "Switch-Portátil",
"Switch-Docked!": "Switch-Docado",
"Warning! Logs are enabled, Sphaira will run slowly!": "",
"Warning! Logs are enabled, Sphaira will run slowly!": "AVISO: O registro de depuração está habilitado, o sphaira ficará lento!",
"Audio disabled due to suspended game": "Áudio desativado devido ao software suspenso.",
"Are you sure you wish to cancel?": "Tem certeza de que quer cancelar?",
"An error occurred": "Ocorreu um erro.",
@@ -50,19 +50,20 @@
"Misc": "Diversos",
"Misc Options": "Diversos",
"Games": "Softwares",
"Game Options": "Opções do software",
"Hide forwarders": "Ocultar atalhos forwarder",
"Launch random game": "Iniciar um software aleatório",
"List meta records": "Registro de dados",
"List meta records": "Lista de registros",
"Entries": "Entradas",
"Failed to list application meta entries": "Falha ao listar as meta entradas do software.",
"No meta entries found...\n": "Nenhuma entrada de registros encontrada...\n",
"Updating application record list": "Atualizando a lista de registros do software...",
"Dump": "Exportar dados",
"Dump": "Exportar software",
"Dump options": "Opções de exportação",
"Dump Options": "Opções de exportação",
"Select content to dump": "Exportação de dados",
"Select content to dump": "Exportação de software",
"Dump All": "Exportar tudo",
"Dump Application": "Exportar software base",
"Dump Patch": "Exportar atualização",
@@ -72,7 +73,7 @@
"Append folder with .xci": "Acrescentar .xci à pasta",
"Trim XCI": "Aparar arquivos .xci",
"Label trimmed XCI": "Rotular .xci aparados",
"Multi-threaded USB transfer": "Trasfêrencia em multithread",
"Multi-threaded USB transfer": "Transferências em multithread",
"Dump All Bins": "Exportar todos os binários",
"Dump XCI": "Exportar .xci",
"Dump Card ID Set": "Exportar conjunto de IDs",
@@ -89,6 +90,33 @@
"Delete successfull!": "Remoção concluída.",
"Delete failed!": "Remoção falhou.",
"Success": "Concluído.",
"Title cache": "Usar cache de títulos",
"Saves": "Dados salvos",
"Save Options": "Opções de dados salvos",
"Account": "Usuário",
"Data Type": "Tipo de dados",
"System": "Sistema",
"BCAT": "BCAT",
"Device": "Dispositivo",
"Temporary": "Temporário",
"Cache": "Cache",
"System BCAT": "BCAT (Sistema)",
"Backup": "Backup",
"Auto backup": "Backup automático",
"Auto backup on restore": "Fazer backup ao restaurar",
"Compress backup": "Comprimir backups",
"Are you sure you want to backup save(s)?": "Tem certeza de que quer fazer backup dos dados salvo?",
"No saves found in ": "Nenhum dado salvo encontrado em ",
"Backed up to ": "Backup salvo em ",
"Backup successfull!": "Backup concluído.",
"Backup failed!": "Backup falhou.",
"Select backup location": "Selecione o local de backup",
"Select restore location": "Selecione o local de restauração",
"Restore save for: ": "Restaurar backup de dados salvo para: ",
"Are you sure you want to restore ": "Tem certeza de que quer restaurar os dados salvo de ",
"Restore successfull!": "Restauração concluída.",
"Restore failed!": "Restauração falhou.",
"Themezer": "Themezer",
"Themezer Options": "Opções do Themezer",
@@ -102,11 +130,10 @@
"GitHub": "GitHub",
"Downloading json": "Baixando JSON",
"Select asset to download for ": "Selecione o recurso para baixar de ",
"Failed to download json": "",
"Failed to download app!": "",
"Failed to download json": "Falha ao baixar json.",
"Failed to download app!": "falha ao baixar aplicativo.",
"FTP Install": "Instalação via FTP",
"FTP Install (EXPERIMENTAL)": "Instalação via FTP (experimental)",
"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",
@@ -116,40 +143,55 @@
"Password:": "Senha:",
"SSID:": "SSID:",
"Passphrase:": "Senha:",
"Failed to install via FTP, press B to exit...": "Falha ao instalar via FTP, aperte B para sair.",
"Ftp install success!": "Instalação via FTP concluída.",
"Ftp install failed!": "Instalação via FTP falhou.",
"Failed to install, press B to exit...": "Falha ao instalar, aperte para sair.",
"Install success!": "Instalação concluída.",
"Install failed!": "Instalação falhou.",
"MTP Install": "Instalação via MTP",
"State: %s | Speed: %s": "Estado: %s | Velocidade: %s",
"Detached": "Desconectado",
"Attached": "Conectado",
"Powered": "Energia",
"Default": "Padrão",
"Address": "Endereço",
"Configured": "Configurado",
"Suspended": "Suspenso",
"USB 1.0 Low Speed": "USB 1.0",
"USB 1.1 Full Speed": "USB 1.1 (Full Speed)",
"USB 2.0 High Speed": "USB 2.0 (High Speed)",
"USB 3.0 Super Speed": "USB 3.0 (SuperSpeed)",
"Drag'n'Drop (NSP, XCI, NSZ, XCZ) to the install folder": "Arraste o(s) arquivo(s) .nsp/.xci/.nsz/.xcz para a pasta \"Install\".",
"Failed to install via MTP, press B to exit...": "Falha ao instalar via MTP, aperte  para sair.",
"MTP install success!": "Instalação via MTP concluída.",
"MTP install failed!": "Instalação via MTP falhou.",
"USB Install": "Instalação via USB",
"USB": "USB",
"Connected, waiting for file list...": "Conectado, aguardando lista de arquivos...",
"Connected, starting transfer...": "Conectado, iniciando transferência...",
"Failed to init usb, press B to exit...": "Falha ao instalar via USB,\naperte B para sair.",
"Failed to init usb, press B to exit...": "Falha ao inicializar USB,\naperte para sair.",
"Waiting for connection...": "Aguardando conexão...",
"Transferring data...": "Transferindo dados...",
"USB connected, sending file list": "USB conectado, enviando lista de arquivos",
"Sent file list, waiting for command...": "Lista de arquivos enviada, aguardando comando...",
"waiting for usb connection...": "Aguardando conexão USB...",
"Disable MTP for usb install": "Escuta MTP desabilitada temporáriamente.",
"Disable MTP for usb install": "Escuta MTP desabilitada temporariamente.",
"Re-enabled MTP": "Escuta MTP reabilitada.",
"Installed via usb": "Instalado via USB",
"Usb install success!": "Instalação via USB concluída.",
"Usb install failed!": "Instalação via USB falhou.",
"Press B to exit...": "Aperte B para sair.",
"Press B to exit...": "Aperte para sair.",
"GameCard Install": "Instalação de cartão de jogo",
"GameCard": "Cartão de jogo",
"GC": "Cartão de jogo",
"System memory %.1f GB": "Memória do console %.1f GB",
"microSD card %.1f GB": "Cartão microSD %.1f GB",
"Exit": "Sair",
"Install disabled...\nPlease enable installing via the install options.": "",
"No GameCard inserted": "",
"GameCard is already trimmed!": "",
"WARNING: GameCard is already trimmed!": "",
"Continue": "",
"Install disabled...\nPlease enable installing via the install options.": "Instalação desabilitada.\nHabilite a função através das opções de instalação.",
"No GameCard inserted": "Nenhum cartão de jogo inserido.",
"GameCard is already trimmed!": "O cartão de jogo já está aparado.",
"WARNING: GameCard is already trimmed!": "AVISO: O cartão de jogo já está aparado.",
"Continue": "Continuar",
"Gc install success!": "Instalação de cartão de jogo concluída.",
"Gc install failed!": "Instalação de cartão de jogo falhou.",
"Gc install success!": "Instalação do cartão de jogo concluída.",
"Gc install failed!": "Instalação do cartão de jogo falhou.",
"IRS (Infrared Joycon Camera)": "Câmera de movimento IR",
"IRS": "Câmera de movimento IR",
@@ -185,19 +227,15 @@
"Trimming Format": "Formato do recorte",
"External Light Filter": "Filtro de luz externa",
"Load Default": "Restaurar padrão",
"Web": "Navegador de internet",
"Select URL": "Selecione uma URL",
"Enter custom URL": "Digitar URL",
"Web": "",
"Select URL": "",
"Enter custom URL": "",
"Enter URL": "",
"Enter URL": "Digitar URL",
"Advanced": "Opções avançadas",
"Advanced Options": "Opções avançadas",
"Logging": "Registro de depuração",
"Logging": "Registros de depuração",
"Replace hbmenu on exit": "Substituir hbmenu ao sair",
"Restore hbmenu?": "Restaurar hbmenu?",
"Restore": "Restaurar",
@@ -217,8 +255,8 @@
"Set right-side menu": "Menu do botão R",
"Install options": "Opções de instalação",
"Install Options": "Opções de instalação",
"Enable sysmmc": "Habilitar sysMMC",
"Enable emummc": "Habilitar emuMMC",
"Enable sysmmc": "Habilitar em sysMMC",
"Enable emummc": "Habilitar em emuMMC",
"Show install warning": "Mostrar aviso de instalação",
"Install location": "Local de instalação",
"System memory": "Memória do console",
@@ -238,13 +276,6 @@
"Convert to standard crypto": "Converter para crypto padrão",
"Lower master key": "Reduzir master keys",
"Lower system version": "Reduzir versão do sistema",
"Dump options": "Opções de exportação",
"Dump Options": "Opções de exportação",
"Created nested folder": "Criar pastas aninhadas",
"Append folder with .xci": "Acrescentar .xci à pasta",
"Trim XCI": "Aparar arquivos .xci",
"Label trimmed XCI": "Rotular .xci aparados",
"Multi-threaded USB transfer": "Transferências em multithread",
"Homebrew": "Homebrews",
"Apps": "Homebrews",
@@ -257,7 +288,7 @@
"Creating Control": "Criando 'Control'",
"Creating Meta": "Criando 'Meta'",
"Writing Nca": "Gravando NCA",
"Updating ncm databse": "Atualizando base de dados NCM",
"Updating ncm database": "Atualizando base de dados NCM",
"Pushing application record": "Aplicando registro do software",
"Failed to install forwarder": "Falha ao instalar atalho forwarder.",
"Unstar": "Desfavoritar",
@@ -274,11 +305,11 @@
"Info": "Informações",
"Changelog": "Alterações",
"Details": "Detalhes",
"version: %s": "versão: %s",
"updated: %s": "atualizado: %s",
"category: %s": "categoria: %s",
"extracted: %.2f MiB": "tam. extraído: %.2f MiB",
"app_dls: %s": "downloads: %s",
"version: %s": "Versão: %s",
"updated: %s": "Atualizado: %s",
"category: %s": "Categoria: %s",
"extracted: %.2f MiB": "Tamanho: %.2f MiB",
"app_dls: %s": "Nº de downloads: %s",
"More by Author": "Mais deste autor",
"Leave Feedback": "Deixar um feedback",
@@ -291,18 +322,17 @@
"Folders First": "Ordenar pastas primeiro",
"Hidden Last": "Ordenar ocultos por último",
"Split": "Dividir",
"Close": "Fechar",
"Cut": "Recortar",
"Copy": "Copiar",
"Copying ": "Copiando ",
"Paste": "Colar",
"Paste file(s)?": "",
"Paste file(s)?": "Colar arquivo(s)?",
"Pasting ": "Colando ",
"Pasting": "Colando ",
"Rename": "Renomear",
"Set New File Name": "Defina o nome do novo arquivo",
"Failed to delete directory": "",
"Failed to delete file": "",
"Failed to delete directory": "Falha ao apagar diretório.",
"Failed to delete file": "Falha ao apagar arquivo.",
"Extract zip": "Extrair .zip",
"Extract Options": "Opções de extração",
"Extract here": "Extrair aqui",
@@ -334,9 +364,8 @@
"Upload failed!": "Envio falhou.",
"Hash": "Calcular hash",
"Hash Options": "Calcular hash",
"Hashing": "",
"Failed to hash file...": "",
"View as text (unfinished)": "Ver como texto (inacabado)",
"Hashing": "Calculando hash...",
"Failed to hash file...": "Falha ao calcular hash do arquivo.",
"Ignore read only": "Ignorar modo somente leitura",
"Mount": "Montar",
"Sd": "SD",
@@ -419,7 +448,7 @@
"Download": "Baixar",
"Downloading ": "Baixando ",
"Downloaded ": "Baixado ",
"Download via the Network options!": "",
"Download via the Network options!": "Baixe através das opções de rede.",
"Update": "Atualizar",
"Update avaliable: ": "Atualização disponível: ",
@@ -436,4 +465,4 @@
"Empty!": "Vazio.",
"Not Ready...": "Não está pronto...",
"Error loading page!": "Erro ao carregar página."
}
}

View File

@@ -3,7 +3,7 @@
"No Internet": "Нет интернета",
"Switch-Handheld!": "Режим Портатива",
"Switch-Docked!": "Режим Дока",
"Warning! Logs are enabled, Sphaira will run slowly!": "",
"Warning! Logs are enabled, Sphaira will run slowly!": "Внимание! Включены логи, Sphaira будет работать медленно!",
"Audio disabled due to suspended game": "Звук отключён из-за приостановки игры",
"Are you sure you wish to cancel?": "Вы уверены, что хотите отменить?",
"An error occurred": "Произошла ошибка",
@@ -73,12 +73,12 @@
"Trim XCI": "Обрезать .xci",
"Label trimmed XCI": "Пометить обрезанный .xci",
"Multi-threaded USB transfer": "Многоядерная USB передача",
"Dump All Bins": "",
"Dump XCI": "",
"Dump Card ID Set": "",
"Dump Card UID": "",
"Dump Certificate": "",
"Dump Initial Data": "",
"Dump All Bins": "Дамп всех BIN-файлов",
"Dump XCI": "Дамп XCI",
"Dump Card ID Set": "Дамп набора ID карты",
"Dump Card UID": "Дамп UID карты",
"Dump Certificate": "Дамп сертификата",
"Dump Initial Data": "Дамп начальных данных",
"Select dump location": "Выберите место для дампа",
"microSD card (/dumps/)": "microSD карта (/dumps/)",
"USB transfer (Switch 2 Switch)": "Передача по USB (Switch 2 Switch)",
@@ -89,6 +89,24 @@
"Delete successfull!": "Удаление успешно!",
"Delete failed!": "Ошибка удаления!",
"Success": "Успех",
"Title cache": "Кэш заголовков",
"Saves": "Сохранения",
"Save Options": "Опции сохранений",
"Account": "Пользователь",
"Backup": "Сделать бэкап",
"Auto backup": "Автоматический бэкап",
"Auto backup on restore": "Бэкап при восстановлении",
"Compress backup": "Сжимать бэкапы",
"Are you sure you want to backup save(s)?": "Вы уверены, что хотите сделать бэкап(ы)?",
"No saves found in ": "Сохранения не найдены в ",
"Backed up to ": "Бэкап сохранен в ",
"Backup successfull!": "Бэкап успешно создан!",
"Backup failed!": "Ошибка бэкапа!",
"Restore save for: ": "Восстановить сохранение для: ",
"Are you sure you want to restore ": "Вы уверены, что хотите восстановить ",
"Restore successfull!": "Восстановление успешно!",
"Restore failed!": "Ошибка восстановления!",
"Themezer": "Themezer",
"Themezer Options": "Опции Themezer",
@@ -102,11 +120,10 @@
"GitHub": "GitHub",
"Downloading json": "Загрузка json",
"Select asset to download for ": "Выберите ресурс для загрузки: ",
"Failed to download json": "",
"Failed to download app!": "",
"Failed to download json": "Не удалось загрузить json",
"Failed to download app!": "Не удалось загрузить приложение!",
"FTP Install": "Установка по FTP",
"FTP Install (EXPERIMENTAL)": "Установка по FTP (ЭКСПЕРИМЕНТАЛЬНО)",
"Connection Type: WiFi | Strength: ": "Тип подключения: WiFi | Сигнал: ",
"Connection Type: Ethernet": "Тип подключения: Ethernet",
"Connection Type: None": "Нет подключения",
@@ -116,9 +133,9 @@
"Password:": "Пароль:",
"SSID:": "SSID:",
"Passphrase:": "Пароль:",
"Failed to install via FTP, press B to exit...": "Не удалось установить по FTP, нажмите B для выхода...",
"Ftp install success!": "Установка по FTP прошла успешно!",
"Ftp install failed!": "Сбой установки по FTP!",
"Failed to install, press B to exit...": "Не удалось установить по FTP, нажмите B для выхода...",
"Install success!": "Установка по FTP прошла успешно!",
"Install failed!": "Сбой установки по FTP!",
"USB Install": "Установка по USB",
"USB": "USB",
"Connected, waiting for file list...": "Подключено, ожидание списка файлов...",
@@ -141,11 +158,11 @@
"System memory %.1f GB": "Память системы %.1f ГБ",
"microSD card %.1f GB": "Карта microSD %.1f ГБ",
"Exit": "Выход",
"Install disabled...\nPlease enable installing via the install options.": "",
"No GameCard inserted": "",
"GameCard is already trimmed!": "",
"WARNING: GameCard is already trimmed!": "",
"Continue": "",
"Install disabled...\nPlease enable installing via the install options.": "Установка отключена...\nПожалуйста, включите установку в опциях установки.",
"No GameCard inserted": "Картридж не вставлен",
"GameCard is already trimmed!": "Картридж уже обрезан!",
"WARNING: GameCard is already trimmed!": "ВНИМАНИЕ: Картридж уже обрезан!",
"Continue": "Продолжить",
"Gc install success!": "Установка с картриджа успешна!",
"Gc install failed!": "Сбой установки с картриджа!",
@@ -184,14 +201,14 @@
"External Light Filter": "Внешний светофильтр",
"Load Default": "По умолчанию",
"Web": "",
"Select URL": "",
"Enter custom URL": "",
"Enter URL": "",
"Web": "Браузер",
"Select URL": "Выберите URL",
"Enter custom URL": "Введите свой URL",
"Enter URL": "Введите URL",
"Advanced": "Продвинутые",
"Advanced Options": "Расширенные опции",
"Logging": "Журналирование",
"Logging": "Логи",
"Replace hbmenu on exit": "Замена hbmenu при выходе",
"Restore hbmenu?": "Восстановить hbmenu?",
"Restore": "Восстановить",
@@ -239,12 +256,12 @@
"Hide Sphaira": "Скрыть Sphaira",
"Install Forwarder": "Установить форвардер",
"WARNING: Installing forwarders will lead to a ban!": "ВНИМАНИЕ: \nУстановка в сиснанд приведет к бану!",
"Installing Forwarder": "Установить форвардер",
"Installing Forwarder": "Установка форвардера",
"Creating Program": "Создание программы",
"Creating Control": "Создание элемента управления",
"Creating Meta": "Создание меты",
"Writing Nca": "Запись NCA",
"Updating ncm databse": "Обновление базы данных NCM",
"Updating ncm database": "Обновление базы данных NCM",
"Pushing application record": "Добавление записи приложения",
"Failed to install forwarder": "Не удалось установить форвардер",
"Unstar": "Убрать из избранного",
@@ -266,8 +283,9 @@
"category: %s": "категория: %s",
"extracted: %.2f MiB": "извлечено: %.2f MiB",
"app_dls: %s": "app_dls: %s",
"More by Author": "Другие приложения автора",
"More by Author": "Другое от автора",
"Leave Feedback": "Оставить отзыв",
"Visit Website": "Посетить сайт",
"FileBrowser": "Файлы",
"Files": "Файлы",
@@ -281,13 +299,13 @@
"Copy": "Копировать",
"Copying ": "Копирование ",
"Paste": "Вставить",
"Paste file(s)?": "",
"Paste file(s)?": "Вставить файл(ы)?",
"Pasting ": "Вставка ",
"Pasting": "Вставка",
"Rename": "Переименовать",
"Set New File Name": "Задайте новое имя файла",
"Failed to delete directory": "",
"Failed to delete file": "",
"Failed to delete directory": "Не удалось удалить папку",
"Failed to delete file": "Не удалось удалить файл",
"Extract zip": "Распаковать .zip",
"Extract Options": "Опции распаковки",
"Extract here": "Распаковать сюда",
@@ -319,10 +337,10 @@
"Upload failed!": "Сбой Отправки!",
"Hash": "Хэш",
"Hash Options": "Опции хэша",
"Hashing": "",
"Failed to hash file...": "",
"Hashing": "Вычисление хэша",
"Failed to hash file...": "Не удалось вычислить хэш файла...",
"Ignore read only": "Игнор. только для чтения",
"Mount": "Монтирован",
"Mount": "Монтировать",
"Sd": "Micro SD",
"Image System memory": "Альбом Сиснанда",
"Image microSD card": "Альбом Эмунанда",
@@ -340,7 +358,7 @@
"Emulators": "Эмуляторы",
"Tools": "Инструменты",
"Themes": "Темы",
"Legacy": "Старые",
"Legacy": "Легаси",
"Sort": "Сортировать",
"Size": "По размеру",
"Size (Star)": "По размеру (избр.)",
@@ -403,7 +421,7 @@
"Download": "Скачать",
"Downloading ": "Загрузка ",
"Downloaded ": "Загружено ",
"Download via the Network options!": "",
"Download via the Network options!": "Скачайте через: Меню - Сеть",
"Update": "Обновить",
"Update avaliable: ": "Доступно обновление: ",
@@ -420,4 +438,4 @@
"Empty!": "Пусто!",
"Not Ready...": "Не готово...",
"Error loading page!": "Ошибка загрузки страницы!"
}
}

View File

@@ -106,7 +106,6 @@
"Failed to download app!": "",
"FTP Install": "",
"FTP Install (EXPERIMENTAL)": "",
"Connection Type: WiFi | Strength: ": "",
"Connection Type: Ethernet": "",
"Connection Type: None": "",
@@ -116,9 +115,9 @@
"Password:": "",
"SSID:": "",
"Passphrase:": "",
"Failed to install via FTP, press B to exit...": "",
"Ftp install success!": "",
"Ftp install failed!": "",
"Failed to install, press B to exit...": "",
"Install success!": "",
"Install failed!": "",
"USB Install": "",
"USB": "",
"Connected, waiting for file list...": "",
@@ -244,7 +243,7 @@
"Creating Control": "Skapar kontroll",
"Creating Meta": "Skapar metadata",
"Writing Nca": "Skriver Nca",
"Updating ncm databse": "Uppdaterar ncm-databas",
"Updating ncm database": "Uppdaterar ncm-databas",
"Pushing application record": "Skickar programpost",
"Failed to install forwarder": "Misslyckades att installera genväg",
"Unstar": "Avmarkera stjärna",
@@ -420,4 +419,4 @@
"Empty!": "Tomt!",
"Not Ready...": "Inte redo...",
"Error loading page!": "Fel vid laddning av sida!"
}
}

View File

@@ -106,7 +106,6 @@
"Failed to download app!": "Не вдалося завантажити програму!",
"FTP Install": "Встановлення через FTP",
"FTP Install (EXPERIMENTAL)": "Встановлення через FTP (ЕКСПЕРИМЕНТАЛЬНО)",
"Connection Type: WiFi | Strength: ": "Тип підключення: WiFi | Сила сигналу: ",
"Connection Type: Ethernet": "Тип підключення: Ethernet",
"Connection Type: None": "Тип підключення: Немає",
@@ -116,9 +115,9 @@
"Password:": "Пароль:",
"SSID:": "SSID:",
"Passphrase:": "Кодова фраза:",
"Failed to install via FTP, press B to exit...": "Не вдалося встановити через FTP, натисніть B для виходу...",
"Ftp install success!": "Встановлення через FTP успішно завершено.",
"Ftp install failed!": "Встановлення через FTP не вдалося.",
"Failed to install, press B to exit...": "Не вдалося встановити через FTP, натисніть B для виходу...",
"Install success!": "Встановлення через FTP успішно завершено.",
"Install failed!": "Встановлення через FTP не вдалося.",
"USB Install": "Встановлення через USB",
"USB": "USB",
"Connected, waiting for file list...": "Підключено, очікування списку файлів...",
@@ -244,7 +243,7 @@
"Creating Control": "Створення контролера",
"Creating Meta": "Створення метаданих",
"Writing Nca": "Запис NCA",
"Updating ncm databse": "Оновлення бази даних NCM",
"Updating ncm database": "Оновлення бази даних NCM",
"Pushing application record": "Запис даних програми",
"Failed to install forwarder": "Не вдалося встановити форвардер",
"Unstar": "Прибрати з обраного",
@@ -420,4 +419,4 @@
"Empty!": "Пусто!",
"Not Ready...": "Не готово...",
"Error loading page!": "Помилка завантаження сторінки!"
}
}

View File

@@ -106,7 +106,6 @@
"Failed to download app!": "",
"FTP Install": "",
"FTP Install (EXPERIMENTAL)": "",
"Connection Type: WiFi | Strength: ": "",
"Connection Type: Ethernet": "",
"Connection Type: None": "",
@@ -116,9 +115,9 @@
"Password:": "",
"SSID:": "",
"Passphrase:": "",
"Failed to install via FTP, press B to exit...": "",
"Ftp install success!": "",
"Ftp install failed!": "",
"Failed to install, press B to exit...": "",
"Install success!": "",
"Install failed!": "",
"USB Install": "",
"USB": "",
"Connected, waiting for file list...": "",
@@ -244,7 +243,7 @@
"Creating Control": "Tạo điều khiển",
"Creating Meta": "Tạo Meta",
"Writing Nca": "Ghi Nca",
"Updating ncm databse": "Cập nhật ncm databse",
"Updating ncm database": "Cập nhật ncm database",
"Pushing application record": "Đẩy ứng dụng",
"Failed to install forwarder": "Cài đặt ra ngoài màn hình thất bại",
"Unstar": "Xoá yêu thích",
@@ -420,4 +419,4 @@
"Empty!": "Trống!",
"Not Ready...": "Chưa sẵn sàng...",
"Error loading page!": "Lỗi tải trang!"
}
}

View File

@@ -49,11 +49,11 @@
"Ukrainian": "Українська",
"Misc": "拓展",
"Misc Options": "拓展设置",
"Misc Options": "拓展选项",
"Games": "游戏",
"Game Options": "游戏选项",
"Hide forwarders": "隐藏前端启动",
"Launch random game": "启随机游戏",
"Launch random game": "启随机游戏",
"List meta records": "列出元数据记录",
"Entries": "条目",
"Failed to list application meta entries": "未能列出应用程序元条目",
@@ -63,12 +63,18 @@
"Select content to dump": "选择要转储的内容",
"Dump All": "转储全部内容",
"Dump Application": "转储应用程序(本体)",
"Dump Patch": "转储补丁(DLC)",
"Dump AddOnContent": "转储追加内容(UPD)",
"Dump DataPatch": "转储游戏数据",
"Dump Patch": "转储更新补丁(UPD)",
"Dump AddOnContent": "转储追加内容(DLC)",
"Dump DataPatch": "转储数据补丁",
"Dump All Bins": "转储全部文件",
"Dump XCI": "转储.xci",
"Dump Card ID Set": "转储游戏卡ID",
"Dump Card UID": "转储游戏卡UID",
"Dump Certificate": "转储游戏证书",
"Dump Initial Data": "转储初始数据",
"Select dump location": "选择转储位置",
"microSD card (/dumps/NSP/)": "microSD卡/dumps/NSP/",
"USB transfer (Switch 2 Switch)": "USB传输(Switch 2 Switch)",
"USB transfer (Switch 2 Switch)": "USB传输SwitchSwitch",
"/dev/null (Speed Test)": "/dev/null速度测试",
"Dumping": "正在转储",
"Dump successfull!": "转储成功!",
@@ -76,6 +82,24 @@
"Success": "成功",
"Delete successfull!": "删除成功!",
"Delete failed!": "删除失败!",
"Title cache": "标题缓存",
"Saves": "存档",
"Save Options": "存档选项",
"Account": "账户",
"Backup": "备份",
"Auto backup": "自动备份",
"Auto backup on restore": "还原时自动备份",
"Compress backup": "压缩备份",
"Are you sure you want to backup save(s)?": "您确定要备份存档吗?",
"No saves found in ": "未找到存档 ",
"Backed up to ": "已备份到 ",
"Backup successfull!": "已备份成功!",
"Backup failed!": "备份失败!",
"Restore save for: ": "还原备份文件: ",
"Are you sure you want to restore ": "您确定要还原已备份文件吗 ",
"Restore successfull!": "已还原成功!",
"Restore failed!": "还原失败!",
"Themezer": "在线主题",
"Themezer Options": "在线主题选项",
@@ -93,7 +117,6 @@
"Failed to download app!": "下载应用程序失败!",
"FTP Install": "通过 FTP 安装",
"FTP Install (EXPERIMENTAL)": "通过 FTP 安装(实验性)",
"Connection Type: WiFi | Strength: ": "连接类型WiFi |强度:",
"Connection Type: Ethernet": "连接类型:以太网",
"Connection Type: None": "连接类型:无",
@@ -103,9 +126,9 @@
"Password:": "密码:",
"SSID:": "网络名称:",
"Passphrase:": "密码:",
"Failed to install via FTP, press B to exit...": "通过 FTP 安装失败,按 B 键退出...",
"Ftp install success!": "通过 FTP 安装成功。",
"Ftp install failed!": "通过 FTP 安装失败。",
"Failed to install, press B to exit...": "通过 FTP 安装失败,按 B 键退出...",
"Install success!": "通过 FTP 安装成功。",
"Install failed!": "通过 FTP 安装失败。",
"USB Install": "通过 USB 安装",
"USB": "USB",
"Connected, waiting for file list...": "已连接,正在等待文件列表...",
@@ -122,11 +145,11 @@
"Usb install success!": "通过 USB 安装成功。",
"Usb install failed!": "通过 USB 安装失败。",
"Press B to exit...": "按 B 键退出...",
"GameCard Install": "卡安装",
"GameCard": "卡",
"GameCard Install": "游戏卡安装",
"GameCard": "游戏卡",
"GC": "GC",
"System memory %.1f GB": "系统内存 %.1f GB",
"microSD card %.1f GB": "SD卡内存 %.1f GB",
"System memory %.1f GB": "主机内存 %.1f GB",
"microSD card %.1f GB": "SD卡 %.1f GB",
"Exit": "退出",
"Install disabled...\nPlease enable installing via the install options.": "安装已禁用...\n请通过安装选项启用安装。",
"No GameCard inserted": "未插入游戏卡",
@@ -143,8 +166,8 @@
"Controller": "控制器",
"Pad ": "手柄 ",
"HandHeld": "掌机模式",
" (Available)": " (可用",
" (Unsupported)": " (不支持",
" (Available)": " (可用)",
" (Unsupported)": " (不支持)",
" (Unconnected)": " (未连接)",
"Rotation": "旋转",
"0 (Sideways)": "0度",
@@ -194,7 +217,7 @@
"Restored hbmenu": "已恢复 hbmenu",
"Restart Sphaira?": "重启 Sphaira",
"Press OK to restart Sphaira": "按OK键以重启shphaira菜单",
"Boost CPU during transfer": "传输过程中提升CPU",
"Boost CPU during transfer": "传输提升CPU频率",
"Text scroll speed": "文本滚动速度",
"Slow": "慢",
"Normal": "正常",
@@ -214,11 +237,11 @@
"Skip if already installed": "若已安装则跳过",
"Ticket only": "仅安装票据",
"Skip base": "跳过基础部分",
"Skip patch": "跳过补丁",
"Skip dlc": "跳过 DLC可下载内容",
"Skip patch": "跳过更新补丁(UPD)",
"Skip dlc": "跳过追加内容(DLC)",
"Skip data patch": "跳过数据补丁",
"Skip ticket": "跳过票据",
"Skip NCA hash verify": "跳过NCA哈希验证",
"Skip NCA hash verify": "跳过 NCA 哈希验证",
"Skip RSA header verify": "跳过 RSA 头部验证",
"Skip RSA NPDM verify": "跳过 RSA NPDM 验证",
"Ignore distribution bit": "忽略分布位",
@@ -244,7 +267,7 @@
"Creating Control": "正在创建控制器",
"Creating Meta": "正在创建元数据",
"Writing Nca": "正在写入Nca",
"Updating ncm databse": "正在更新ncm数据库",
"Updating ncm database": "正在更新ncm数据库",
"Pushing application record": "正在推送应用记录",
"Failed to install forwarder": "前端应用安装失败",
"Unstar": "取消星标",
@@ -319,8 +342,8 @@
"Upload failed!": "上传失败!",
"Hash": "哈希",
"Hash Options": "哈希选项",
"Hashing": "正在哈希文件",
"Failed to hash file...": "哈希文件失败...",
"Hashing": "正在计算文件哈希",
"Failed to hash file...": "计算文件哈希失败...",
"Ignore read only": "忽略只读",
"Mount": "挂载",
"Sd": "SD卡",
@@ -377,8 +400,8 @@
"Prev": "上一项",
"Yes": "是",
"No": "否",
"On": "开",
"Off": "关",
"On": "开",
"Off": "关",
"Install": "安装",
"Install Selected files?": "安装所选文件?",
@@ -417,7 +440,7 @@
"Loading...": "加载中...",
"Loading": "加载中",
"Empty!": "空空如",
"Empty!": "空空如",
"Not Ready...": "尚未准备好...",
"Error loading page!": "页面加载失败!"
}

View File

@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.13)
set(sphaira_VERSION 0.12.0)
set(sphaira_VERSION 0.13.1)
project(sphaira
VERSION ${sphaira_VERSION}
@@ -45,9 +45,11 @@ add_executable(sphaira
source/ui/menus/ghdl.cpp
source/ui/menus/usb_menu.cpp
source/ui/menus/ftp_menu.cpp
source/ui/menus/mtp_menu.cpp
source/ui/menus/gc_menu.cpp
source/ui/menus/game_menu.cpp
source/ui/menus/grid_menu_base.cpp
source/ui/menus/install_stream_menu_base.cpp
source/ui/error_box.cpp
source/ui/notification.cpp
@@ -79,6 +81,7 @@ add_executable(sphaira
source/hasher.cpp
source/i18n.cpp
source/ftpsrv_helper.cpp
source/haze_helper.cpp
source/threaded_file_transfer.cpp
source/minizip_helper.cpp
@@ -154,13 +157,13 @@ set(FETCHCONTENT_QUIET FALSE)
FetchContent_Declare(ftpsrv
GIT_REPOSITORY https://github.com/ITotalJustice/ftpsrv.git
# GIT_TAG 1.2.2
GIT_TAG 8c18431
GIT_TAG 8782f6b
SOURCE_SUBDIR NONE
)
FetchContent_Declare(libhaze
GIT_REPOSITORY https://github.com/ITotalJustice/libhaze.git
GIT_TAG 8e16df2
GIT_TAG af69c0a
)
FetchContent_Declare(libpulsar
@@ -196,7 +199,7 @@ FetchContent_Declare(zstd
FetchContent_Declare(libusbhsfs
GIT_REPOSITORY https://github.com/ITotalJustice/libusbhsfs.git
GIT_TAG db2bf2a
GIT_TAG d0a973e
)
FetchContent_Declare(libnxtc
@@ -205,8 +208,8 @@ FetchContent_Declare(libnxtc
)
FetchContent_Declare(nvjpg
GIT_REPOSITORY https://github.com/averne/oss-nvjpg.git
GIT_TAG fdcaba8
GIT_REPOSITORY https://github.com/ITotalJustice/oss-nvjpg.git
GIT_TAG 45680e7
)
set(USE_NEW_ZSTD ON)
@@ -268,7 +271,6 @@ FetchContent_MakeAvailable(
)
set(FTPSRV_LIB_BUILD TRUE)
set(FTPSRV_LIB_SOCK_UNISTD TRUE)
set(FTPSRV_LIB_VFS_CUSTOM ${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs_nx.h)
set(FTPSRV_LIB_PATH_SIZE 0x301)
set(FTPSRV_LIB_SESSIONS 16)
@@ -285,6 +287,7 @@ set(FTPSRV_LIB_CUSTOM_DEFINES
USE_VFS_USBHSFS_INIT=$<BOOL:FALSE>
# disable romfs mounting as otherwise we cannot write / modify sphaira.nro
USE_VFS_ROMFS=$<BOOL:FALSE>
FTP_SOCKET_HEADER="${ftpsrv_SOURCE_DIR}/src/platform/nx/socket_nx.h"
)
add_subdirectory(${ftpsrv_SOURCE_DIR} binary_dir)

View File

@@ -192,6 +192,50 @@ public:
}
}
static void SetBoostMode(bool enable, bool force = false) {
static Mutex mutex{};
static int ref_count{};
mutexLock(&mutex);
ON_SCOPE_EXIT(mutexUnlock(&mutex));
if (enable) {
ref_count++;
appletSetCpuBoostMode(ApmCpuBoostMode_FastLoad);
} else {
if (ref_count) {
ref_count--;
}
}
if (!ref_count || force) {
ref_count = 0;
appletSetCpuBoostMode(ApmCpuBoostMode_Normal);
}
}
static auto GetAccountList() -> std::vector<AccountProfileBase> {
std::vector<AccountProfileBase> out;
AccountUid uids[ACC_USER_LIST_SIZE];
s32 account_count;
if (R_SUCCEEDED(accountListAllUsers(uids, std::size(uids), &account_count))) {
for (s32 i = 0; i < account_count; i++) {
AccountProfile profile;
if (R_SUCCEEDED(accountGetProfile(&profile, uids[i]))) {
ON_SCOPE_EXIT(accountProfileClose(&profile));
AccountProfileBase base;
if (R_SUCCEEDED(accountProfileGet(&profile, nullptr, &base))) {
out.emplace_back(base);
}
}
}
}
return out;
}
// private:
static constexpr inline auto CONFIG_PATH = "/config/sphaira/config.ini";
static constexpr inline auto PLAYLOG_PATH = "/config/sphaira/playlog.ini";
@@ -259,6 +303,7 @@ public:
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_common_ticket{INI_SECTION, "convert_to_common_ticket", true};
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};
@@ -269,6 +314,7 @@ public:
option::OptionBool m_dump_trim_xci{"dump", "trim_xci", false};
option::OptionBool m_dump_label_trim_xci{"dump", "label_trim_xci", false};
option::OptionBool m_dump_usb_transfer_stream{"dump", "usb_transfer_stream", true};
option::OptionBool m_dump_convert_to_common_ticket{"dump", "convert_to_common_ticket", true};
// todo: move this into it's own menu
option::OptionLong m_text_scroll_speed{"accessibility", "text_scroll_speed", 1}; // normal

View File

@@ -174,6 +174,7 @@ enum {
Module_Npln = 321,
Module_Tspm = 499,
Module_Devmenu = 500,
Module_Sphaira = 505,
};
enum SvcError {
@@ -494,7 +495,269 @@ enum NcmError {
NcmError_WriteToReadOnlyContentStorage = 0x17C05,
};
#define R_SUCCEED() return 0
enum class SphairaResult : Result {
TransferCancelled,
StreamBadSeek,
FsTooManyEntries,
FsNewPathTooLarge,
FsInvalidType,
FsEmpty,
FsAlreadyRoot,
FsNoCurrentPath,
FsBrokenCurrentPath,
FsIndexOutOfBounds,
FsFsNotActive,
FsNewPathEmpty,
FsLoadingCancelled,
FsBrokenRoot,
FsUnknownStdioError,
FsReadOnly,
FsNotActive,
FsFailedStdioStat,
FsFailedStdioOpendir,
NroBadMagic,
NroBadSize,
AppFailedMusicDownload,
CurlFailedEasyInit,
DumpFailedNetworkUpload,
UnzOpen2_64,
UnzGetGlobalInfo64,
UnzLocateFile,
UnzGoToFirstFile,
UnzGoToNextFile,
UnzOpenCurrentFile,
UnzGetCurrentFileInfo64,
UnzReadCurrentFile,
ZipOpen2_64,
ZipOpenNewFileInZip,
ZipWriteInFileInZip,
FileBrowserFailedUpload,
FileBrowserDirNotDaybreak,
AppstoreFailedZipDownload,
AppstoreFailedMd5,
AppstoreFailedParseManifest,
GameBadReadForDump,
GameEmptyMetaEntries,
GameMultipleKeysFound,
GameNoNspEntriesBuilt,
KeyMissingNcaKeyArea,
KeyMissingTitleKek,
KeyMissingMasterKey,
KeyFailedDecyptETicketDeviceKey,
NcaFailedNcaHeaderHashVerify,
NcaBadSigKeyGen,
GcBadReadForDump,
GcEmptyGamecard,
GcBadXciMagic,
GcBadXciRomSize,
GcFailedToGetSecurityInfo,
GhdlEmptyAsset,
GhdlFailedToDownloadAsset,
GhdlFailedToDownloadAssetJson,
ThemezerFailedToDownloadThemeMeta,
ThemezerFailedToDownloadTheme,
MainFailedToDownloadUpdate,
UsbDsBadDeviceSpeed,
NspBadMagic,
XciBadMagic,
EsBadTitleKeyType,
EsPersonalisedTicketDeviceIdMissmatch,
EsFailedDecryptPersonalisedTicket,
EsBadDecryptedPersonalisedTicketSize,
EsBadTicketSize,
// found ticket has missmatching rights_id from it's name.
EsInvalidTicketBadRightsId,
EsInvalidTicketFromatVersion,
EsInvalidTicketKeyType,
EsInvalidTicketKeyRevision,
OwoBadArgs,
UsbCancelled,
UsbBadMagic,
UsbBadVersion,
UsbBadCount,
UsbBadTransferSize,
UsbBadTotalSize,
UsbUploadBadMagic,
UsbUploadExit,
UsbUploadBadCount,
UsbUploadBadTransferSize,
UsbUploadBadTotalSize,
UsbUploadBadCommand,
// unkown container for the source provided.
YatiContainerNotFound,
// nca required by the cnmt but not found in collection.
YatiNcaNotFound,
YatiInvalidNcaReadSize,
YatiInvalidNcaSigKeyGen,
YatiInvalidNcaMagic,
YatiInvalidNcaSignature0,
YatiInvalidNcaSignature1,
// invalid sha256 over the entire nca.
YatiInvalidNcaSha256,
// section could not be found.
YatiNczSectionNotFound,
// section count == 0.
YatiInvalidNczSectionCount,
// block could not be found.
YatiNczBlockNotFound,
// block version != 2.
YatiInvalidNczBlockVersion,
// block type != 1.
YatiInvalidNczBlockType,
// block count == 0.
YatiInvalidNczBlockTotal,
// block size exponent < 14 || > 32.
YatiInvalidNczBlockSizeExponent,
// zstd error while decompressing ncz.
YatiInvalidNczZstdError,
// nca has rights_id but matching ticket wasn't found.
YatiTicketNotFound,
// found ticket has missmatching rights_id from it's name.
YatiInvalidTicketBadRightsId,
// cert not found for the ticket.
YatiCertNotFound,
// unable to fetch header from ncm database.
YatiNcmDbCorruptHeader,
// unable to total infos from ncm database.
YatiNcmDbCorruptInfos,
};
#define MAKE_SPHAIRA_RESULT_ENUM(x) Result_##x = MAKERESULT(Module_Sphaira, (Result)SphairaResult::x)
enum : Result {
MAKE_SPHAIRA_RESULT_ENUM(TransferCancelled),
MAKE_SPHAIRA_RESULT_ENUM(StreamBadSeek),
MAKE_SPHAIRA_RESULT_ENUM(FsTooManyEntries),
MAKE_SPHAIRA_RESULT_ENUM(FsNewPathTooLarge),
MAKE_SPHAIRA_RESULT_ENUM(FsInvalidType),
MAKE_SPHAIRA_RESULT_ENUM(FsEmpty),
MAKE_SPHAIRA_RESULT_ENUM(FsAlreadyRoot),
MAKE_SPHAIRA_RESULT_ENUM(FsNoCurrentPath),
MAKE_SPHAIRA_RESULT_ENUM(FsBrokenCurrentPath),
MAKE_SPHAIRA_RESULT_ENUM(FsIndexOutOfBounds),
MAKE_SPHAIRA_RESULT_ENUM(FsFsNotActive),
MAKE_SPHAIRA_RESULT_ENUM(FsNewPathEmpty),
MAKE_SPHAIRA_RESULT_ENUM(FsLoadingCancelled),
MAKE_SPHAIRA_RESULT_ENUM(FsBrokenRoot),
MAKE_SPHAIRA_RESULT_ENUM(FsUnknownStdioError),
MAKE_SPHAIRA_RESULT_ENUM(FsReadOnly),
MAKE_SPHAIRA_RESULT_ENUM(FsNotActive),
MAKE_SPHAIRA_RESULT_ENUM(FsFailedStdioStat),
MAKE_SPHAIRA_RESULT_ENUM(FsFailedStdioOpendir),
MAKE_SPHAIRA_RESULT_ENUM(NroBadMagic),
MAKE_SPHAIRA_RESULT_ENUM(NroBadSize),
MAKE_SPHAIRA_RESULT_ENUM(AppFailedMusicDownload),
MAKE_SPHAIRA_RESULT_ENUM(CurlFailedEasyInit),
MAKE_SPHAIRA_RESULT_ENUM(DumpFailedNetworkUpload),
MAKE_SPHAIRA_RESULT_ENUM(UnzOpen2_64),
MAKE_SPHAIRA_RESULT_ENUM(UnzGetGlobalInfo64),
MAKE_SPHAIRA_RESULT_ENUM(UnzLocateFile),
MAKE_SPHAIRA_RESULT_ENUM(UnzGoToFirstFile),
MAKE_SPHAIRA_RESULT_ENUM(UnzGoToNextFile),
MAKE_SPHAIRA_RESULT_ENUM(UnzOpenCurrentFile),
MAKE_SPHAIRA_RESULT_ENUM(UnzGetCurrentFileInfo64),
MAKE_SPHAIRA_RESULT_ENUM(UnzReadCurrentFile),
MAKE_SPHAIRA_RESULT_ENUM(ZipOpen2_64),
MAKE_SPHAIRA_RESULT_ENUM(ZipOpenNewFileInZip),
MAKE_SPHAIRA_RESULT_ENUM(ZipWriteInFileInZip),
MAKE_SPHAIRA_RESULT_ENUM(FileBrowserFailedUpload),
MAKE_SPHAIRA_RESULT_ENUM(FileBrowserDirNotDaybreak),
MAKE_SPHAIRA_RESULT_ENUM(AppstoreFailedZipDownload),
MAKE_SPHAIRA_RESULT_ENUM(AppstoreFailedMd5),
MAKE_SPHAIRA_RESULT_ENUM(AppstoreFailedParseManifest),
MAKE_SPHAIRA_RESULT_ENUM(GameBadReadForDump),
MAKE_SPHAIRA_RESULT_ENUM(GameEmptyMetaEntries),
MAKE_SPHAIRA_RESULT_ENUM(GameMultipleKeysFound),
MAKE_SPHAIRA_RESULT_ENUM(GameNoNspEntriesBuilt),
MAKE_SPHAIRA_RESULT_ENUM(KeyMissingNcaKeyArea),
MAKE_SPHAIRA_RESULT_ENUM(KeyMissingTitleKek),
MAKE_SPHAIRA_RESULT_ENUM(KeyMissingMasterKey),
MAKE_SPHAIRA_RESULT_ENUM(KeyFailedDecyptETicketDeviceKey),
MAKE_SPHAIRA_RESULT_ENUM(NcaFailedNcaHeaderHashVerify),
MAKE_SPHAIRA_RESULT_ENUM(NcaBadSigKeyGen),
MAKE_SPHAIRA_RESULT_ENUM(GcBadReadForDump),
MAKE_SPHAIRA_RESULT_ENUM(GcEmptyGamecard),
MAKE_SPHAIRA_RESULT_ENUM(GcBadXciMagic),
MAKE_SPHAIRA_RESULT_ENUM(GcBadXciRomSize),
MAKE_SPHAIRA_RESULT_ENUM(GcFailedToGetSecurityInfo),
MAKE_SPHAIRA_RESULT_ENUM(GhdlEmptyAsset),
MAKE_SPHAIRA_RESULT_ENUM(GhdlFailedToDownloadAsset),
MAKE_SPHAIRA_RESULT_ENUM(GhdlFailedToDownloadAssetJson),
MAKE_SPHAIRA_RESULT_ENUM(ThemezerFailedToDownloadThemeMeta),
MAKE_SPHAIRA_RESULT_ENUM(ThemezerFailedToDownloadTheme),
MAKE_SPHAIRA_RESULT_ENUM(MainFailedToDownloadUpdate),
MAKE_SPHAIRA_RESULT_ENUM(UsbDsBadDeviceSpeed),
MAKE_SPHAIRA_RESULT_ENUM(NspBadMagic),
MAKE_SPHAIRA_RESULT_ENUM(XciBadMagic),
MAKE_SPHAIRA_RESULT_ENUM(EsBadTitleKeyType),
MAKE_SPHAIRA_RESULT_ENUM(EsPersonalisedTicketDeviceIdMissmatch),
MAKE_SPHAIRA_RESULT_ENUM(EsFailedDecryptPersonalisedTicket),
MAKE_SPHAIRA_RESULT_ENUM(EsBadDecryptedPersonalisedTicketSize),
MAKE_SPHAIRA_RESULT_ENUM(EsBadTicketSize),
MAKE_SPHAIRA_RESULT_ENUM(EsInvalidTicketBadRightsId),
MAKE_SPHAIRA_RESULT_ENUM(EsInvalidTicketFromatVersion),
MAKE_SPHAIRA_RESULT_ENUM(EsInvalidTicketKeyType),
MAKE_SPHAIRA_RESULT_ENUM(EsInvalidTicketKeyRevision),
MAKE_SPHAIRA_RESULT_ENUM(OwoBadArgs),
MAKE_SPHAIRA_RESULT_ENUM(UsbCancelled),
MAKE_SPHAIRA_RESULT_ENUM(UsbBadMagic),
MAKE_SPHAIRA_RESULT_ENUM(UsbBadVersion),
MAKE_SPHAIRA_RESULT_ENUM(UsbBadCount),
MAKE_SPHAIRA_RESULT_ENUM(UsbBadTransferSize),
MAKE_SPHAIRA_RESULT_ENUM(UsbBadTotalSize),
MAKE_SPHAIRA_RESULT_ENUM(UsbUploadBadMagic),
MAKE_SPHAIRA_RESULT_ENUM(UsbUploadExit),
MAKE_SPHAIRA_RESULT_ENUM(UsbUploadBadCount),
MAKE_SPHAIRA_RESULT_ENUM(UsbUploadBadTransferSize),
MAKE_SPHAIRA_RESULT_ENUM(UsbUploadBadTotalSize),
MAKE_SPHAIRA_RESULT_ENUM(UsbUploadBadCommand),
MAKE_SPHAIRA_RESULT_ENUM(YatiContainerNotFound),
MAKE_SPHAIRA_RESULT_ENUM(YatiNcaNotFound),
MAKE_SPHAIRA_RESULT_ENUM(YatiInvalidNcaReadSize),
MAKE_SPHAIRA_RESULT_ENUM(YatiInvalidNcaSigKeyGen),
MAKE_SPHAIRA_RESULT_ENUM(YatiInvalidNcaMagic),
MAKE_SPHAIRA_RESULT_ENUM(YatiInvalidNcaSignature0),
MAKE_SPHAIRA_RESULT_ENUM(YatiInvalidNcaSignature1),
MAKE_SPHAIRA_RESULT_ENUM(YatiInvalidNcaSha256),
MAKE_SPHAIRA_RESULT_ENUM(YatiNczSectionNotFound),
MAKE_SPHAIRA_RESULT_ENUM(YatiInvalidNczSectionCount),
MAKE_SPHAIRA_RESULT_ENUM(YatiNczBlockNotFound),
MAKE_SPHAIRA_RESULT_ENUM(YatiInvalidNczBlockVersion),
MAKE_SPHAIRA_RESULT_ENUM(YatiInvalidNczBlockType),
MAKE_SPHAIRA_RESULT_ENUM(YatiInvalidNczBlockTotal),
MAKE_SPHAIRA_RESULT_ENUM(YatiInvalidNczBlockSizeExponent),
MAKE_SPHAIRA_RESULT_ENUM(YatiInvalidNczZstdError),
MAKE_SPHAIRA_RESULT_ENUM(YatiTicketNotFound),
MAKE_SPHAIRA_RESULT_ENUM(YatiInvalidTicketBadRightsId),
MAKE_SPHAIRA_RESULT_ENUM(YatiCertNotFound),
MAKE_SPHAIRA_RESULT_ENUM(YatiNcmDbCorruptHeader),
MAKE_SPHAIRA_RESULT_ENUM(YatiNcmDbCorruptInfos),
};
#undef MAKE_SPHAIRA_RESULT_ENUM
#define R_SUCCEED() return (Result)0
#define R_THROW(_rc) return _rc
@@ -521,8 +784,8 @@ enum NcmError {
#define ANONYMOUS_VARIABLE(pref) CONCATENATE(pref, __COUNTER__)
#define ON_SCOPE_EXIT(_f) std::experimental::scope_exit ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE_){[&] { _f; }};
#define ON_SCOPE_FAIL(_f) std::experimental::scope_exit ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE_){[&] { if (R_FAILED(rc)) { _f; } }};
#define ON_SCOPE_SUCCESS(_f) std::experimental::scope_exit ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE_){[&] { if (R_SUCCEEDED(rc)) { _f; } }};
// #define ON_SCOPE_FAIL(_f) std::experimental::scope_exit ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE_){[&] { if (R_FAILED(rc)) { _f; } }};
// #define ON_SCOPE_SUCCESS(_f) std::experimental::scope_exit ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE_){[&] { if (R_SUCCEEDED(rc)) { _f; } }};
// threading helpers.
#define PRIO_PREEMPTIVE 0x3B
@@ -535,4 +798,6 @@ enum NcmError {
#define THREAD_AFFINITY_ALL (THREAD_AFFINITY_CORE0|THREAD_AFFINITY_CORE1|THREAD_AFFINITY_CORE2)
// mutex helpers.
#define SCOPED_MUTEX(mutex) mutexLock(mutex); ON_SCOPE_EXIT(mutexUnlock(mutex))
#define SCOPED_MUTEX(mutex) \
mutexLock(mutex); \
ON_SCOPE_EXIT(mutexUnlock(mutex))

View File

@@ -1,6 +1,7 @@
#pragma once
#include "fs.hpp"
#include "location.hpp"
#include <switch.h>
#include <vector>
#include <memory>
@@ -30,6 +31,17 @@ enum DumpLocationFlag {
DumpLocationFlag_All = DumpLocationFlag_SdCard | DumpLocationFlag_UsbS2S | DumpLocationFlag_DevNull | DumpLocationFlag_Stdio | DumpLocationFlag_Network,
};
struct DumpEntry {
DumpLocationType type;
s32 index;
};
struct DumpLocation {
DumpEntry entry{};
location::Entries network{};
location::StdioEntries stdio{};
};
struct BaseSource {
virtual ~BaseSource() = default;
virtual Result Read(const std::string& path, void* buf, s64 off, s64 size, u64* bytes_read) = 0;
@@ -40,7 +52,13 @@ struct BaseSource {
// called after dump has finished.
using OnExit = std::function<void(Result rc)>;
using OnLocation = std::function<void(const DumpLocation& loc)>;
// prompts the user to select dump location, calls on_loc on success with the selected location.
void DumpGetLocation(const std::string& title, u32 location_flags, OnLocation on_loc);
// dumps to a fetched location using DumpGetLocation().
void Dump(std::shared_ptr<BaseSource> source, const DumpLocation& location, const std::vector<fs::FsPath>& paths, OnExit on_exit);
// DumpGetLocation() + Dump() all in one.
void Dump(std::shared_ptr<BaseSource> source, const std::vector<fs::FsPath>& paths, OnExit on_exit = [](Result){}, u32 location_flags = DumpLocationFlag_All);
} // namespace sphaira::dump

View File

@@ -7,7 +7,6 @@
#include <string>
#include <switch.h>
#include <nxlink.h>
#include <haze.h>
#include "download.hpp"
namespace sphaira::evman {
@@ -24,7 +23,6 @@ struct ExitEventData {
using EventData = std::variant<
LaunchNroEventData,
ExitEventData,
HazeCallbackData,
NxlinkCallbackData,
curl::DownloadEventData
>;

View File

@@ -12,10 +12,14 @@ namespace fs {
struct FsPath {
FsPath() = default;
constexpr FsPath(const auto& str) { From(str); }
constexpr FsPath(const FsPath& p) { From(p); }
constexpr FsPath(const char* str) { From(str); }
constexpr FsPath(const std::string& str) { From(str); }
constexpr FsPath(const std::string_view& str) { From(str); }
constexpr void From(const FsPath& p) {
*this = p;
From(p.s);
}
constexpr void From(const char* str) {
@@ -40,6 +44,10 @@ struct FsPath {
return s;
}
constexpr auto starts_with(std::string_view str) const -> bool {
return !strncasecmp(s, str.data(), str.length());
}
constexpr auto empty() const {
return s[0] == '\0';
}
@@ -65,6 +73,11 @@ struct FsPath {
constexpr char& operator[](std::size_t idx) { return s[idx]; }
constexpr const char& operator[](std::size_t idx) const { return s[idx]; }
constexpr FsPath& operator=(const FsPath& p) noexcept {
From(p.s);
return *this;
}
constexpr FsPath operator+(const FsPath& v) const noexcept {
FsPath r{*this};
return r += v;
@@ -186,15 +199,14 @@ struct File {
FsFile m_native{};
std::FILE* m_stdio{};
s64 m_stdio_off{};
// sadly, fatfs doesn't support fstat, so we have to manually
// stat the file to get it's size.
FsPath m_path{};
u32 m_mode{};
};
struct Dir {
~Dir();
Result GetEntryCount(s64* out);
Result Read(s64 *total_entries, size_t max_entries, FsDirectoryEntry *buf);
Result ReadAll(std::vector<FsDirectoryEntry>& buf);
void Close();
@@ -261,22 +273,6 @@ Result FileGetSizeAndTimestamp(fs::Fs* fs, const FsPath& path, FsTimeStampRaw* t
Result IsDirEmpty(fs::Fs* m_fs, const fs::FsPath& path, bool* out);
struct Fs {
static constexpr inline u32 FsModule = 505;
static constexpr inline Result ResultTooManyEntries = MAKERESULT(FsModule, 1);
static constexpr inline Result ResultNewPathTooLarge = MAKERESULT(FsModule, 2);
static constexpr inline Result ResultInvalidType = MAKERESULT(FsModule, 3);
static constexpr inline Result ResultEmpty = MAKERESULT(FsModule, 4);
static constexpr inline Result ResultAlreadyRoot = MAKERESULT(FsModule, 5);
static constexpr inline Result ResultNoCurrentPath = MAKERESULT(FsModule, 6);
static constexpr inline Result ResultBrokenCurrentPath = MAKERESULT(FsModule, 7);
static constexpr inline Result ResultIndexOutOfBounds = MAKERESULT(FsModule, 8);
static constexpr inline Result ResultFsNotActive = MAKERESULT(FsModule, 9);
static constexpr inline Result ResultNewPathEmpty = MAKERESULT(FsModule, 10);
static constexpr inline Result ResultLoadingCancelled = MAKERESULT(FsModule, 11);
static constexpr inline Result ResultBrokenRoot = MAKERESULT(FsModule, 12);
static constexpr inline Result ResultUnknownStdioError = MAKERESULT(FsModule, 13);
static constexpr inline Result ResultReadOnly = MAKERESULT(FsModule, 14);
Fs(bool ignore_read_only = true) : m_ignore_read_only{ignore_read_only} {}
virtual ~Fs() = default;
@@ -292,6 +288,7 @@ struct Fs {
virtual Result GetEntryType(const FsPath& path, FsDirEntryType* out) = 0;
virtual Result GetFileTimeStampRaw(const FsPath& path, FsTimeStampRaw *out) = 0;
virtual Result SetTimestamp(const FsPath& path, const FsTimeStampRaw* ts) = 0;
virtual Result Commit() = 0;
virtual bool FileExists(const FsPath& path) = 0;
virtual bool DirExists(const FsPath& path) = 0;
virtual bool IsNative() const = 0;
@@ -367,6 +364,9 @@ struct FsStdio : Fs {
Result SetTimestamp(const FsPath& path, const FsTimeStampRaw *ts) override {
return fs::SetTimestamp(path, ts);
}
Result Commit() override {
R_SUCCEED();
}
bool FileExists(const FsPath& path) override {
return fs::FileExists(path);
}
@@ -402,10 +402,6 @@ struct FsNative : Fs {
}
}
Result Commit() {
return fsFsCommit(&m_fs);
}
Result GetFreeSpace(const FsPath& path, s64* out) {
return fsFsGetFreeSpace(&m_fs, path, out);
}
@@ -414,36 +410,6 @@ struct FsNative : Fs {
return fsFsGetTotalSpace(&m_fs, path, out);
}
// Result OpenDirectory(const FsPath& path, u32 mode, FsDir *out) {
// return fsFsOpenDirectory(&m_fs, path, mode, out);
// }
// void DirClose(FsDir *d) {
// fsDirClose(d);
// }
// Result DirGetEntryCount(FsDir *d, s64* out) {
// return fsDirGetEntryCount(d, out);
// }
// Result DirGetEntryCount(const FsPath& path, u32 mode, s64* out) {
// FsDir d;
// R_TRY(OpenDirectory(path, mode, &d));
// ON_SCOPE_EXIT(DirClose(&d));
// return DirGetEntryCount(&d, out);
// }
// Result DirRead(FsDir *d, s64 *total_entries, size_t max_entries, FsDirectoryEntry *buf) {
// return fsDirRead(d, total_entries, max_entries, buf);
// }
// Result DirRead(const FsPath& path, u32 mode, s64 *total_entries, size_t max_entries, FsDirectoryEntry *buf) {
// FsDir d;
// R_TRY(OpenDirectory(path, mode, &d));
// ON_SCOPE_EXIT(DirClose(&d));
// return DirRead(&d, total_entries, max_entries, buf);
// }
virtual bool IsFsActive() {
return serviceIsActive(&m_fs.s);
}
@@ -488,6 +454,9 @@ struct FsNative : Fs {
Result SetTimestamp(const FsPath& path, const FsTimeStampRaw *ts) override {
return fs::SetTimestamp(&m_fs, path, ts);
}
Result Commit() override {
return fsFsCommit(&m_fs);
}
bool FileExists(const FsPath& path) override {
return fs::FileExists(&m_fs, path);
}
@@ -551,11 +520,15 @@ struct FsNativeGameCard final : FsNative {
};
struct FsNativeSave final : FsNative {
FsNativeSave(FsSaveDataSpaceId save_data_space_id, const FsSaveDataAttribute *attr, bool read_only) {
if (read_only) {
m_open_result = fsOpenReadOnlySaveDataFileSystem(&m_fs, save_data_space_id, attr);
FsNativeSave(FsSaveDataType data_type, FsSaveDataSpaceId save_data_space_id, const FsSaveDataAttribute *attr, bool read_only) {
if (data_type == FsSaveDataType_System || data_type == FsSaveDataType_SystemBcat) {
m_open_result = fsOpenSaveDataFileSystemBySystemSaveDataId(&m_fs, FsSaveDataSpaceId_System, attr);
} else {
m_open_result = fsOpenSaveDataFileSystem(&m_fs, save_data_space_id, attr);
if (read_only) {
m_open_result = fsOpenReadOnlySaveDataFileSystem(&m_fs, save_data_space_id, attr);
} else {
m_open_result = fsOpenSaveDataFileSystem(&m_fs, save_data_space_id, attr);
}
}
}
};

View File

@@ -7,11 +7,11 @@ 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)>;
using OnInstallStart = std::function<bool(const char* path)>;
using OnInstallWrite = std::function<bool(const void* buf, size_t size)>;
using OnInstallClose = std::function<void()>;
void InitInstallMode(void* user, OnInstallStart on_start, OnInstallWrite on_write, OnInstallClose on_close);
void InitInstallMode(OnInstallStart on_start, OnInstallWrite on_write, OnInstallClose on_close);
void DisableInstallMode();
unsigned GetPort();

View File

@@ -0,0 +1,17 @@
#pragma once
#include <functional>
namespace sphaira::haze {
bool Init();
void Exit();
using OnInstallStart = std::function<bool(const char* path)>;
using OnInstallWrite = std::function<bool(const void* buf, size_t size)>;
using OnInstallClose = std::function<void()>;
void InitInstallMode(OnInstallStart on_start, OnInstallWrite on_write, OnInstallClose on_close);
void DisableInstallMode();
} // namespace sphaira::haze

View File

@@ -12,6 +12,8 @@ extern "C" {
bool log_file_init();
bool log_nxlink_init();
void log_file_exit();
bool log_is_init();
void log_nxlink_exit();
void log_write(const char* s, ...) __attribute__ ((format (printf, 1, 2)));
void log_write_arg(const char* s, va_list* v);

View File

@@ -20,17 +20,6 @@ struct OwoConfig {
std::vector<u8> program_nca{};
};
enum {
Module_Owo = 424,
};
enum OwoError {
OwoError_BadArgs = MAKERESULT(Module_Owo, 1),
};
// fwd
// struct ui::ProgressBox;
auto install_forwarder(OwoConfig& config, NcmStorageId storage_id) -> Result;
auto install_forwarder(ui::ProgressBox* pbox, OwoConfig& config, NcmStorageId storage_id) -> Result;

View File

@@ -40,17 +40,17 @@ Result TransferPull(ui::ProgressBox* pbox, s64 size, ReadCallback rfunc, StartCa
// helper for extract zips.
// this will multi-thread unzip if size >= 512KiB, otherwise it'll single pass.
Result TransferUnzip(ui::ProgressBox* pbox, void* zfile, fs::Fs* fs, const fs::FsPath& path, s64 size, u32 crc32 = 0);
Result TransferUnzip(ui::ProgressBox* pbox, void* zfile, fs::Fs* fs, const fs::FsPath& path, s64 size, u32 crc32 = 0, Mode mode = Mode::SingleThreadedIfSmaller);
// same as above but for zipping files.
Result TransferZip(ui::ProgressBox* pbox, void* zfile, fs::Fs* fs, const fs::FsPath& path, u32* crc32 = nullptr);
Result TransferZip(ui::ProgressBox* pbox, void* zfile, fs::Fs* fs, const fs::FsPath& path, u32* crc32 = nullptr, Mode mode = Mode::SingleThreadedIfSmaller);
// passes the name inside the zip an final output path.
using UnzipAllFilter = std::function<bool(const fs::FsPath& name, fs::FsPath& path)>;
// helper all-in-one unzip function that unzips a zip (either open or path provided).
// the filter function can be used to modify the path and filter out unwanted files.
Result TransferUnzipAll(ui::ProgressBox* pbox, void* zfile, fs::Fs* fs, const fs::FsPath& base_path, UnzipAllFilter filter = nullptr);
Result TransferUnzipAll(ui::ProgressBox* pbox, const fs::FsPath& zip_out, fs::Fs* fs, const fs::FsPath& base_path, UnzipAllFilter filter = nullptr);
Result TransferUnzipAll(ui::ProgressBox* pbox, void* zfile, fs::Fs* fs, const fs::FsPath& base_path, UnzipAllFilter filter = nullptr, Mode mode = Mode::SingleThreadedIfSmaller);
Result TransferUnzipAll(ui::ProgressBox* pbox, const fs::FsPath& zip_out, fs::Fs* fs, const fs::FsPath& base_path, UnzipAllFilter filter = nullptr, Mode mode = Mode::SingleThreadedIfSmaller);
} // namespace sphaira::thread

View File

@@ -16,6 +16,7 @@ public:
private:
std::optional<Result> m_code{};
std::string m_message{};
std::string m_code_message{};
};
} // namespace sphaira::ui

View File

@@ -2,6 +2,7 @@
#include "ui/menus/menu_base.hpp"
#include "ui/scrolling_text.hpp"
#include "ui/progress_box.hpp"
#include "ui/list.hpp"
#include "fs.hpp"
#include "option.hpp"
@@ -154,6 +155,8 @@ struct FsDirCollection {
using FsDirCollections = std::vector<FsDirCollection>;
void SignalChange();
struct Menu;
struct FsView final : Widget {
@@ -181,6 +184,7 @@ struct FsView final : Widget {
void SetSide(ViewSide side);
static Result DeleteAllCollections(ProgressBox* pbox, fs::Fs* fs, const FsDirCollections& collections, u32 mode = FsDirOpenMode_ReadDirs|FsDirOpenMode_ReadFiles);
static auto get_collection(fs::Fs* fs, const fs::FsPath& path, const fs::FsPath& parent_name, FsDirCollection& out, bool inc_file, bool inc_dir, bool inc_size) -> Result;
static auto get_collections(fs::Fs* fs, const fs::FsPath& path, const fs::FsPath& parent_name, FsDirCollections& out, bool inc_size = false) -> Result;
@@ -244,7 +248,7 @@ private:
}
void Sort();
void SortAndFindLastFile();
void SortAndFindLastFile(bool scan = false);
void SetIndexFromLastFile(const LastFile& last_file);
void OnDeleteCallback();

View File

@@ -1,56 +1,20 @@
#pragma once
#include "ui/menus/menu_base.hpp"
#include "yati/source/stream.hpp"
#include "ui/menus/install_stream_menu_base.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 {
struct Menu final : stream::Menu {
Menu(u32 flags);
~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};
void OnDisableInstallMode() override;
private:
const char* m_user{};
const char* m_pass{};
unsigned m_port{};

View File

@@ -25,6 +25,7 @@ enum OrderType {
using LayoutType = grid::LayoutType;
auto GetNroEntries() -> std::span<const NroEntry>;
void SignalChange();
struct Menu final : grid::Menu {
Menu();
@@ -47,7 +48,7 @@ private:
void InstallHomebrew();
void ScanHomebrew();
void Sort();
void SortAndFindLastFile();
void SortAndFindLastFile(bool scan = false);
void FreeEntries();
void OnLayoutChange();
@@ -61,6 +62,7 @@ private:
std::vector<NroEntry> m_entries{};
s64 m_index{}; // where i am in the array
std::unique_ptr<List> m_list{};
bool m_dirty{};
option::OptionLong m_sort{INI_SECTION, "sort", SortType::SortType_AlphabeticalStar};
option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Descending};

View File

@@ -0,0 +1,64 @@
#pragma once
#include "ui/menus/menu_base.hpp"
#include "yati/source/stream.hpp"
namespace sphaira::ui::menu::stream {
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,
};
using OnInstallStart = std::function<bool(const char* path)>;
using OnInstallWrite = std::function<bool(const void* buf, size_t size)>;
using OnInstallClose = std::function<void()>;
struct Stream final : yati::source::Stream {
Stream(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();
auto& GetPath() const { return m_path; }
private:
fs::FsPath m_path{};
std::stop_token m_token{};
std::vector<u8> m_buffer{};
CondVar m_can_read{};
public:
Mutex m_mutex{};
bool m_active{};
};
struct Menu : MenuBase {
Menu(const std::string& title, u32 flags);
virtual ~Menu();
virtual void Update(Controller* controller, TouchInfo* touch);
virtual void Draw(NVGcontext* vg, Theme* theme);
virtual void OnDisableInstallMode() = 0;
protected:
bool OnInstallStart(const char* path);
bool OnInstallWrite(const void* buf, size_t size);
void OnInstallClose();
private:
std::shared_ptr<Stream> m_source{};
Thread m_thread{};
Mutex m_mutex{};
State m_state{State::None};
};
} // namespace sphaira::ui::menu::stream

View File

@@ -0,0 +1,19 @@
#pragma once
#include "ui/menus/install_stream_menu_base.hpp"
namespace sphaira::ui::menu::mtp {
struct Menu final : stream::Menu {
Menu(u32 flags);
~Menu();
auto GetShortTitle() const -> const char* override { return "MTP"; };
void Update(Controller* controller, TouchInfo* touch) override;
void OnDisableInstallMode() override;
private:
bool m_was_mtp_enabled{};
};
} // namespace sphaira::ui::menu::mtp

View File

@@ -4,6 +4,7 @@
#include "ui/list.hpp"
#include "fs.hpp"
#include "option.hpp"
#include "dumper.hpp"
#include <memory>
#include <vector>
#include <span>
@@ -39,12 +40,6 @@ struct Entry final : FsSaveDataInfo {
}
};
struct AccountEntry {
AccountUid uid;
AccountProfile profile;
AccountProfileBase base;
};
struct ThreadResultData {
u64 id{};
std::shared_ptr<NsApplicationControlData> control{};
@@ -87,6 +82,8 @@ enum OrderType {
using LayoutType = grid::LayoutType;
void SignalChange();
struct Menu final : grid::Menu {
Menu(u32 flags);
~Menu();
@@ -130,6 +127,10 @@ private:
void BackupSaves(std::vector<std::reference_wrapper<Entry>>& entries);
void RestoreSave();
auto BuildSavePath(const Entry& e, bool is_auto) const -> fs::FsPath;
Result RestoreSaveInternal(ProgressBox* pbox, const Entry& e, const fs::FsPath& path) const;
Result BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& location, const Entry& e, bool compressed, bool is_auto = false) const;
private:
static constexpr inline const char* INI_SECTION = "saves";
@@ -140,8 +141,9 @@ private:
bool m_is_reversed{};
bool m_dirty{};
std::vector<AccountEntry> m_accounts{};
std::vector<AccountProfileBase> m_accounts{};
s64 m_account_index{};
u8 m_data_type{FsSaveDataType_Account};
ThreadData m_thread_data{};
Thread m_thread{};

View File

@@ -30,7 +30,8 @@ void drawTextArgs(NVGcontext*, float x, float y, float size, int align, const NV
void drawTextBox(NVGcontext*, float x, float y, float size, float bound, const NVGcolor& c, const char* str, int align = NVG_ALIGN_LEFT | NVG_ALIGN_TOP, const char* end = nullptr);
void textBounds(NVGcontext*, float x, float y, float *bounds, const char* str, ...) __attribute__ ((format (printf, 5, 6)));
void textBounds(NVGcontext*, float x, float y, float *bounds, const char* str);
void textBoundsArgs(NVGcontext*, float x, float y, float *bounds, const char* str, ...) __attribute__ ((format (printf, 5, 6)));
auto getButton(Button button) -> const char*;
void drawScrollbar(NVGcontext*, const Theme*, u32 index_off, u32 count, u32 max_per_page);

View File

@@ -39,9 +39,9 @@ struct ProgressBox final : Widget {
auto ShouldExitResult() -> Result;
// helper functions
auto CopyFile(fs::Fs* fs_src, fs::Fs* fs_dst, const fs::FsPath& src, const fs::FsPath& dst) -> Result;
auto CopyFile(fs::Fs* fs, const fs::FsPath& src, const fs::FsPath& dst) -> Result;
auto CopyFile(const fs::FsPath& src, const fs::FsPath& dst) -> Result;
auto CopyFile(fs::Fs* fs_src, fs::Fs* fs_dst, const fs::FsPath& src, const fs::FsPath& dst, bool single_threaded = false) -> Result;
auto CopyFile(fs::Fs* fs, const fs::FsPath& src, const fs::FsPath& dst, bool single_threaded = false) -> Result;
auto CopyFile(const fs::FsPath& src, const fs::FsPath& dst, bool single_threaded = false) -> Result;
void Yield();
auto GetCpuId() const {

View File

@@ -62,6 +62,12 @@ struct Widget : public Object {
m_actions.clear();
}
void RemoveActions(const Actions& actions) {
for (auto& e : actions) {
RemoveAction(e.first);
}
}
auto FireAction(Button button, u8 type = ActionType::DOWN) -> bool;
void SetPop(bool pop = true) {

View File

@@ -8,12 +8,6 @@
namespace sphaira::usb {
struct Base {
enum { USBModule = 523 };
enum : Result {
Result_Cancelled = MAKERESULT(USBModule, 100),
};
Base(u64 transfer_timeout);
virtual ~Base();

View File

@@ -10,17 +10,6 @@
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();

View File

@@ -2,6 +2,11 @@
#include "base.hpp"
// TODO: remove these when libnx pr is merged.
enum { UsbDeviceSpeed_None = 0x0 };
enum { UsbDeviceSpeed_Low = 0x1 };
Result usbDsGetSpeed(UsbDeviceSpeed *out);
namespace sphaira::usb {
// Device Host

View File

@@ -2,49 +2,132 @@
#include <switch.h>
#include <span>
#include <vector>
#include "ncm.hpp"
#include "keys.hpp"
namespace sphaira::es {
enum { TicketModule = 507 };
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 TitleKeyType : u8 {
TitleKeyType_Common = 0,
TitleKeyType_Personalized = 1,
};
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 SigType : u32 {
SigType_Rsa4096Sha1 = 65536,
SigType_Rsa2048Sha1 = 65537,
SigType_Ecc480Sha1 = 65538,
SigType_Rsa4096Sha256 = 65539,
SigType_Rsa2048Sha256 = 65540,
SigType_Ecc480Sha256 = 65541,
SigType_Hmac160Sha1 = 65542
};
enum TicketTitleKeyType {
TicketTitleKeyType_Common = 0,
TicketTitleKeyType_Personalized = 1,
enum PubKeyType : u32 {
PubKeyType_Rsa4096 = 0,
PubKeyType_Rsa2048 = 1,
PubKeyType_Ecc480 = 2
};
enum TicketPropertiesBitfield {
TicketPropertiesBitfield_None = 0,
// temporary ticket, removed on restart
TicketPropertiesBitfield_Temporary = 1 << 4,
struct SignatureBlockRsa4096 {
SigType sig_type;
u8 sign[0x200];
u8 reserved_1[0x3C];
};
static_assert(sizeof(SignatureBlockRsa4096) == 0x240);
struct SignatureBlockRsa2048 {
SigType sig_type;
u8 sign[0x100];
u8 reserved_1[0x3C];
};
static_assert(sizeof(SignatureBlockRsa2048) == 0x140);
struct SignatureBlockEcc480 {
SigType sig_type;
u8 sign[0x3C];
u8 reserved_1[0x40];
};
static_assert(sizeof(SignatureBlockEcc480) == 0x80);
struct SignatureBlockHmac160 {
SigType sig_type;
u8 sign[0x14];
u8 reserved_1[0x28];
};
static_assert(sizeof(SignatureBlockHmac160) == 0x40);
struct CertHeader {
char issuer[0x40];
PubKeyType pub_key_type;
char subject[0x40]; /* ServerId, DeviceId */
u32 date;
};
static_assert(sizeof(CertHeader) == 0x88);
struct PublicKeyBlockRsa4096 {
u8 public_key[0x200];
u32 public_exponent;
u8 reserved_1[0x34];
};
static_assert(sizeof(PublicKeyBlockRsa4096) == 0x238);
struct PublicKeyBlockRsa2048 {
u8 public_key[0x100];
u32 public_exponent;
u8 reserved_1[0x34];
};
static_assert(sizeof(PublicKeyBlockRsa2048) == 0x138);
struct PublicKeyBlockEcc480 {
u8 public_key[0x3C];
u8 reserved_1[0x3C];
};
static_assert(sizeof(PublicKeyBlockEcc480) == 0x78);
template<typename Sig, typename Pub>
struct Cert {
Sig signature_block;
CertHeader cert_header;
Pub public_key_block;
};
using CertRsa4096PubRsa4096 = Cert<SignatureBlockRsa4096, PublicKeyBlockRsa4096>;
using CertRsa4096PubRsa2048 = Cert<SignatureBlockRsa4096, PublicKeyBlockRsa2048>;
using CertRsa4096PubEcc480 = Cert<SignatureBlockRsa4096, PublicKeyBlockEcc480>;
using CertRsa2048PubRsa4096 = Cert<SignatureBlockRsa2048, PublicKeyBlockRsa4096>;
using CertRsa2048PubRsa2048 = Cert<SignatureBlockRsa2048, PublicKeyBlockRsa2048>;
using CertRsa2048PubEcc480 = Cert<SignatureBlockRsa2048, PublicKeyBlockEcc480>;
using CertEcc480PubRsa4096 = Cert<SignatureBlockEcc480, PublicKeyBlockRsa4096>;
using CertEcc480PubRsa2048 = Cert<SignatureBlockEcc480, PublicKeyBlockRsa2048>;
using CertEcc480PubEcc480 = Cert<SignatureBlockEcc480, PublicKeyBlockEcc480>;
using CertHmac160PubRsa4096 = Cert<SignatureBlockHmac160, PublicKeyBlockRsa4096>;
using CertHmac160PubRsa2048 = Cert<SignatureBlockHmac160, PublicKeyBlockRsa2048>;
using CertHmac160PubEcc480 = Cert<SignatureBlockHmac160, PublicKeyBlockEcc480>;
static_assert(sizeof(CertRsa4096PubRsa4096) == 0x500);
static_assert(sizeof(CertRsa4096PubRsa2048) == 0x400);
static_assert(sizeof(CertRsa4096PubEcc480) == 0x340);
static_assert(sizeof(CertRsa2048PubRsa4096) == 0x400);
static_assert(sizeof(CertRsa2048PubRsa2048) == 0x300);
static_assert(sizeof(CertRsa2048PubEcc480) == 0x240);
static_assert(sizeof(CertEcc480PubRsa4096) == 0x340);
static_assert(sizeof(CertEcc480PubRsa2048) == 0x240);
static_assert(sizeof(CertEcc480PubEcc480) == 0x180);
static_assert(sizeof(CertHmac160PubRsa4096) == 0x300);
static_assert(sizeof(CertHmac160PubRsa2048) == 0x200);
static_assert(sizeof(CertHmac160PubEcc480) == 0x140);
struct TicketData {
u8 issuer[0x40];
char issuer[0x40];
u8 title_key_block[0x100];
u8 ticket_version1;
u8 format_version;
u8 title_key_type;
u16 ticket_version2;
u8 license_type;
u16 version;
TitleKeyType license_type;
u8 master_key_revision;
u16 properties_bitfield;
u8 _0x148[0x8];
@@ -52,10 +135,28 @@ struct TicketData {
u64 device_id;
FsRightsId rights_id;
u32 account_id;
u8 _0x174[0xC];
u8 _0x180[0x140];
u32 sect_total_size;
u32 sect_hdr_offset;
u16 sect_hdr_count;
u16 sect_hdr_entry_size;
};
static_assert(sizeof(TicketData) == 0x2C0);
static_assert(sizeof(TicketData) == 0x180);
template<typename Sig>
struct Ticket {
Sig signature_block;
TicketData data;
};
using TicketRsa4096 = Ticket<SignatureBlockRsa4096>;
using TicketRsa2048 = Ticket<SignatureBlockRsa2048>;
using TicketEcc480 = Ticket<SignatureBlockEcc480>;
using TicketHmac160 = Ticket<SignatureBlockHmac160>;
static_assert(sizeof(TicketRsa4096) == 0x3C0);
static_assert(sizeof(TicketRsa2048) == 0x2C0);
static_assert(sizeof(TicketEcc480) == 0x200);
static_assert(sizeof(TicketHmac160) == 0x1C0);
struct EticketRsaDeviceKey {
u8 ctr[AES_128_KEY_SIZE];
@@ -90,12 +191,16 @@ Result GetCommonTicketAndCertificateSize(u64 *tik_size_out, u64 *cert_size_out,
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 GetTicketDataOffset(std::span<const u8> ticket, u64& out, bool is_cert = false);
Result GetTicketData(std::span<const u8> ticket, es::TicketData* out);
Result SetTicketData(std::span<u8> ticket, const es::TicketData* in);
// gets the title key and performs RSA-2048-OAEP if needed.
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);
Result EncryptTitleKey(keys::KeyEntry& out, u8 key_gen, const keys::Keys& keys);
Result ShouldPatchTicket(const TicketData& data, std::span<const u8> ticket, std::span<const u8> cert_chain, bool patch_personalised, bool& should_patch);
Result ShouldPatchTicket(std::span<const u8> ticket, std::span<const u8> cert_chain, bool patch_personalised, bool& should_patch);
Result PatchTicket(std::vector<u8>& ticket, std::span<const u8> cert_chain, u8 key_gen, const keys::Keys& keys, bool patch_personalised);
} // namespace sphaira::es

View File

@@ -46,19 +46,19 @@ struct Keys {
}
auto GetNcaKeyArea(KeyEntry* out, u8 key, u8 index) const -> Result {
R_UNLESS(HasNcaKeyArea(key, index), 0x1);
R_UNLESS(HasNcaKeyArea(key, index), Result_KeyMissingNcaKeyArea);
*out = key_area_key[index][FixKey(key)];
R_SUCCEED();
}
auto GetTitleKek(KeyEntry* out, u8 key) const -> Result {
R_UNLESS(HasTitleKek(key), 0x1);
R_UNLESS(HasTitleKek(key), Result_KeyMissingTitleKek);
*out = titlekek[FixKey(key)];
R_SUCCEED();
}
auto GetMasterKey(KeyEntry* out, u8 key) const -> Result {
R_UNLESS(HasMasterKey(key), 0x1);
R_UNLESS(HasMasterKey(key), Result_KeyMissingMasterKey);
*out = master_key[FixKey(key)];
R_SUCCEED();
}

View File

@@ -11,16 +11,6 @@
namespace sphaira::yati::source {
struct Usb final : Base {
enum { USBModule = 508 };
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();

View File

@@ -16,67 +16,6 @@
namespace sphaira::yati {
enum { YatiModule = 506 };
/*
Improving compression ratio via block splitting is now enabled by default for high compression levels (16+).
The amount of benefit varies depending on the workload.
Compressing archives comprised of heavily differing files will see more improvement than compression of single files that dont
vary much entropically (like text files/enwik). At levels 16+, we observe no measurable regression to compression speed.
The block splitter can be forcibly enabled on lower compression levels as well with the advanced parameter ZSTD_c_splitBlocks.
When forcibly enabled at lower levels, speed regressions can become more notable.
Additionally, since more compressed blocks may be produced, decompression speed on these blobs may also see small regressions.
*/
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{};
@@ -109,6 +48,9 @@ struct Config {
// if set, it will ignore the distribution bit in the nca header.
bool ignore_distribution_bit{};
// converts a personalised ticket to common.
bool convert_to_common_ticket{};
// 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{};
@@ -130,6 +72,7 @@ struct ConfigOverride {
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_common_ticket{};
std::optional<bool> convert_to_standard_crypto{};
std::optional<bool> lower_master_key{};
std::optional<bool> lower_system_version{};

View File

@@ -19,13 +19,13 @@
#include "defines.hpp"
#include "i18n.hpp"
#include "ftpsrv_helper.hpp"
#include "haze_helper.hpp"
#include "web.hpp"
#include "swkbd.hpp"
#include <nanovg_dk.h>
#include <minIni.h>
#include <pulsar.h>
#include <haze.h>
#include <algorithm>
#include <ranges>
#include <cassert>
@@ -51,7 +51,7 @@ constexpr const u8 DEFAULT_IMAGE_DATA[]{
};
void download_default_music() {
App::Push(std::make_shared<ui::ProgressBox>(0, "Downloading "_i18n, "default_music.bfstm", [](auto pbox){
App::Push(std::make_shared<ui::ProgressBox>(0, "Downloading "_i18n, "default_music.bfstm", [](auto pbox) -> Result {
const auto result = curl::Api().ToFile(
curl::Url{DEFAULT_MUSIC_URL},
curl::Path{DEFAULT_MUSIC_PATH},
@@ -59,7 +59,7 @@ void download_default_music() {
);
if (!result.success) {
R_THROW(0x1);
R_THROW(Result_AppFailedMusicDownload);
}
R_SUCCEED();
@@ -400,11 +400,6 @@ void LoadThemeInternal(ThemeMeta meta, ThemeData& theme_data, int inherit_level
}
}
void haze_callback(const HazeCallbackData *data) {
App::NotifyFlashLed();
evman::push(*data, false);
}
void nxlink_callback(const NxlinkCallbackData *data) {
App::NotifyFlashLed();
evman::push(*data, false);
@@ -460,9 +455,6 @@ void App::Loop() {
} else if constexpr(std::is_same_v<T, evman::ExitEventData>) {
log_write("[ExitEventData] got event\n");
m_quit = true;
} else if constexpr(std::is_same_v<T, HazeCallbackData>) {
// log_write("[ExitEventData] got event\n");
// m_quit = true;
} else if constexpr(std::is_same_v<T, NxlinkCallbackData>) {
switch (arg.type) {
case NxlinkCallbackType_Connected:
@@ -857,9 +849,9 @@ void App::SetMtpEnable(bool enable) {
if (App::GetMtpEnable() != enable) {
g_app->m_mtp_enabled.Set(enable);
if (enable) {
hazeInitialize(haze_callback, 0x2C, 2);
haze::Init();
} else {
hazeExit();
haze::Exit();
}
}
}
@@ -1283,8 +1275,8 @@ void App::ScanThemeEntries() {
App::App(const char* argv0) {
TimeStamp ts;
appletSetCpuBoostMode(ApmCpuBoostMode_FastLoad);
ON_SCOPE_EXIT(appletSetCpuBoostMode(ApmCpuBoostMode_Normal));
// boost mode is enabled in userAppInit().
ON_SCOPE_EXIT(App::SetBoostMode(false));
g_app = this;
m_start_timestamp = armGetSystemTick();
@@ -1300,21 +1292,6 @@ App::App(const char* argv0) {
__nx_applet_exit_mode = 1;
}
// get emummc config.
alignas(0x1000) AmsEmummcPaths 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);
m_emummc_paths = paths;
log_write("emummc : %u\n", App::IsEmummc());
if (App::IsEmummc()) {
log_write("[emummc] file based path: %s\n", m_emummc_paths.file_based_path);
log_write("[emummc] nintendo path: %s\n", m_emummc_paths.nintendo);
}
fs::FsNativeSd fs;
fs.CreateDirectoryRecursively("/config/sphaira");
fs.CreateDirectory("/config/sphaira/assoc");
@@ -1356,6 +1333,7 @@ App::App(const char* argv0) {
else if (app->m_skip_rsa_header_fixed_key_verify.LoadFrom(Key, Value)) {}
else if (app->m_skip_rsa_npdm_fixed_key_verify.LoadFrom(Key, Value)) {}
else if (app->m_ignore_distribution_bit.LoadFrom(Key, Value)) {}
else if (app->m_convert_to_common_ticket.LoadFrom(Key, Value)) {}
else if (app->m_convert_to_standard_crypto.LoadFrom(Key, Value)) {}
else if (app->m_lower_master_key.LoadFrom(Key, Value)) {}
else if (app->m_lower_system_version.LoadFrom(Key, Value)) {}
@@ -1378,8 +1356,50 @@ App::App(const char* argv0) {
App::Notify("Warning! Logs are enabled, Sphaira will run slowly!"_i18n);
}
if (log_is_init()) {
SetSysFirmwareVersion fw_version{};
setsysInitialize();
ON_SCOPE_EXIT(setsysExit());
setsysGetFirmwareVersion(&fw_version);
log_write("[version] platform: %s\n", fw_version.platform);
log_write("[version] version_hash: %s\n", fw_version.version_hash);
log_write("[version] display_version: %s\n", fw_version.display_version);
log_write("[version] display_title: %s\n", fw_version.display_title);
splInitialize();
ON_SCOPE_EXIT(splExit());
u64 out{};
splGetConfig((SplConfigItem)65000, &out);
log_write("[ams] version: %lu.%lu.%lu\n", (out >> 56) & 0xFF, (out >> 48) & 0xFF, (out >> 40) & 0xFF);
log_write("[ams] target version: %lu.%lu.%lu\n", (out >> 24) & 0xFF, (out >> 16) & 0xFF, (out >> 8) & 0xFF);
log_write("[ams] key gen: %lu\n", (out >> 32) & 0xFF);
splGetConfig((SplConfigItem)65003, &out);
log_write("[ams] hash: %lx\n", out);
splGetConfig((SplConfigItem)65010, &out);
log_write("[ams] usb 3.0 enabled: %lu\n", out);
}
// get emummc config.
alignas(0x1000) AmsEmummcPaths 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);
m_emummc_paths = paths;
log_write("[emummc] enabled: %u\n", App::IsEmummc());
if (App::IsEmummc()) {
log_write("[emummc] file based path: %s\n", m_emummc_paths.file_based_path);
log_write("[emummc] nintendo path: %s\n", m_emummc_paths.nintendo);
}
if (App::GetMtpEnable()) {
hazeInitialize(haze_callback, PRIO_PREEMPTIVE, 2);
haze::Init();
}
if (App::GetFtpEnable()) {
@@ -1812,6 +1832,10 @@ void App::DisplayInstallOptions(bool left_side) {
App::GetApp()->m_ignore_distribution_bit.Set(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Convert to common ticket"_i18n, App::GetApp()->m_convert_to_common_ticket.Get(), [](bool& enable){
App::GetApp()->m_convert_to_common_ticket.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);
}));
@@ -1848,27 +1872,36 @@ void App::DisplayDumpOptions(bool left_side) {
options->Add(std::make_shared<ui::SidebarEntryBool>("Multi-threaded USB transfer"_i18n, App::GetApp()->m_dump_usb_transfer_stream.Get(), [](bool& enable){
App::GetApp()->m_dump_usb_transfer_stream.Set(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Convert to common ticket"_i18n, App::GetApp()->m_dump_convert_to_common_ticket.Get(), [](bool& enable){
App::GetApp()->m_dump_convert_to_common_ticket.Set(enable);
}));
}
App::~App() {
appletSetCpuBoostMode(ApmCpuBoostMode_FastLoad);
ON_SCOPE_EXIT(appletSetCpuBoostMode(ApmCpuBoostMode_Normal));
// boost mode is disabled in userAppExit().
App::SetBoostMode(true);
log_write("starting to exit\n");
TimeStamp ts;
i18n::exit();
curl::Exit();
appletUnhook(&m_appletHookCookie);
// destroy this first as it seems to prevent a crash when exiting the appstore
// when an image that was being drawn is displayed
// replicate: saves -> homebrew -> misc -> appstore -> sphaira -> changelog -> exit
// it will crash when deleting image 43.
this->destroyFramebufferResources();
// this has to be called before any cleanup to ensure the lifetime of
// nvg is still active as some widgets may need to free images.
m_widgets.clear();
nvgDeleteImage(vg, m_default_image);
appletUnhook(&m_appletHookCookie);
i18n::exit();
curl::Exit();
ini_puts("config", "theme", m_theme.meta.ini_path, CONFIG_PATH);
CloseTheme();
// Free any loaded sound from memory
@@ -1881,7 +1914,6 @@ App::~App() {
// De-initialize our player
plsrPlayerExit();
this->destroyFramebufferResources();
nvgDeleteDk(this->vg);
this->renderer.reset();
@@ -1944,7 +1976,7 @@ App::~App() {
if (App::GetMtpEnable()) {
log_write("closing mtp\n");
hazeExit();
haze::Exit();
}
if (App::GetFtpEnable()) {

View File

@@ -259,7 +259,7 @@ private:
struct ThreadEntry {
auto Create() -> Result {
m_curl = curl_easy_init();
R_UNLESS(m_curl != nullptr, 0x1);
R_UNLESS(m_curl != nullptr, Result_CurlFailedEasyInit);
ueventCreate(&m_uevent, true);
R_TRY(threadCreate(&m_thread, ThreadFunc, this, nullptr, 1024*32, THREAD_PRIO, THREAD_CORE));

View File

@@ -23,17 +23,12 @@
namespace sphaira::dump {
namespace {
struct DumpEntry {
DumpLocationType type;
s32 index;
};
struct DumpLocation {
struct DumpLocationEntry {
const DumpLocationType type;
const char* name;
};
constexpr DumpLocation DUMP_LOCATIONS[]{
constexpr DumpLocationEntry DUMP_LOCATIONS[]{
{ DumpLocationType_SdCard, "microSD card (/dumps/)" },
{ DumpLocationType_UsbS2S, "USB transfer (Switch 2 Switch)" },
{ DumpLocationType_DevNull, "/dev/null (Speed Test)" },
@@ -232,7 +227,7 @@ Result DumpToUsbS2S(ui::ProgressBox* pbox, BaseSource* source, std::span<const f
log_write("skipped polling for exit command\n");
}
if (rc == usb->Result_Exit) {
if (rc == Result_UsbUploadExit) {
log_write("got exit command\n");
R_SUCCEED();
}
@@ -308,7 +303,7 @@ Result DumpToNetwork(ui::ProgressBox* pbox, const location::Entry& loc, BaseSour
}
);
R_UNLESS(result.success, 0x1);
R_UNLESS(result.success, Result_DumpFailedNetworkUpload);
R_SUCCEED();
}
));
@@ -319,23 +314,24 @@ Result DumpToNetwork(ui::ProgressBox* pbox, const location::Entry& loc, BaseSour
} // namespace
void Dump(std::shared_ptr<BaseSource> source, const std::vector<fs::FsPath>& paths, OnExit on_exit, u32 location_flags) {
void DumpGetLocation(const std::string& title, u32 location_flags, OnLocation on_loc) {
DumpLocation out;
ui::PopupList::Items items;
std::vector<DumpEntry> dump_entries;
const auto network_locations = location::Load();
out.network = location::Load();
if (location_flags & (1 << DumpLocationType_Network)) {
for (s32 i = 0; i < std::size(network_locations); i++) {
for (s32 i = 0; i < std::size(out.network); i++) {
dump_entries.emplace_back(DumpLocationType_Network, i);
items.emplace_back(network_locations[i].name);
items.emplace_back(out.network[i].name);
}
}
const auto stdio_locations = location::GetStdio(true);
out.stdio = location::GetStdio(true);
if (location_flags & (1 << DumpLocationType_Stdio)) {
for (s32 i = 0; i < std::size(stdio_locations); i++) {
for (s32 i = 0; i < std::size(out.stdio); i++) {
dump_entries.emplace_back(DumpLocationType_Stdio, i);
items.emplace_back(stdio_locations[i].name);
items.emplace_back(out.stdio[i].name);
}
}
@@ -347,40 +343,44 @@ void Dump(std::shared_ptr<BaseSource> source, const std::vector<fs::FsPath>& pat
}
App::Push(std::make_shared<ui::PopupList>(
"Select dump location"_i18n, items, [source, paths, on_exit, network_locations, stdio_locations, dump_entries](auto op_index){
if (!op_index) {
on_exit(0xFFFF);
return;
}
const auto dump_entry = dump_entries[*op_index];
App::Push(std::make_shared<ui::ProgressBox>(0, "Dumping"_i18n, "", [source, paths, network_locations, stdio_locations, dump_entry](auto pbox) -> Result {
if (dump_entry.type == DumpLocationType_Network) {
R_TRY(DumpToNetwork(pbox, network_locations[dump_entry.index], source.get(), paths));
} else if (dump_entry.type == DumpLocationType_Stdio) {
R_TRY(DumpToStdio(pbox, stdio_locations[dump_entry.index], source.get(), paths));
} else if (dump_entry.type == DumpLocationType_SdCard) {
R_TRY(DumpToFileNative(pbox, source.get(), paths));
} else if (dump_entry.type == DumpLocationType_UsbS2S) {
R_TRY(DumpToUsbS2S(pbox, source.get(), paths));
} else if (dump_entry.type == DumpLocationType_DevNull) {
R_TRY(DumpToDevNull(pbox, source.get(), paths));
}
R_SUCCEED();
}, [on_exit](Result rc){
App::PushErrorBox(rc, "Dump failed!"_i18n);
if (R_SUCCEEDED(rc)) {
App::Notify("Dump successfull!"_i18n);
log_write("dump successfull!!!\n");
}
on_exit(rc);
}));
title, items, [dump_entries, out, on_loc](auto op_index) mutable {
out.entry = dump_entries[*op_index];
on_loc(out);
}
));
}
void Dump(std::shared_ptr<BaseSource> source, const DumpLocation& location, const std::vector<fs::FsPath>& paths, OnExit on_exit) {
App::Push(std::make_shared<ui::ProgressBox>(0, "Dumping"_i18n, "", [source, paths, location](auto pbox) -> Result {
if (location.entry.type == DumpLocationType_Network) {
R_TRY(DumpToNetwork(pbox, location.network[location.entry.index], source.get(), paths));
} else if (location.entry.type == DumpLocationType_Stdio) {
R_TRY(DumpToStdio(pbox, location.stdio[location.entry.index], source.get(), paths));
} else if (location.entry.type == DumpLocationType_SdCard) {
R_TRY(DumpToFileNative(pbox, source.get(), paths));
} else if (location.entry.type == DumpLocationType_UsbS2S) {
R_TRY(DumpToUsbS2S(pbox, source.get(), paths));
} else if (location.entry.type == DumpLocationType_DevNull) {
R_TRY(DumpToDevNull(pbox, source.get(), paths));
}
R_SUCCEED();
}, [on_exit](Result rc){
App::PushErrorBox(rc, "Dump failed!"_i18n);
if (R_SUCCEEDED(rc)) {
App::Notify("Dump successfull!"_i18n);
log_write("dump successfull!!!\n");
}
on_exit(rc);
}));
}
void Dump(std::shared_ptr<BaseSource> source, const std::vector<fs::FsPath>& paths, OnExit on_exit, u32 location_flags) {
DumpGetLocation("Select dump location"_i18n, location_flags, [source, paths, on_exit](const DumpLocation& loc){
Dump(source, loc, paths, on_exit);
});
}
} // namespace sphaira::dump

View File

@@ -93,30 +93,40 @@ bool is_read_only(std::string_view path) {
} // namespace
FsPath AppendPath(const FsPath& root_path, const FsPath& file_path) {
FsPath AppendPath(const FsPath& root_path, const FsPath& _file_path) {
// strip leading '/' in file path.
auto file_path = _file_path.s;
while (file_path[0] == '/') {
file_path++;
}
FsPath path;
if (root_path[std::strlen(root_path) - 1] != '/') {
std::snprintf(path, sizeof(path), "%s/%s", root_path.s, file_path.s);
std::snprintf(path, sizeof(path), "%s/%s", root_path.s, file_path);
} else {
std::snprintf(path, sizeof(path), "%s%s", root_path.s, file_path.s);
std::snprintf(path, sizeof(path), "%s%s", root_path.s, file_path);
}
return path;
}
Result CreateFile(FsFileSystem* fs, const FsPath& path, u64 size, u32 option, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only_root(path), Fs::ResultReadOnly);
R_UNLESS(ignore_read_only || !is_read_only_root(path), Result_FsReadOnly);
return fsFsCreateFile(fs, path, size, option);
R_TRY(fsFsCreateFile(fs, path, size, option));
fsFsCommit(fs);
R_SUCCEED();
}
Result CreateDirectory(FsFileSystem* fs, const FsPath& path, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only_root(path), Fs::ResultReadOnly);
R_UNLESS(ignore_read_only || !is_read_only_root(path), Result_FsReadOnly);
return fsFsCreateDirectory(fs, path);
R_TRY(fsFsCreateDirectory(fs, path));
fsFsCommit(fs);
R_SUCCEED();
}
Result CreateDirectoryRecursively(FsFileSystem* fs, const FsPath& _path, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only_root(_path), Fs::ResultReadOnly);
R_UNLESS(ignore_read_only || !is_read_only_root(_path), Result_FsReadOnly);
// try and create the directory / see if it already exists before the loop.
Result rc;
@@ -164,7 +174,7 @@ Result CreateDirectoryRecursively(FsFileSystem* fs, const FsPath& _path, bool ig
}
Result CreateDirectoryRecursivelyWithPath(FsFileSystem* fs, const FsPath& _path, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only_root(_path), Fs::ResultReadOnly);
R_UNLESS(ignore_read_only || !is_read_only_root(_path), Result_FsReadOnly);
// strip file name form path.
const auto last_slash = std::strrchr(_path, '/');
@@ -174,38 +184,50 @@ Result CreateDirectoryRecursivelyWithPath(FsFileSystem* fs, const FsPath& _path,
FsPath new_path{};
std::snprintf(new_path, sizeof(new_path), "%.*s", (int)(last_slash - _path.s), _path.s);
return CreateDirectoryRecursively(fs, new_path, ignore_read_only);
R_TRY(CreateDirectoryRecursively(fs, new_path, ignore_read_only));
fsFsCommit(fs);
R_SUCCEED();
}
Result DeleteFile(FsFileSystem* fs, const FsPath& path, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only(path), Fs::ResultReadOnly);
return fsFsDeleteFile(fs, path);
R_UNLESS(ignore_read_only || !is_read_only(path), Result_FsReadOnly);
R_TRY(fsFsDeleteFile(fs, path));
fsFsCommit(fs);
R_SUCCEED();
}
Result DeleteDirectory(FsFileSystem* fs, const FsPath& path, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only(path), Fs::ResultReadOnly);
R_UNLESS(ignore_read_only || !is_read_only(path), Result_FsReadOnly);
return fsFsDeleteDirectory(fs, path);
R_TRY(fsFsDeleteDirectory(fs, path));
fsFsCommit(fs);
R_SUCCEED();
}
Result DeleteDirectoryRecursively(FsFileSystem* fs, const FsPath& path, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only(path), Fs::ResultReadOnly);
R_UNLESS(ignore_read_only || !is_read_only(path), Result_FsReadOnly);
return fsFsDeleteDirectoryRecursively(fs, path);
R_TRY(fsFsDeleteDirectoryRecursively(fs, path));
fsFsCommit(fs);
R_SUCCEED();
}
Result RenameFile(FsFileSystem* fs, const FsPath& src, const FsPath& dst, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only(src), Fs::ResultReadOnly);
R_UNLESS(ignore_read_only || !is_read_only(dst), Fs::ResultReadOnly);
R_UNLESS(ignore_read_only || !is_read_only(src), Result_FsReadOnly);
R_UNLESS(ignore_read_only || !is_read_only(dst), Result_FsReadOnly);
return fsFsRenameFile(fs, src, dst);
R_TRY(fsFsRenameFile(fs, src, dst));
fsFsCommit(fs);
R_SUCCEED();
}
Result RenameDirectory(FsFileSystem* fs, const FsPath& src, const FsPath& dst, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only(src), Fs::ResultReadOnly);
R_UNLESS(ignore_read_only || !is_read_only(dst), Fs::ResultReadOnly);
R_UNLESS(ignore_read_only || !is_read_only(src), Result_FsReadOnly);
R_UNLESS(ignore_read_only || !is_read_only(dst), Result_FsReadOnly);
return fsFsRenameDirectory(fs, src, dst);
R_TRY(fsFsRenameDirectory(fs, src, dst));
fsFsCommit(fs);
R_SUCCEED();
}
Result GetEntryType(FsFileSystem* fs, const FsPath& path, FsDirEntryType* out) {
@@ -253,10 +275,11 @@ Result read_entire_file(FsFileSystem* _fs, const FsPath& path, std::vector<u8>&
}
Result write_entire_file(FsFileSystem* _fs, const FsPath& path, const std::vector<u8>& in, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only(path), Fs::ResultReadOnly);
R_UNLESS(ignore_read_only || !is_read_only(path), Result_FsReadOnly);
FsNative fs{_fs, false, ignore_read_only};
R_TRY(fs.GetFsOpenResult());
ON_SCOPE_EXIT(fs.Commit());
if (auto rc = fs.CreateFile(path, in.size(), 0); R_FAILED(rc) && rc != FsError_PathAlreadyExists) {
return rc;
@@ -273,7 +296,7 @@ Result write_entire_file(FsFileSystem* _fs, const FsPath& path, const std::vecto
}
Result copy_entire_file(FsFileSystem* fs, const FsPath& dst, const FsPath& src, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only(dst), Fs::ResultReadOnly);
R_UNLESS(ignore_read_only || !is_read_only(dst), Result_FsReadOnly);
std::vector<u8> data;
R_TRY(read_entire_file(fs, src, data));
@@ -281,7 +304,7 @@ Result copy_entire_file(FsFileSystem* fs, const FsPath& dst, const FsPath& src,
}
Result CreateFile(const FsPath& path, u64 size, u32 option, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only_root(path), Fs::ResultReadOnly);
R_UNLESS(ignore_read_only || !is_read_only_root(path), Result_FsReadOnly);
auto fd = open(path, O_WRONLY | O_CREAT, DEFFILEMODE);
if (fd == -1) {
@@ -290,19 +313,19 @@ Result CreateFile(const FsPath& path, u64 size, u32 option, bool ignore_read_onl
}
R_TRY(fsdevGetLastResult());
return Fs::ResultUnknownStdioError;
return Result_FsUnknownStdioError;
}
ON_SCOPE_EXIT(close(fd));
if (size) {
R_UNLESS(!ftruncate(fd, size), Fs::ResultUnknownStdioError);
R_UNLESS(!ftruncate(fd, size), Result_FsUnknownStdioError);
}
R_SUCCEED();
}
Result CreateDirectory(const FsPath& path, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only_root(path), Fs::ResultReadOnly);
R_UNLESS(ignore_read_only || !is_read_only_root(path), Result_FsReadOnly);
if (mkdir(path, ACCESSPERMS)) {
if (errno == EEXIST) {
@@ -310,46 +333,46 @@ Result CreateDirectory(const FsPath& path, bool ignore_read_only) {
}
R_TRY(fsdevGetLastResult());
return Fs::ResultUnknownStdioError;
return Result_FsUnknownStdioError;
}
R_SUCCEED();
}
Result CreateDirectoryRecursively(const FsPath& path, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only_root(path), Fs::ResultReadOnly);
R_UNLESS(ignore_read_only || !is_read_only_root(path), Result_FsReadOnly);
return CreateDirectoryRecursively(nullptr, path, ignore_read_only);
}
Result CreateDirectoryRecursivelyWithPath(const FsPath& path, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only_root(path), Fs::ResultReadOnly);
R_UNLESS(ignore_read_only || !is_read_only_root(path), Result_FsReadOnly);
return CreateDirectoryRecursivelyWithPath(nullptr, path, ignore_read_only);
}
Result DeleteFile(const FsPath& path, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only(path), Fs::ResultReadOnly);
R_UNLESS(ignore_read_only || !is_read_only(path), Result_FsReadOnly);
if (unlink(path)) {
R_TRY(fsdevGetLastResult());
return Fs::ResultUnknownStdioError;
return Result_FsUnknownStdioError;
}
R_SUCCEED();
}
Result DeleteDirectory(const FsPath& path, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only(path), Fs::ResultReadOnly);
R_UNLESS(ignore_read_only || !is_read_only(path), Result_FsReadOnly);
if (rmdir(path)) {
R_TRY(fsdevGetLastResult());
return Fs::ResultUnknownStdioError;
return Result_FsUnknownStdioError;
}
R_SUCCEED();
}
// ftw / ntfw isn't found by linker...
Result DeleteDirectoryRecursively(const FsPath& path, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only(path), Fs::ResultReadOnly);
R_UNLESS(ignore_read_only || !is_read_only(path), Result_FsReadOnly);
#if 0
// const auto unlink_cb = [](const char *fpath, const struct stat *sb, int typeflag, struct FTW *ftwbuf) -> int {
@@ -360,7 +383,7 @@ Result DeleteDirectoryRecursively(const FsPath& path, bool ignore_read_only) {
// if (nftw(path, unlink_cb, 16, FTW_DEPTH)) {
if (ftw(path, unlink_cb, 16)) {
R_TRY(fsdevGetLastResult());
return Fs::ResultUnknownStdioError;
return Result_FsUnknownStdioError;
}
R_SUCCEED();
#else
@@ -369,19 +392,19 @@ Result DeleteDirectoryRecursively(const FsPath& path, bool ignore_read_only) {
}
Result RenameFile(const FsPath& src, const FsPath& dst, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only(src), Fs::ResultReadOnly);
R_UNLESS(ignore_read_only || !is_read_only(dst), Fs::ResultReadOnly);
R_UNLESS(ignore_read_only || !is_read_only(src), Result_FsReadOnly);
R_UNLESS(ignore_read_only || !is_read_only(dst), Result_FsReadOnly);
if (rename(src, dst)) {
R_TRY(fsdevGetLastResult());
return Fs::ResultUnknownStdioError;
return Result_FsUnknownStdioError;
}
R_SUCCEED();
}
Result RenameDirectory(const FsPath& src, const FsPath& dst, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only(src), Fs::ResultReadOnly);
R_UNLESS(ignore_read_only || !is_read_only(dst), Fs::ResultReadOnly);
R_UNLESS(ignore_read_only || !is_read_only(src), Result_FsReadOnly);
R_UNLESS(ignore_read_only || !is_read_only(dst), Result_FsReadOnly);
return RenameFile(src, dst, ignore_read_only);
}
@@ -390,7 +413,7 @@ Result GetEntryType(const FsPath& path, FsDirEntryType* out) {
struct stat st;
if (stat(path, &st)) {
R_TRY(fsdevGetLastResult());
return Fs::ResultUnknownStdioError;
return Result_FsUnknownStdioError;
}
*out = S_ISREG(st.st_mode) ? FsDirEntryType_File : FsDirEntryType_Dir;
R_SUCCEED();
@@ -400,7 +423,7 @@ Result GetFileTimeStampRaw(const FsPath& path, FsTimeStampRaw *out) {
struct stat st;
if (stat(path, &st)) {
R_TRY(fsdevGetLastResult());
return Fs::ResultUnknownStdioError;
return Result_FsUnknownStdioError;
}
out->is_valid = true;
@@ -440,7 +463,7 @@ Result read_entire_file(const FsPath& path, std::vector<u8>& out) {
auto f = std::fopen(path, "rb");
if (!f) {
R_TRY(fsdevGetLastResult());
return Fs::ResultUnknownStdioError;
return Result_FsUnknownStdioError;
}
ON_SCOPE_EXIT(std::fclose(f));
@@ -455,12 +478,12 @@ Result read_entire_file(const FsPath& path, std::vector<u8>& out) {
}
Result write_entire_file(const FsPath& path, const std::vector<u8>& in, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only(path), Fs::ResultReadOnly);
R_UNLESS(ignore_read_only || !is_read_only(path), Result_FsReadOnly);
auto f = std::fopen(path, "wb");
if (!f) {
R_TRY(fsdevGetLastResult());
return Fs::ResultUnknownStdioError;
return Result_FsUnknownStdioError;
}
ON_SCOPE_EXIT(std::fclose(f));
@@ -469,7 +492,7 @@ Result write_entire_file(const FsPath& path, const std::vector<u8>& in, bool ign
}
Result copy_entire_file(const FsPath& dst, const FsPath& src, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only(dst), Fs::ResultReadOnly);
R_UNLESS(ignore_read_only || !is_read_only(dst), Result_FsReadOnly);
std::vector<u8> data;
R_TRY(read_entire_file(src, data));
@@ -478,6 +501,7 @@ Result copy_entire_file(const FsPath& dst, const FsPath& src, bool ignore_read_o
Result OpenFile(fs::Fs* fs, const fs::FsPath& path, u32 mode, File* f) {
f->m_fs = fs;
f->m_mode = mode;
if (f->m_fs->IsNative()) {
auto fs = (fs::FsNative*)f->m_fs;
@@ -493,8 +517,7 @@ Result OpenFile(fs::Fs* fs, const fs::FsPath& path, u32 mode, File* f) {
f->m_stdio = std::fopen(path, "rb+");
}
R_UNLESS(f->m_stdio, Fs::ResultUnknownStdioError);
std::strcpy(f->m_path, path);
R_UNLESS(f->m_stdio, Result_FsUnknownStdioError);
}
R_SUCCEED();
@@ -506,7 +529,7 @@ File::~File() {
Result File::Read( s64 off, void* buf, u64 read_size, u32 option, u64* bytes_read) {
*bytes_read = 0;
R_UNLESS(m_fs, 0x1);
R_UNLESS(m_fs, Result_FsNotActive);
if (m_fs->IsNative()) {
R_TRY(fsFileRead(&m_native, off, buf, read_size, option, bytes_read));
@@ -521,7 +544,7 @@ Result File::Read( s64 off, void* buf, u64 read_size, u32 option, u64* bytes_rea
// if we read less bytes than expected, check if there was an error (ignoring eof).
if (*bytes_read < read_size) {
if (!std::feof(m_stdio) && std::ferror(m_stdio)) {
R_THROW(Fs::ResultUnknownStdioError);
R_THROW(Result_FsUnknownStdioError);
}
}
@@ -532,7 +555,7 @@ Result File::Read( s64 off, void* buf, u64 read_size, u32 option, u64* bytes_rea
}
Result File::Write(s64 off, const void* buf, u64 write_size, u32 option) {
R_UNLESS(m_fs, 0x1);
R_UNLESS(m_fs, Result_FsNotActive);
if (m_fs->IsNative()) {
R_TRY(fsFileWrite(&m_native, off, buf, write_size, option));
@@ -545,7 +568,7 @@ Result File::Write(s64 off, const void* buf, u64 write_size, u32 option) {
const auto result = std::fwrite(buf, 1, write_size, m_stdio);
// log_write("[FS] fwrite res: %zu vs %zu\n", result, write_size);
R_UNLESS(result == write_size, Fs::ResultUnknownStdioError);
R_UNLESS(result == write_size, Result_FsUnknownStdioError);
m_stdio_off += write_size;
}
@@ -554,37 +577,27 @@ Result File::Write(s64 off, const void* buf, u64 write_size, u32 option) {
}
Result File::SetSize(s64 sz) {
R_UNLESS(m_fs, 0x1);
R_UNLESS(m_fs, Result_FsNotActive);
if (m_fs->IsNative()) {
R_TRY(fsFileSetSize(&m_native, sz));
} else {
const auto fd = fileno(m_stdio);
R_UNLESS(fd > 0, Fs::ResultUnknownStdioError);
R_UNLESS(!ftruncate(fd, sz), Fs::ResultUnknownStdioError);
R_UNLESS(fd > 0, Result_FsUnknownStdioError);
R_UNLESS(!ftruncate(fd, sz), Result_FsUnknownStdioError);
}
R_SUCCEED();
}
Result File::GetSize(s64* out) {
R_UNLESS(m_fs, 0x1);
R_UNLESS(m_fs, Result_FsNotActive);
if (m_fs->IsNative()) {
R_TRY(fsFileGetSize(&m_native, out));
} else {
struct stat st;
const auto fd = fileno(m_stdio);
bool did_stat{};
if (fd && !fstat(fd, &st)) {
did_stat = true;
}
if (!did_stat) {
R_UNLESS(!lstat(m_path, &st), Fs::ResultUnknownStdioError);
}
R_UNLESS(!fstat(fileno(m_stdio), &st), Result_FsUnknownStdioError);
*out = st.st_size;
}
@@ -599,6 +612,9 @@ void File::Close() {
if (m_fs->IsNative()) {
if (serviceIsActive(&m_native.s)) {
fsFileClose(&m_native);
if (m_mode & FsOpenMode_Write) {
m_fs->Commit();
}
m_native = {};
}
} else {
@@ -618,7 +634,7 @@ Result OpenDirectory(fs::Fs* fs, const fs::FsPath& path, u32 mode, Dir* d) {
R_TRY(fsFsOpenDirectory(&fs->m_fs, path, mode, &d->m_native));
} else {
d->m_stdio = opendir(path);
R_UNLESS(d->m_stdio, Fs::ResultUnknownStdioError);
R_UNLESS(d->m_stdio, Result_FsUnknownStdioError);
}
R_SUCCEED();
@@ -677,7 +693,7 @@ Dir::~Dir() {
Result Dir::GetEntryCount(s64* out) {
*out = 0;
R_UNLESS(m_fs, 0x1);
R_UNLESS(m_fs, Result_FsNotActive);
if (m_fs->IsNative()) {
R_TRY(fsDirGetEntryCount(&m_native, out));
@@ -696,9 +712,50 @@ Result Dir::GetEntryCount(s64* out) {
R_SUCCEED();
}
Result Dir::Read(s64 *total_entries, size_t max_entries, FsDirectoryEntry *buf) {
R_UNLESS(m_fs, Result_FsNotActive);
*total_entries = 0;
if (m_fs->IsNative()) {
R_TRY(fsDirRead(&m_native, total_entries, max_entries, buf));
} else {
while (auto d = readdir(m_stdio)) {
if (!std::strcmp(d->d_name, ".") || !std::strcmp(d->d_name, "..")) {
continue;
}
FsDirectoryEntry entry{};
if (d->d_type == DT_DIR) {
if (!(m_mode & FsDirOpenMode_ReadDirs)) {
continue;
}
entry.type = FsDirEntryType_Dir;
} else if (d->d_type == DT_REG) {
if (!(m_mode & FsDirOpenMode_ReadFiles)) {
continue;
}
entry.type = FsDirEntryType_File;
} else {
log_write("[FS] WARNING: unknown type when reading dir: %u\n", d->d_type);
continue;
}
std::strcpy(entry.name, d->d_name);
std::memcpy(&buf[*total_entries], &entry, sizeof(*buf));
*total_entries = *total_entries + 1;
if (*total_entries >= max_entries) {
break;
}
}
}
R_SUCCEED();
}
Result Dir::ReadAll(std::vector<FsDirectoryEntry>& buf) {
buf.clear();
R_UNLESS(m_fs, 0x1);
R_UNLESS(m_fs, Result_FsNotActive);
if (m_fs->IsNative()) {
s64 count;
@@ -773,7 +830,7 @@ Result FileGetSizeAndTimestamp(fs::Fs* m_fs, const FsPath& path, FsTimeStampRaw*
R_TRY(f.GetSize(size));
} else {
struct stat st;
R_UNLESS(!lstat(path, &st), 0x1);
R_UNLESS(!lstat(path, &st), Result_FsFailedStdioStat);
ts->is_valid = true;
ts->created = st.st_ctim.tv_sec;
@@ -796,7 +853,7 @@ Result IsDirEmpty(fs::Fs* m_fs, const fs::FsPath& path, bool* out) {
*out = !count;
} else {
auto dir = opendir(path);
R_UNLESS(dir, 0x1);
R_UNLESS(dir, Result_FsFailedStdioOpendir);
ON_SCOPE_EXIT(closedir(dir));
while (auto d = readdir(dir)) {

View File

@@ -4,7 +4,6 @@
#include "fs.hpp"
#include "log.hpp"
#include <mutex>
#include <algorithm>
#include <minIni.h>
#include <ftpsrv.h>
@@ -16,8 +15,7 @@ namespace sphaira::ftpsrv {
namespace {
struct InstallSharedData {
std::mutex mutex;
Mutex mutex;
std::deque<std::string> queued_files;
void* user;
@@ -36,7 +34,7 @@ FtpSrvConfig g_ftpsrv_config = {0};
volatile bool g_should_exit = false;
bool g_is_running{false};
Thread g_thread;
std::mutex g_mutex{};
Mutex g_mutex{};
InstallSharedData g_shared_data{};
void ftp_log_callback(enum FTP_API_LOG_TYPE type, const char* msg) {
@@ -59,13 +57,13 @@ struct VfsUserData {
// ive given up with good names.
void on_thing() {
log_write("[FTP] doing on_thing\n");
std::scoped_lock lock{g_shared_data.mutex};
SCOPED_MUTEX(&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())) {
if (!g_shared_data.on_start || !g_shared_data.on_start(g_shared_data.queued_files[0].c_str())) {
g_shared_data.queued_files.clear();
} else {
log_write("[FTP] success on new file push\n");
@@ -77,7 +75,7 @@ void on_thing() {
int vfs_install_open(void* user, const char* path, enum FtpVfsOpenMode mode) {
{
std::scoped_lock lock{g_shared_data.mutex};
SCOPED_MUTEX(&g_shared_data.mutex);
auto data = static_cast<VfsUserData*>(user);
data->valid = 0;
@@ -133,7 +131,7 @@ int vfs_install_read(void* user, void* buf, size_t size) {
}
int vfs_install_write(void* user, const void* buf, size_t size) {
std::scoped_lock lock{g_shared_data.mutex};
SCOPED_MUTEX(&g_shared_data.mutex);
if (!g_shared_data.enabled) {
errno = EACCES;
return -1;
@@ -145,7 +143,7 @@ int vfs_install_write(void* user, const void* buf, size_t size) {
return -1;
}
if (!g_shared_data.on_write || !g_shared_data.on_write(g_shared_data.user, buf, size)) {
if (!g_shared_data.on_write || !g_shared_data.on_write(buf, size)) {
errno = EIO;
return -1;
}
@@ -159,13 +157,13 @@ int vfs_install_seek(void* user, const void* buf, size_t size, size_t off) {
}
int vfs_install_isfile_open(void* user) {
std::scoped_lock lock{g_shared_data.mutex};
SCOPED_MUTEX(&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};
SCOPED_MUTEX(&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;
@@ -174,7 +172,7 @@ int vfs_install_isfile_ready(void* user) {
int vfs_install_close(void* user) {
{
log_write("[FTP] closing file\n");
std::scoped_lock lock{g_shared_data.mutex};
SCOPED_MUTEX(&g_shared_data.mutex);
auto data = static_cast<VfsUserData*>(user);
if (data->valid) {
log_write("[FTP] closing valid file\n");
@@ -184,7 +182,7 @@ int vfs_install_close(void* user) {
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.on_close();
}
g_shared_data.in_progress = false;
@@ -296,7 +294,7 @@ void loop(void* arg) {
} // namespace
bool Init() {
std::scoped_lock lock{g_mutex};
SCOPED_MUTEX(&g_mutex);
if (g_is_running) {
log_write("[FTP] already enabled, cannot open\n");
return false;
@@ -380,7 +378,7 @@ bool Init() {
}
void Exit() {
std::scoped_lock lock{g_mutex};
SCOPED_MUTEX(&g_mutex);
if (!g_is_running) {
return;
}
@@ -398,9 +396,8 @@ void Exit() {
log_write("[FTP] exitied\n");
}
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;
void InitInstallMode(OnInstallStart on_start, OnInstallWrite on_write, OnInstallClose on_close) {
SCOPED_MUTEX(&g_shared_data.mutex);
g_shared_data.on_start = on_start;
g_shared_data.on_write = on_write;
g_shared_data.on_close = on_close;
@@ -408,27 +405,27 @@ void InitInstallMode(void* user, OnInstallStart on_start, OnInstallWrite on_writ
}
void DisableInstallMode() {
std::scoped_lock lock{g_shared_data.mutex};
SCOPED_MUTEX(&g_shared_data.mutex);
g_shared_data.enabled = false;
}
unsigned GetPort() {
std::scoped_lock lock{g_mutex};
SCOPED_MUTEX(&g_mutex);
return g_ftpsrv_config.port;
}
bool IsAnon() {
std::scoped_lock lock{g_mutex};
SCOPED_MUTEX(&g_mutex);
return g_ftpsrv_config.anon;
}
const char* GetUser() {
std::scoped_lock lock{g_mutex};
SCOPED_MUTEX(&g_mutex);
return g_ftpsrv_config.user;
}
const char* GetPass() {
std::scoped_lock lock{g_mutex};
SCOPED_MUTEX(&g_mutex);
return g_ftpsrv_config.pass;
}

View File

@@ -2,6 +2,7 @@
#include "app.hpp"
#include "threaded_file_transfer.hpp"
#include <mbedtls/md5.h>
#include <utility>
namespace sphaira::hash {
namespace {
@@ -16,11 +17,11 @@ struct FileSource final : BaseSource {
m_is_file_based_emummc = App::IsFileBaseEmummc();
}
Result Size(s64* out) {
Result Size(s64* out) override {
return m_file.GetSize(out);
}
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) {
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override {
const auto rc = m_file.Read(off, buf, size, 0, bytes_read);
if (m_fs->IsNative() && m_is_file_based_emummc) {
svcSleepThread(2e+6); // 2ms
@@ -38,12 +39,12 @@ private:
struct MemSource final : BaseSource {
MemSource(std::span<const u8> data) : m_data{data} { }
Result Size(s64* out) {
Result Size(s64* out) override {
*out = m_data.size();
R_SUCCEED();
}
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) {
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override {
size = std::min<s64>(size, m_data.size() - off);
std::memcpy(buf, m_data.data() + off, size);
*bytes_read = size;
@@ -61,11 +62,11 @@ struct HashSource {
};
struct HashCrc32 final : HashSource {
void Update(const void* buf, s64 size) {
void Update(const void* buf, s64 size) override {
m_seed = crc32CalculateWithSeed(m_seed, buf, size);
}
void Get(std::string& out) {
void Get(std::string& out) override {
char str[CalculateHashStrLen(sizeof(m_seed))];
std::snprintf(str, sizeof(str), "%08x", m_seed);
out = str;
@@ -85,11 +86,11 @@ struct HashMd5 final : HashSource {
mbedtls_md5_free(&m_ctx);
}
void Update(const void* buf, s64 size) {
void Update(const void* buf, s64 size) override {
mbedtls_md5_update_ret(&m_ctx, (const u8*)buf, size);
}
void Get(std::string& out) {
void Get(std::string& out) override {
u8 hash[16];
mbedtls_md5_finish_ret(&m_ctx, hash);
@@ -110,11 +111,11 @@ struct HashSha1 final : HashSource {
sha1ContextCreate(&m_ctx);
}
void Update(const void* buf, s64 size) {
void Update(const void* buf, s64 size) override {
sha1ContextUpdate(&m_ctx, buf, size);
}
void Get(std::string& out) {
void Get(std::string& out) override {
u8 hash[SHA1_HASH_SIZE];
sha1ContextGetHash(&m_ctx, hash);
@@ -135,11 +136,11 @@ struct HashSha256 final : HashSource {
sha256ContextCreate(&m_ctx);
}
void Update(const void* buf, s64 size) {
void Update(const void* buf, s64 size) override {
sha256ContextUpdate(&m_ctx, buf, size);
}
void Get(std::string& out) {
void Get(std::string& out) override {
u8 hash[SHA256_HASH_SIZE];
sha256ContextGetHash(&m_ctx, hash);
@@ -192,7 +193,7 @@ Result Hash(ui::ProgressBox* pbox, Type type, std::shared_ptr<BaseSource> source
case Type::Sha1: return Hash(pbox, std::make_unique<HashSha1>(), source, out);
case Type::Sha256: return Hash(pbox, std::make_unique<HashSha256>(), source, out);
}
R_THROW(0x1);
std::unreachable();
}
Result Hash(ui::ProgressBox* pbox, Type type, fs::Fs* fs, const fs::FsPath& path, std::string& out) {

View File

@@ -0,0 +1,603 @@
#include "haze_helper.hpp"
#include "app.hpp"
#include "fs.hpp"
#include "log.hpp"
#include "evman.hpp"
#include "i18n.hpp"
#include <algorithm>
#include <haze.h>
namespace sphaira::haze {
namespace {
struct InstallSharedData {
Mutex mutex;
std::string current_file;
void* user;
OnInstallStart on_start;
OnInstallWrite on_write;
OnInstallClose on_close;
bool in_progress;
bool enabled;
};
constexpr int THREAD_PRIO = PRIO_PREEMPTIVE;
constexpr int THREAD_CORE = 2;
volatile bool g_should_exit = false;
bool g_is_running{false};
Mutex g_mutex{};
InstallSharedData g_shared_data{};
const char* SUPPORTED_EXT[] = {
".nsp", ".xci", ".nsz", ".xcz",
};
// ive given up with good names.
void on_thing() {
log_write("[MTP] doing on_thing\n");
SCOPED_MUTEX(&g_shared_data.mutex);
log_write("[MTP] locked on_thing\n");
if (!g_shared_data.in_progress) {
if (!g_shared_data.current_file.empty()) {
log_write("[MTP] pushing new file data\n");
if (!g_shared_data.on_start || !g_shared_data.on_start(g_shared_data.current_file.c_str())) {
g_shared_data.current_file.clear();
} else {
log_write("[MTP] success on new file push\n");
g_shared_data.in_progress = true;
}
}
}
}
struct FsProxyBase : ::haze::FileSystemProxyImpl {
FsProxyBase(const char* name, const char* display_name) : m_name{name}, m_display_name{display_name} {
}
auto FixPath(const char* path) const {
fs::FsPath buf;
const auto len = std::strlen(GetName());
if (len && !strncasecmp(path + 1, GetName(), len)) {
std::snprintf(buf, sizeof(buf), "/%s", path + 1 + len);
} else {
std::strcpy(buf, path);
}
// log_write("[FixPath] %s -> %s\n", path, buf.s);
return buf;
}
const char* GetName() const override {
return m_name.c_str();
}
const char* GetDisplayName() const override {
return m_display_name.c_str();
}
protected:
const std::string m_name;
const std::string m_display_name;
};
struct FsProxy final : FsProxyBase {
FsProxy(std::shared_ptr<fs::Fs> fs, const char* name, const char* display_name) : FsProxyBase{name, display_name} {
m_fs = fs;
}
~FsProxy() {
if (m_fs->IsNative()) {
auto fs = (fs::FsNative*)m_fs.get();
fsFsCommit(&fs->m_fs);
}
}
// TODO: impl this for stdio
Result GetTotalSpace(const char *path, s64 *out) override {
if (m_fs->IsNative()) {
auto fs = (fs::FsNative*)m_fs.get();
return fsFsGetTotalSpace(&fs->m_fs, FixPath(path), out);
}
*out = 1024ULL * 1024ULL * 1024ULL * 256ULL;
R_SUCCEED();
}
Result GetFreeSpace(const char *path, s64 *out) override {
if (m_fs->IsNative()) {
auto fs = (fs::FsNative*)m_fs.get();
return fsFsGetFreeSpace(&fs->m_fs, FixPath(path), out);
}
*out = 1024ULL * 1024ULL * 1024ULL * 256ULL;
R_SUCCEED();
}
Result GetEntryType(const char *path, FsDirEntryType *out_entry_type) override {
const auto rc = m_fs->GetEntryType(FixPath(path), out_entry_type);
log_write("[HAZE] GetEntryType(%s) 0x%X\n", path, rc);
return rc;
}
Result CreateFile(const char* path, s64 size, u32 option) override {
log_write("[HAZE] CreateFile(%s)\n", path);
return m_fs->CreateFile(FixPath(path), size, option);
}
Result DeleteFile(const char* path) override {
log_write("[HAZE] DeleteFile(%s)\n", path);
return m_fs->DeleteFile(FixPath(path));
}
Result RenameFile(const char *old_path, const char *new_path) override {
log_write("[HAZE] RenameFile(%s -> %s)\n", old_path, new_path);
return m_fs->RenameFile(FixPath(old_path), FixPath(new_path));
}
Result OpenFile(const char *path, u32 mode, FsFile *out_file) override {
log_write("[HAZE] OpenFile(%s)\n", path);
auto fptr = new fs::File();
const auto rc = m_fs->OpenFile(FixPath(path), mode, fptr);
if (R_SUCCEEDED(rc)) {
std::memcpy(&out_file->s, &fptr, sizeof(fptr));
} else {
delete fptr;
}
return rc;
}
Result GetFileSize(FsFile *file, s64 *out_size) override {
log_write("[HAZE] GetFileSize()\n");
fs::File* f;
std::memcpy(&f, &file->s, sizeof(f));
return f->GetSize(out_size);
}
Result SetFileSize(FsFile *file, s64 size) override {
log_write("[HAZE] SetFileSize()\n");
fs::File* f;
std::memcpy(&f, &file->s, sizeof(f));
return f->SetSize(size);
}
Result ReadFile(FsFile *file, s64 off, void *buf, u64 read_size, u32 option, u64 *out_bytes_read) override {
log_write("[HAZE] ReadFile()\n");
fs::File* f;
std::memcpy(&f, &file->s, sizeof(f));
return f->Read(off, buf, read_size, option, out_bytes_read);
}
Result WriteFile(FsFile *file, s64 off, const void *buf, u64 write_size, u32 option) override {
log_write("[HAZE] WriteFile()\n");
fs::File* f;
std::memcpy(&f, &file->s, sizeof(f));
return f->Write(off, buf, write_size, option);
}
void CloseFile(FsFile *file) override {
log_write("[HAZE] CloseFile()\n");
fs::File* f;
std::memcpy(&f, &file->s, sizeof(f));
if (f) {
delete f;
}
std::memset(file, 0, sizeof(*file));
}
Result CreateDirectory(const char* path) override {
log_write("[HAZE] DeleteFile(%s)\n", path);
return m_fs->CreateDirectory(FixPath(path));
}
Result DeleteDirectoryRecursively(const char* path) override {
log_write("[HAZE] DeleteDirectoryRecursively(%s)\n", path);
return m_fs->DeleteDirectoryRecursively(FixPath(path));
}
Result RenameDirectory(const char *old_path, const char *new_path) override {
log_write("[HAZE] RenameDirectory(%s -> %s)\n", old_path, new_path);
return m_fs->RenameDirectory(FixPath(old_path), FixPath(new_path));
}
Result OpenDirectory(const char *path, u32 mode, FsDir *out_dir) override {
auto fptr = new fs::Dir();
const auto rc = m_fs->OpenDirectory(FixPath(path), mode, fptr);
if (R_SUCCEEDED(rc)) {
std::memcpy(&out_dir->s, &fptr, sizeof(fptr));
} else {
delete fptr;
}
log_write("[HAZE] OpenDirectory(%s) 0x%X\n", path, rc);
return rc;
}
Result ReadDirectory(FsDir *d, s64 *out_total_entries, size_t max_entries, FsDirectoryEntry *buf) override {
fs::Dir* f;
std::memcpy(&f, &d->s, sizeof(f));
const auto rc = f->Read(out_total_entries, max_entries, buf);
log_write("[HAZE] ReadDirectory(%zd) 0x%X\n", *out_total_entries, rc);
return rc;
}
Result GetDirectoryEntryCount(FsDir *d, s64 *out_count) override {
fs::Dir* f;
std::memcpy(&f, &d->s, sizeof(f));
const auto rc = f->GetEntryCount(out_count);
log_write("[HAZE] GetDirectoryEntryCount(%zd) 0x%X\n", *out_count, rc);
return rc;
}
void CloseDirectory(FsDir *d) override {
log_write("[HAZE] CloseDirectory()\n");
fs::Dir* f;
std::memcpy(&f, &d->s, sizeof(f));
if (f) {
delete f;
}
std::memset(d, 0, sizeof(*d));
}
private:
std::shared_ptr<fs::Fs> m_fs{};
};
// fake fs that allows for files to create r/w on the root.
// folders are not yet supported.
struct FsProxyVfs : FsProxyBase {
using FsProxyBase::FsProxyBase;
virtual ~FsProxyVfs() = default;
auto GetFileName(const char* s) -> const char* {
const auto file_name = std::strrchr(s, '/');
if (!file_name || file_name[1] == '\0') {
return nullptr;
}
return file_name + 1;
}
virtual Result GetEntryType(const char *path, FsDirEntryType *out_entry_type) {
if (FixPath(path) == "/") {
*out_entry_type = FsDirEntryType_Dir;
R_SUCCEED();
} else {
const auto file_name = GetFileName(path);
R_UNLESS(file_name, FsError_PathNotFound);
const auto it = std::ranges::find_if(m_entries, [file_name](auto& e){
return !strcasecmp(file_name, e.name);
});
R_UNLESS(it != m_entries.end(), FsError_PathNotFound);
*out_entry_type = FsDirEntryType_File;
R_SUCCEED();
}
}
virtual Result CreateFile(const char* path, s64 size, u32 option) {
const auto file_name = GetFileName(path);
R_UNLESS(file_name, FsError_PathNotFound);
const auto it = std::ranges::find_if(m_entries, [file_name](auto& e){
return !strcasecmp(file_name, e.name);
});
R_UNLESS(it == m_entries.end(), FsError_PathAlreadyExists);
FsDirectoryEntry entry{};
std::strcpy(entry.name, file_name);
entry.type = FsDirEntryType_File;
entry.file_size = size;
m_entries.emplace_back(entry);
R_SUCCEED();
}
virtual Result DeleteFile(const char* path) {
const auto file_name = GetFileName(path);
R_UNLESS(file_name, FsError_PathNotFound);
const auto it = std::ranges::find_if(m_entries, [file_name](auto& e){
return !strcasecmp(file_name, e.name);
});
R_UNLESS(it != m_entries.end(), FsError_PathNotFound);
m_entries.erase(it);
R_SUCCEED();
}
virtual Result RenameFile(const char *old_path, const char *new_path) {
const auto file_name = GetFileName(old_path);
R_UNLESS(file_name, FsError_PathNotFound);
const auto it = std::ranges::find_if(m_entries, [file_name](auto& e){
return !strcasecmp(file_name, e.name);
});
R_UNLESS(it != m_entries.end(), FsError_PathNotFound);
const auto file_name_new = GetFileName(new_path);
R_UNLESS(file_name_new, FsError_PathNotFound);
const auto new_it = std::ranges::find_if(m_entries, [file_name_new](auto& e){
return !strcasecmp(file_name_new, e.name);
});
R_UNLESS(new_it == m_entries.end(), FsError_PathAlreadyExists);
std::strcpy(it->name, file_name_new);
R_SUCCEED();
}
virtual Result OpenFile(const char *path, u32 mode, FsFile *out_file) {
const auto file_name = GetFileName(path);
R_UNLESS(file_name, FsError_PathNotFound);
const auto it = std::ranges::find_if(m_entries, [file_name](auto& e){
return !strcasecmp(file_name, e.name);
});
R_UNLESS(it != m_entries.end(), FsError_PathNotFound);
out_file->s.object_id = std::distance(m_entries.begin(), it);
out_file->s.own_handle = mode;
R_SUCCEED();
}
virtual Result GetFileSize(FsFile *file, s64 *out_size) {
auto& e = m_entries[file->s.object_id];
*out_size = e.file_size;
R_SUCCEED();
}
virtual Result SetFileSize(FsFile *file, s64 size) {
auto& e = m_entries[file->s.object_id];
e.file_size = size;
R_SUCCEED();
}
virtual Result ReadFile(FsFile *file, s64 off, void *buf, u64 read_size, u32 option, u64 *out_bytes_read) {
// stub for now as it may confuse users who think that the returned file is valid.
// the code below can be used to benchmark mtp reads.
R_THROW(FsError_NotImplemented);
// auto& e = m_entries[file->s.object_id];
// read_size = std::min<s64>(e.file_size - off, read_size);
// std::memset(buf, 0, read_size);
// *out_bytes_read = read_size;
// R_SUCCEED();
}
virtual Result WriteFile(FsFile *file, s64 off, const void *buf, u64 write_size, u32 option) {
auto& e = m_entries[file->s.object_id];
e.file_size = std::max<s64>(e.file_size, off + write_size);
R_SUCCEED();
}
virtual void CloseFile(FsFile *file) {
std::memset(file, 0, sizeof(*file));
}
Result CreateDirectory(const char* path) override {
R_THROW(FsError_NotImplemented);
}
Result DeleteDirectoryRecursively(const char* path) override {
R_THROW(FsError_NotImplemented);
}
Result RenameDirectory(const char *old_path, const char *new_path) override {
R_THROW(FsError_NotImplemented);
}
Result OpenDirectory(const char *path, u32 mode, FsDir *out_dir) override {
std::memset(out_dir, 0, sizeof(*out_dir));
R_SUCCEED();
}
Result ReadDirectory(FsDir *d, s64 *out_total_entries, size_t max_entries, FsDirectoryEntry *buf) override {
max_entries = std::min<s64>(m_entries.size()- d->s.object_id, max_entries);
std::memcpy(buf, m_entries.data() + d->s.object_id, max_entries * sizeof(*buf));
d->s.object_id += max_entries;
*out_total_entries = max_entries;
R_SUCCEED();
}
Result GetDirectoryEntryCount(FsDir *d, s64 *out_count) override {
*out_count = m_entries.size();
R_SUCCEED();
}
void CloseDirectory(FsDir *d) override {
std::memset(d, 0, sizeof(*d));
}
protected:
std::vector<FsDirectoryEntry> m_entries;
};
struct FsDevNullProxy final : FsProxyVfs {
using FsProxyVfs::FsProxyVfs;
Result GetTotalSpace(const char *path, s64 *out) override {
*out = 1024ULL * 1024ULL * 1024ULL * 256ULL;
R_SUCCEED();
}
Result GetFreeSpace(const char *path, s64 *out) override {
*out = 1024ULL * 1024ULL * 1024ULL * 256ULL;
R_SUCCEED();
}
};
struct FsInstallProxy final : FsProxyVfs {
using FsProxyVfs::FsProxyVfs;
Result FailedIfNotEnabled() {
SCOPED_MUTEX(&g_shared_data.mutex);
if (!g_shared_data.enabled) {
App::Notify("Please launch MTP install menu before trying to install"_i18n);
R_THROW(FsError_NotImplemented);
}
R_SUCCEED();
}
Result IsValidFileType(const char* name) {
const char* ext = std::strrchr(name, '.');
if (!ext) {
R_THROW(FsError_NotImplemented);
}
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) {
R_THROW(FsError_NotImplemented);
}
R_SUCCEED();
}
Result GetTotalSpace(const char *path, s64 *out) override {
if (App::GetApp()->m_install_sd.Get()) {
return fs::FsNativeContentStorage(FsContentStorageId_SdCard).GetTotalSpace("/", out);
} else {
return fs::FsNativeContentStorage(FsContentStorageId_User).GetTotalSpace("/", out);
}
}
Result GetFreeSpace(const char *path, s64 *out) override {
if (App::GetApp()->m_install_sd.Get()) {
return fs::FsNativeContentStorage(FsContentStorageId_SdCard).GetFreeSpace("/", out);
} else {
return fs::FsNativeContentStorage(FsContentStorageId_User).GetFreeSpace("/", out);
}
}
Result GetEntryType(const char *path, FsDirEntryType *out_entry_type) override {
R_TRY(FsProxyVfs::GetEntryType(path, out_entry_type));
if (*out_entry_type == FsDirEntryType_File) {
R_TRY(FailedIfNotEnabled());
}
R_SUCCEED();
}
Result CreateFile(const char* path, s64 size, u32 option) override {
R_TRY(FailedIfNotEnabled());
R_TRY(IsValidFileType(path));
R_TRY(FsProxyVfs::CreateFile(path, size, option));
R_SUCCEED();
}
Result OpenFile(const char *path, u32 mode, FsFile *out_file) override {
R_TRY(FailedIfNotEnabled());
R_TRY(IsValidFileType(path));
R_TRY(FsProxyVfs::OpenFile(path, mode, out_file));
log_write("[MTP] done file open: %s mode: 0x%X\n", path, mode);
if (mode & FsOpenMode_Write) {
const auto& e = m_entries[out_file->s.object_id];
// check if we already have this file queued.
log_write("[MTP] checking if empty\n");
R_UNLESS(g_shared_data.current_file.empty(), FsError_NotImplemented);
log_write("[MTP] is empty\n");
g_shared_data.current_file = e.name;
on_thing();
}
log_write("[MTP] got file: %s\n", path);
R_SUCCEED();
}
Result WriteFile(FsFile *file, s64 off, const void *buf, u64 write_size, u32 option) override {
SCOPED_MUTEX(&g_shared_data.mutex);
if (!g_shared_data.enabled) {
log_write("[MTP] failing as not enabled\n");
R_THROW(FsError_NotImplemented);
}
if (!g_shared_data.on_write || !g_shared_data.on_write(buf, write_size)) {
log_write("[MTP] failing as not written\n");
R_THROW(FsError_NotImplemented);
}
R_TRY(FsProxyVfs::WriteFile(file, off, buf, write_size, option));
R_SUCCEED();
}
void CloseFile(FsFile *file) override {
bool update{};
{
SCOPED_MUTEX(&g_shared_data.mutex);
if (file->s.own_handle & FsOpenMode_Write) {
log_write("[MTP] closing current file\n");
if (g_shared_data.on_close) {
g_shared_data.on_close();
}
g_shared_data.in_progress = false;
g_shared_data.current_file.clear();
update = true;
}
}
if (update) {
on_thing();
}
FsProxyVfs::CloseFile(file);
}
};
::haze::FsEntries g_fs_entries{};
void haze_callback(const ::haze::CallbackData *data) {
auto& e = *data;
switch (e.type) {
case ::haze::CallbackType_OpenSession: log_write("[LIBHAZE] Opening Session\n"); break;
case ::haze::CallbackType_CloseSession: log_write("[LIBHAZE] Closing Session\n"); break;
case ::haze::CallbackType_CreateFile: log_write("[LIBHAZE] Creating File: %s\n", e.file.filename); break;
case ::haze::CallbackType_DeleteFile: log_write("[LIBHAZE] Deleting File: %s\n", e.file.filename); break;
case ::haze::CallbackType_RenameFile: log_write("[LIBHAZE] Rename File: %s -> %s\n", e.rename.filename, e.rename.newname); break;
case ::haze::CallbackType_RenameFolder: log_write("[LIBHAZE] Rename Folder: %s -> %s\n", e.rename.filename, e.rename.newname); break;
case ::haze::CallbackType_CreateFolder: log_write("[LIBHAZE] Creating Folder: %s\n", e.file.filename); break;
case ::haze::CallbackType_DeleteFolder: log_write("[LIBHAZE] Deleting Folder: %s\n", e.file.filename); break;
case ::haze::CallbackType_ReadBegin: log_write("[LIBHAZE] Reading File Begin: %s \n", e.file.filename); break;
case ::haze::CallbackType_ReadProgress: log_write("\t[LIBHAZE] Reading File: offset: %lld size: %lld\n", e.progress.offset, e.progress.size); break;
case ::haze::CallbackType_ReadEnd: log_write("[LIBHAZE] Reading File Finished: %s\n", e.file.filename); break;
case ::haze::CallbackType_WriteBegin: log_write("[LIBHAZE] Writing File Begin: %s \n", e.file.filename); break;
case ::haze::CallbackType_WriteProgress: log_write("\t[LIBHAZE] Writing File: offset: %lld size: %lld\n", e.progress.offset, e.progress.size); break;
case ::haze::CallbackType_WriteEnd: log_write("[LIBHAZE] Writing File Finished: %s\n", e.file.filename); break;
}
App::NotifyFlashLed();
}
} // namespace
bool Init() {
SCOPED_MUTEX(&g_mutex);
if (g_is_running) {
log_write("[MTP] already enabled, cannot open\n");
return false;
}
g_fs_entries.emplace_back(std::make_shared<FsProxy>(std::make_shared<fs::FsNativeSd>(), "", "microSD card"));
g_fs_entries.emplace_back(std::make_shared<FsProxy>(std::make_shared<fs::FsNativeImage>(FsImageDirectoryId_Nand), "image_nand", "Image nand"));
g_fs_entries.emplace_back(std::make_shared<FsProxy>(std::make_shared<fs::FsNativeImage>(FsImageDirectoryId_Sd), "image_sd", "Image sd"));
g_fs_entries.emplace_back(std::make_shared<FsDevNullProxy>("DevNull", "DevNull (Speed Test)"));
g_fs_entries.emplace_back(std::make_shared<FsInstallProxy>("install", "Install (NSP, XCI, NSZ, XCZ)"));
g_should_exit = false;
if (!::haze::Initialize(haze_callback, THREAD_PRIO, THREAD_CORE, g_fs_entries)) {
return false;
}
log_write("[MTP] started\n");
return g_is_running = true;
}
void Exit() {
SCOPED_MUTEX(&g_mutex);
if (!g_is_running) {
return;
}
::haze::Exit();
g_is_running = false;
g_should_exit = true;
g_fs_entries.clear();
log_write("[MTP] exitied\n");
}
void InitInstallMode(OnInstallStart on_start, OnInstallWrite on_write, OnInstallClose on_close) {
SCOPED_MUTEX(&g_shared_data.mutex);
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() {
SCOPED_MUTEX(&g_shared_data.mutex);
g_shared_data.enabled = false;
}
} // namespace sphaira::haze

View File

@@ -10,17 +10,27 @@ namespace {
constexpr const char* logpath = "/config/sphaira/log.txt";
std::FILE* file{};
int nxlink_socket{};
bool g_file_open{};
std::mutex mutex{};
void log_write_arg_internal(const char* s, std::va_list* v) {
if (file) {
std::vfprintf(file, s, *v);
std::fflush(file);
const auto t = std::time(nullptr);
const auto tm = std::localtime(&t);
static char buf[512];
const auto len = std::snprintf(buf, sizeof(buf), "[%02u:%02u:%02u] -> ", tm->tm_hour, tm->tm_min, tm->tm_sec);
std::vsnprintf(buf + len, sizeof(buf) - len, s, *v);
if (g_file_open) {
auto file = std::fopen(logpath, "a");
if (file) {
std::fprintf(file, "%s", buf);
std::fclose(file);
}
}
if (nxlink_socket) {
std::vprintf(s, *v);
std::printf("%s", buf);
}
}
@@ -30,12 +40,18 @@ extern "C" {
auto log_file_init() -> bool {
std::scoped_lock lock{mutex};
if (file) {
if (g_file_open) {
return false;
}
file = std::fopen(logpath, "w");
return file != nullptr;
auto file = std::fopen(logpath, "w");
if (file) {
g_file_open = true;
std::fclose(file);
return true;
}
return false;
}
auto log_nxlink_init() -> bool {
@@ -50,9 +66,8 @@ auto log_nxlink_init() -> bool {
void log_file_exit() {
std::scoped_lock lock{mutex};
if (file) {
std::fclose(file);
file = nullptr;
if (g_file_open) {
g_file_open = false;
}
}
@@ -64,12 +79,17 @@ void log_nxlink_exit() {
}
}
void log_write(const char* s, ...) {
bool log_is_init() {
std::scoped_lock lock{mutex};
if (!file && !nxlink_socket) {
return g_file_open || nxlink_socket;
}
void log_write(const char* s, ...) {
if (!log_is_init()) {
return;
}
std::scoped_lock lock{mutex};
std::va_list v{};
va_start(v, s);
log_write_arg_internal(s, &v);
@@ -77,11 +97,11 @@ void log_write(const char* s, ...) {
}
void log_write_arg(const char* s, va_list* v) {
std::scoped_lock lock{mutex};
if (!file && !nxlink_socket) {
if (!log_is_init()) {
return;
}
std::scoped_lock lock{mutex};
log_write_arg_internal(s, v);
}

View File

@@ -16,7 +16,7 @@ int main(int argc, char** argv) {
extern "C" {
void userAppInit(void) {
Result rc;
sphaira::App::SetBoostMode(true);
// https://github.com/mtheall/ftpd/blob/e27898f0c3101522311f330e82a324861e0e3f7e/source/switch/init.c#L31
const SocketInitConfig socket_config_application = {
@@ -47,6 +47,7 @@ void userAppInit(void) {
const auto socket_config = is_application ? socket_config_application : socket_config_applet;
Result rc;
if (R_FAILED(rc = appletLockExit()))
diagAbortWithResult(rc);
if (R_FAILED(rc = socketInitialize(&socket_config)))
@@ -87,6 +88,8 @@ void userAppExit(void) {
if (auto fs = fsdevGetDeviceFileSystem("sdmc:")) {
fsFsCommit(fs);
}
sphaira::App::SetBoostMode(false);
appletUnlockExit();
}

View File

@@ -13,15 +13,6 @@
namespace sphaira {
namespace {
enum {
Module_Nro = 421,
};
enum NroError {
NroError_BadMagic = MAKERESULT(Module_Nro, 1),
NroError_BadSize = MAKERESULT(Module_Nro, 2),
};
struct NroData {
NroStart start;
NroHeader header;
@@ -47,11 +38,11 @@ auto nro_parse_internal(fs::Fs* fs, const fs::FsPath& path, NroEntry& entry) ->
NroData data;
u64 bytes_read;
R_TRY(f.Read(0, &data, sizeof(data), FsReadOption_None, &bytes_read));
R_UNLESS(data.header.magic == NROHEADER_MAGIC, NroError_BadMagic);
R_UNLESS(data.header.magic == NROHEADER_MAGIC, Result_NroBadMagic);
NroAssetHeader asset;
R_TRY(f.Read(data.header.size, &asset, sizeof(asset), FsReadOption_None, &bytes_read));
// R_UNLESS(asset.magic == NROASSETHEADER_MAGIC, NroError_BadMagic);
// R_UNLESS(asset.magic == NROASSETHEADER_MAGIC, Result_NroBadMagic);
// we can avoid a GetSize() call by calculating the size manually.
entry.size = data.header.size;
@@ -68,18 +59,20 @@ auto nro_parse_internal(fs::Fs* fs, const fs::FsPath& path, NroEntry& entry) ->
std::strncpy(nacp.lang.name, file_name, file_name_len - 4);
std::strcpy(nacp.lang.author, "Unknown");
std::strcpy(nacp.display_version, "Unknown");
entry.icon_offset = entry.icon_size = 0;
entry.is_nacp_valid = false;
} else {
entry.size += sizeof(asset) + asset.icon.size + asset.nacp.size + asset.romfs.size;
R_TRY(f.Read(data.header.size + asset.nacp.offset, &nacp.lang, sizeof(nacp.lang), FsReadOption_None, &bytes_read));
R_TRY(f.Read(data.header.size + asset.nacp.offset + offsetof(NacpStruct, display_version), nacp.display_version, sizeof(nacp.display_version), FsReadOption_None, &bytes_read));
// lazy load the icons
entry.icon_size = asset.icon.size;
entry.icon_offset = data.header.size + asset.icon.offset;
entry.is_nacp_valid = true;
}
// lazy load the icons
entry.icon_size = asset.icon.size;
entry.icon_offset = data.header.size + asset.icon.offset;
R_SUCCEED();
}
@@ -192,10 +185,10 @@ auto launch_internal(const std::string& path, const std::string& argv) -> Result
auto nro_verify(std::span<const u8> data) -> Result {
NroData nro;
R_UNLESS(data.size() >= sizeof(nro), NroError_BadSize);
R_UNLESS(data.size() >= sizeof(nro), Result_NroBadSize);
memcpy(&nro, data.data(), sizeof(nro));
R_UNLESS(nro.header.magic == NROHEADER_MAGIC, NroError_BadMagic);
R_UNLESS(nro.header.magic == NROHEADER_MAGIC, Result_NroBadMagic);
R_SUCCEED();
}
@@ -243,9 +236,9 @@ auto nro_get_nacp(const fs::FsPath& path, NacpStruct& nacp) -> Result {
R_TRY(fs.OpenFile(path, FsOpenMode_Read, &f));
R_TRY(f.Read(0, &data, sizeof(data), FsReadOption_None, &bytes_read));
R_UNLESS(data.header.magic == NROHEADER_MAGIC, NroError_BadMagic);
R_UNLESS(data.header.magic == NROHEADER_MAGIC, Result_NroBadMagic);
R_TRY(f.Read(data.header.size, &asset, sizeof(asset), FsReadOption_None, &bytes_read));
R_UNLESS(asset.magic == NROASSETHEADER_MAGIC, NroError_BadMagic);
R_UNLESS(asset.magic == NROASSETHEADER_MAGIC, Result_NroBadMagic);
R_TRY(f.Read(data.header.size + asset.nacp.offset, &nacp, sizeof(nacp), FsReadOption_None, &bytes_read));
R_SUCCEED();

View File

@@ -842,7 +842,7 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
pbox->SetTitle(config.name);
pbox->SetImageDataConst(config.icon);
R_UNLESS(!config.nro_path.empty(), OwoError_BadArgs);
R_UNLESS(!config.nro_path.empty(), Result_OwoBadArgs);
// R_UNLESS(!config.icon.empty(), OwoError_BadArgs);
R_TRY(splCryptoInitialize());
@@ -962,7 +962,7 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
// setup database
{
pbox->NewTransfer("Updating ncm databse"_i18n).UpdateTransfer(4, 8);
pbox->NewTransfer("Updating ncm database"_i18n).UpdateTransfer(4, 8);
NcmContentMetaDatabase db;
R_TRY(ncmOpenContentMetaDatabase(&db, storage_id));
ON_SCOPE_EXIT(ncmContentMetaDatabaseClose(&db));

View File

@@ -155,7 +155,7 @@ ThreadData::ThreadData(ui::ProgressBox* _pbox, s64 size, ReadCallback _rfunc, Wr
}
auto ThreadData::GetResults() -> Result {
R_UNLESS(!pbox->ShouldExit(), 0x1);
R_UNLESS(!pbox->ShouldExit(), Result_TransferCancelled);
R_TRY(read_result);
R_TRY(write_result);
R_TRY(pull_result);
@@ -416,21 +416,21 @@ Result TransferInternal(ui::ProgressBox* pbox, s64 size, ReadCallback rfunc, Wri
} // namespace
Result Transfer(ui::ProgressBox* pbox, s64 size, ReadCallback rfunc, WriteCallback wfunc, Mode mode) {
return TransferInternal(pbox, size, rfunc, wfunc, nullptr, Mode::MultiThreaded);
return TransferInternal(pbox, size, rfunc, wfunc, nullptr, mode);
}
Result TransferPull(ui::ProgressBox* pbox, s64 size, ReadCallback rfunc, StartCallback sfunc, Mode mode) {
return TransferInternal(pbox, size, rfunc, nullptr, [sfunc](StartThreadCallback start, PullCallback pull) -> Result {
R_TRY(start());
return sfunc(pull);
}, Mode::MultiThreaded);
}, mode);
}
Result TransferPull(ui::ProgressBox* pbox, s64 size, ReadCallback rfunc, StartCallback2 sfunc, Mode mode) {
return TransferInternal(pbox, size, rfunc, nullptr, sfunc, Mode::MultiThreaded);
return TransferInternal(pbox, size, rfunc, nullptr, sfunc, mode);
}
Result TransferUnzip(ui::ProgressBox* pbox, void* zfile, fs::Fs* fs, const fs::FsPath& path, s64 size, u32 crc32) {
Result TransferUnzip(ui::ProgressBox* pbox, void* zfile, fs::Fs* fs, const fs::FsPath& path, s64 size, u32 crc32, Mode mode) {
Result rc;
if (R_FAILED(rc = fs->CreateDirectoryRecursivelyWithPath(path)) && rc != FsError_PathAlreadyExists) {
log_write("failed to create folder: %s 0x%04X\n", path.s, rc);
@@ -458,7 +458,7 @@ Result TransferUnzip(ui::ProgressBox* pbox, void* zfile, fs::Fs* fs, const fs::F
const auto result = unzReadCurrentFile(zfile, data, size);
if (result <= 0) {
log_write("failed to read zip file: %s %d\n", path.s, result);
R_THROW(0x1);
R_THROW(Result_UnzReadCurrentFile);
}
if (crc32) {
@@ -471,7 +471,7 @@ Result TransferUnzip(ui::ProgressBox* pbox, void* zfile, fs::Fs* fs, const fs::F
[&](const void* data, s64 off, s64 size) -> Result {
return f.Write(off, data, size, FsWriteOption_None);
},
nullptr, Mode::SingleThreadedIfSmaller, SMALL_BUFFER_SIZE
nullptr, mode, SMALL_BUFFER_SIZE
));
// validate crc32 (if set in the info).
@@ -480,7 +480,7 @@ Result TransferUnzip(ui::ProgressBox* pbox, void* zfile, fs::Fs* fs, const fs::F
R_SUCCEED();
}
Result TransferZip(ui::ProgressBox* pbox, void* zfile, fs::Fs* fs, const fs::FsPath& path, u32* crc32) {
Result TransferZip(ui::ProgressBox* pbox, void* zfile, fs::Fs* fs, const fs::FsPath& path, u32* crc32, Mode mode) {
fs::File f;
R_TRY(fs->OpenFile(path, FsOpenMode_Read, &f));
@@ -502,22 +502,22 @@ Result TransferZip(ui::ProgressBox* pbox, void* zfile, fs::Fs* fs, const fs::FsP
[&](const void* data, s64 off, s64 size) -> Result {
if (ZIP_OK != zipWriteInFileInZip(zfile, data, size)) {
log_write("failed to write zip file: %s\n", path.s);
R_THROW(0x1);
R_THROW(Result_ZipWriteInFileInZip);
}
R_SUCCEED();
},
nullptr, Mode::SingleThreadedIfSmaller, SMALL_BUFFER_SIZE
nullptr, mode, SMALL_BUFFER_SIZE
);
}
Result TransferUnzipAll(ui::ProgressBox* pbox, void* zfile, fs::Fs* fs, const fs::FsPath& base_path, UnzipAllFilter filter) {
Result TransferUnzipAll(ui::ProgressBox* pbox, void* zfile, fs::Fs* fs, const fs::FsPath& base_path, UnzipAllFilter filter, Mode mode) {
unz_global_info64 ginfo;
if (UNZ_OK != unzGetGlobalInfo64(zfile, &ginfo)) {
R_THROW(0x1);
R_THROW(Result_UnzGetGlobalInfo64);
}
if (UNZ_OK != unzGoToFirstFile(zfile)) {
R_THROW(0x1);
R_THROW(Result_UnzGoToFirstFile);
}
for (s64 i = 0; i < ginfo.number_entry; i++) {
@@ -526,13 +526,13 @@ Result TransferUnzipAll(ui::ProgressBox* pbox, void* zfile, fs::Fs* fs, const fs
if (i > 0) {
if (UNZ_OK != unzGoToNextFile(zfile)) {
log_write("failed to unzGoToNextFile\n");
R_THROW(0x1);
R_THROW(Result_UnzGoToNextFile);
}
}
if (UNZ_OK != unzOpenCurrentFile(zfile)) {
log_write("failed to open current file\n");
R_THROW(0x1);
R_THROW(Result_UnzOpenCurrentFile);
}
ON_SCOPE_EXIT(unzCloseCurrentFile(zfile));
@@ -540,7 +540,7 @@ Result TransferUnzipAll(ui::ProgressBox* pbox, void* zfile, fs::Fs* fs, const fs
fs::FsPath name;
if (UNZ_OK != unzGetCurrentFileInfo64(zfile, &info, name, sizeof(name), 0, 0, 0, 0)) {
log_write("failed to get current info\n");
R_THROW(0x1);
R_THROW(Result_UnzGetCurrentFileInfo64);
}
// check if we should skip this file.
@@ -560,22 +560,22 @@ Result TransferUnzipAll(ui::ProgressBox* pbox, void* zfile, fs::Fs* fs, const fs
R_THROW(rc);
}
} else {
R_TRY(TransferUnzip(pbox, zfile, fs, path, info.uncompressed_size, info.crc));
R_TRY(TransferUnzip(pbox, zfile, fs, path, info.uncompressed_size, info.crc, mode));
}
}
R_SUCCEED();
}
Result TransferUnzipAll(ui::ProgressBox* pbox, const fs::FsPath& zip_out, fs::Fs* fs, const fs::FsPath& base_path, UnzipAllFilter filter) {
Result TransferUnzipAll(ui::ProgressBox* pbox, const fs::FsPath& zip_out, fs::Fs* fs, const fs::FsPath& base_path, UnzipAllFilter filter, Mode mode) {
zlib_filefunc64_def file_func;
mz::FileFuncStdio(&file_func);
auto zfile = unzOpen2_64(zip_out, &file_func);
R_UNLESS(zfile, 0x1);
R_UNLESS(zfile, Result_UnzOpen2_64);
ON_SCOPE_EXIT(unzClose(zfile));
return TransferUnzipAll(pbox, zfile, fs, base_path, filter);
return TransferUnzipAll(pbox, zfile, fs, base_path, filter, mode);
}
} // namespace::thread

View File

@@ -4,6 +4,126 @@
#include "i18n.hpp"
namespace sphaira::ui {
namespace {
auto GetCodeMessage(Result rc) -> const char* {
switch (rc) {
case FsError_PathNotFound: return "FsError_PathNotFound";
case FsError_PathAlreadyExists: return "FsError_PathAlreadyExists";
case FsError_TargetLocked: return "FsError_TargetLocked";
case Result_TransferCancelled: return "SphairaError_TransferCancelled";
case Result_StreamBadSeek: return "SphairaError_StreamBadSeek";
case Result_FsTooManyEntries: return "SphairaError_FsTooManyEntries";
case Result_FsNewPathTooLarge: return "SphairaError_FsNewPathTooLarge";
case Result_FsInvalidType: return "SphairaError_FsInvalidType";
case Result_FsEmpty: return "SphairaError_FsEmpty";
case Result_FsAlreadyRoot: return "SphairaError_FsAlreadyRoot";
case Result_FsNoCurrentPath: return "SphairaError_FsNoCurrentPath";
case Result_FsBrokenCurrentPath: return "SphairaError_FsBrokenCurrentPath";
case Result_FsIndexOutOfBounds: return "SphairaError_FsIndexOutOfBounds";
case Result_FsFsNotActive: return "SphairaError_FsFsNotActive";
case Result_FsNewPathEmpty: return "SphairaError_FsNewPathEmpty";
case Result_FsLoadingCancelled: return "SphairaError_FsLoadingCancelled";
case Result_FsBrokenRoot: return "SphairaError_FsBrokenRoot";
case Result_FsUnknownStdioError: return "SphairaError_FsUnknownStdioError";
case Result_FsReadOnly: return "SphairaError_FsReadOnly";
case Result_FsNotActive: return "SphairaError_FsNotActive";
case Result_FsFailedStdioStat: return "SphairaError_FsFailedStdioStat";
case Result_FsFailedStdioOpendir: return "SphairaError_FsFailedStdioOpendir";
case Result_NroBadMagic: return "SphairaError_NroBadMagic";
case Result_NroBadSize: return "SphairaError_NroBadSize";
case Result_AppFailedMusicDownload: return "SphairaError_AppFailedMusicDownload";
case Result_CurlFailedEasyInit: return "SphairaError_CurlFailedEasyInit";
case Result_DumpFailedNetworkUpload: return "SphairaError_DumpFailedNetworkUpload";
case Result_UnzOpen2_64: return "SphairaError_UnzOpen2_64";
case Result_UnzGetGlobalInfo64: return "SphairaError_UnzGetGlobalInfo64";
case Result_UnzLocateFile: return "SphairaError_UnzLocateFile";
case Result_UnzGoToFirstFile: return "SphairaError_UnzGoToFirstFile";
case Result_UnzGoToNextFile: return "SphairaError_UnzGoToNextFile";
case Result_UnzOpenCurrentFile: return "SphairaError_UnzOpenCurrentFile";
case Result_UnzGetCurrentFileInfo64: return "SphairaError_UnzGetCurrentFileInfo64";
case Result_UnzReadCurrentFile: return "SphairaError_UnzReadCurrentFile";
case Result_ZipOpen2_64: return "SphairaError_ZipOpen2_64";
case Result_ZipOpenNewFileInZip: return "SphairaError_ZipOpenNewFileInZip";
case Result_ZipWriteInFileInZip: return "SphairaError_ZipWriteInFileInZip";
case Result_FileBrowserFailedUpload: return "SphairaError_FileBrowserFailedUpload";
case Result_FileBrowserDirNotDaybreak: return "SphairaError_FileBrowserDirNotDaybreak";
case Result_AppstoreFailedZipDownload: return "SphairaError_AppstoreFailedZipDownload";
case Result_AppstoreFailedMd5: return "SphairaError_AppstoreFailedMd5";
case Result_AppstoreFailedParseManifest: return "SphairaError_AppstoreFailedParseManifest";
case Result_GameBadReadForDump: return "SphairaError_GameBadReadForDump";
case Result_GameEmptyMetaEntries: return "SphairaError_GameEmptyMetaEntries";
case Result_GameMultipleKeysFound: return "SphairaError_GameMultipleKeysFound";
case Result_GameNoNspEntriesBuilt: return "SphairaError_GameNoNspEntriesBuilt";
case Result_KeyMissingNcaKeyArea: return "SphairaError_KeyMissingNcaKeyArea";
case Result_KeyMissingTitleKek: return "SphairaError_KeyMissingTitleKek";
case Result_KeyMissingMasterKey: return "SphairaError_KeyMissingMasterKey";
case Result_KeyFailedDecyptETicketDeviceKey: return "SphairaError_KeyFailedDecyptETicketDeviceKey";
case Result_NcaFailedNcaHeaderHashVerify: return "SphairaError_NcaFailedNcaHeaderHashVerify";
case Result_NcaBadSigKeyGen: return "SphairaError_NcaBadSigKeyGen";
case Result_GcBadReadForDump: return "SphairaError_GcBadReadForDump";
case Result_GcEmptyGamecard: return "SphairaError_GcEmptyGamecard";
case Result_GcBadXciMagic: return "SphairaError_GcBadXciMagic";
case Result_GcBadXciRomSize: return "SphairaError_GcBadXciRomSize";
case Result_GcFailedToGetSecurityInfo: return "SphairaError_GcFailedToGetSecurityInfo";
case Result_GhdlEmptyAsset: return "SphairaError_GhdlEmptyAsset";
case Result_GhdlFailedToDownloadAsset: return "SphairaError_GhdlFailedToDownloadAsset";
case Result_GhdlFailedToDownloadAssetJson: return "SphairaError_GhdlFailedToDownloadAssetJson";
case Result_ThemezerFailedToDownloadThemeMeta: return "SphairaError_ThemezerFailedToDownloadThemeMeta";
case Result_ThemezerFailedToDownloadTheme: return "SphairaError_ThemezerFailedToDownloadTheme";
case Result_MainFailedToDownloadUpdate: return "SphairaError_MainFailedToDownloadUpdate";
case Result_UsbDsBadDeviceSpeed: return "SphairaError_UsbDsBadDeviceSpeed";
case Result_NspBadMagic: return "SphairaError_NspBadMagic";
case Result_XciBadMagic: return "SphairaError_XciBadMagic";
case Result_EsBadTitleKeyType: return "SphairaError_EsBadTitleKeyType";
case Result_EsPersonalisedTicketDeviceIdMissmatch: return "SphairaError_EsPersonalisedTicketDeviceIdMissmatch";
case Result_EsFailedDecryptPersonalisedTicket: return "SphairaError_EsFailedDecryptPersonalisedTicket";
case Result_EsBadDecryptedPersonalisedTicketSize: return "SphairaError_EsBadDecryptedPersonalisedTicketSize";
case Result_EsInvalidTicketBadRightsId: return "SphairaError_EsInvalidTicketBadRightsId";
case Result_EsInvalidTicketFromatVersion: return "SphairaError_EsInvalidTicketFromatVersion";
case Result_EsInvalidTicketKeyType: return "SphairaError_EsInvalidTicketKeyType";
case Result_EsInvalidTicketKeyRevision: return "SphairaError_EsInvalidTicketKeyRevision";
case Result_OwoBadArgs: return "SphairaError_OwoBadArgs";
case Result_UsbCancelled: return "SphairaError_UsbCancelled";
case Result_UsbBadMagic: return "SphairaError_UsbBadMagic";
case Result_UsbBadVersion: return "SphairaError_UsbBadVersion";
case Result_UsbBadCount: return "SphairaError_UsbBadCount";
case Result_UsbBadTransferSize: return "SphairaError_UsbBadTransferSize";
case Result_UsbBadTotalSize: return "SphairaError_UsbBadTotalSize";
case Result_UsbUploadBadMagic: return "SphairaError_UsbUploadBadMagic";
case Result_UsbUploadExit: return "SphairaError_UsbUploadExit";
case Result_UsbUploadBadCount: return "SphairaError_UsbUploadBadCount";
case Result_UsbUploadBadTransferSize: return "SphairaError_UsbUploadBadTransferSize";
case Result_UsbUploadBadTotalSize: return "SphairaError_UsbUploadBadTotalSize";
case Result_UsbUploadBadCommand: return "SphairaError_UsbUploadBadCommand";
case Result_YatiContainerNotFound: return "SphairaError_YatiContainerNotFound";
case Result_YatiNcaNotFound: return "SphairaError_YatiNcaNotFound";
case Result_YatiInvalidNcaReadSize: return "SphairaError_YatiInvalidNcaReadSize";
case Result_YatiInvalidNcaSigKeyGen: return "SphairaError_YatiInvalidNcaSigKeyGen";
case Result_YatiInvalidNcaMagic: return "SphairaError_YatiInvalidNcaMagic";
case Result_YatiInvalidNcaSignature0: return "SphairaError_YatiInvalidNcaSignature0";
case Result_YatiInvalidNcaSignature1: return "SphairaError_YatiInvalidNcaSignature1";
case Result_YatiInvalidNcaSha256: return "SphairaError_YatiInvalidNcaSha256";
case Result_YatiNczSectionNotFound: return "SphairaError_YatiNczSectionNotFound";
case Result_YatiInvalidNczSectionCount: return "SphairaError_YatiInvalidNczSectionCount";
case Result_YatiNczBlockNotFound: return "SphairaError_YatiNczBlockNotFound";
case Result_YatiInvalidNczBlockVersion: return "SphairaError_YatiInvalidNczBlockVersion";
case Result_YatiInvalidNczBlockType: return "SphairaError_YatiInvalidNczBlockType";
case Result_YatiInvalidNczBlockTotal: return "SphairaError_YatiInvalidNczBlockTotal";
case Result_YatiInvalidNczBlockSizeExponent: return "SphairaError_YatiInvalidNczBlockSizeExponent";
case Result_YatiInvalidNczZstdError: return "SphairaError_YatiInvalidNczZstdError";
case Result_YatiTicketNotFound: return "SphairaError_YatiTicketNotFound";
case Result_YatiInvalidTicketBadRightsId: return "SphairaError_YatiInvalidTicketBadRightsId";
case Result_YatiCertNotFound: return "SphairaError_YatiCertNotFound";
case Result_YatiNcmDbCorruptHeader: return "SphairaError_YatiNcmDbCorruptHeader";
case Result_YatiNcmDbCorruptInfos: return "SphairaError_YatiNcmDbCorruptInfos";
}
return "";
}
} // namespace
ErrorBox::ErrorBox(const std::string& message) : m_message{message} {
log_write("[ERROR] %s\n", m_message.c_str());
@@ -22,6 +142,7 @@ ErrorBox::ErrorBox(const std::string& message) : m_message{message} {
ErrorBox::ErrorBox(Result code, const std::string& message) : ErrorBox{message} {
m_code = code;
m_code_message = GetCodeMessage(code);
log_write("[ERROR] Code: 0x%X Module: %u Description: %u\n", R_VALUE(code), R_MODULE(code), R_DESCRIPTION(code));
}
@@ -39,7 +160,11 @@ auto ErrorBox::Draw(NVGcontext* vg, Theme* theme) -> void {
gfx::drawTextArgs(vg, center_x, 180, 63, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_ERROR), "\uE140");
if (m_code.has_value()) {
const auto code = m_code.value();
gfx::drawTextArgs(vg, center_x, 270, 25, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Code: 0x%X Module: %u Description: 0x%X", R_VALUE(code), R_MODULE(code), R_DESCRIPTION(code));
if (m_code_message.empty()) {
gfx::drawTextArgs(vg, center_x, 270, 25, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Code: 0x%X Module: %u Description: 0x%X", R_VALUE(code), R_MODULE(code), R_DESCRIPTION(code));
} else {
gfx::drawTextArgs(vg, center_x, 270, 25, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%s", m_code_message.c_str());
}
} else {
gfx::drawTextArgs(vg, center_x, 270, 25, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "An error occurred"_i18n.c_str());
}

View File

@@ -1,4 +1,5 @@
#include "ui/menus/appstore.hpp"
#include "ui/menus/homebrew.hpp"
#include "ui/sidebar.hpp"
#include "ui/popup_list.hpp"
#include "ui/progress_box.hpp"
@@ -329,8 +330,9 @@ auto UninstallApp(ProgressBox* pbox, const Entry& entry) -> Result {
fs::FsNativeSd fs;
if (manifest.empty()) {
R_UNLESS(!entry.binary.empty(), 0x1);
fs.DeleteFile(entry.binary);
if (!entry.binary.empty()) {
R_TRY(fs.DeleteFile(entry.binary));
}
} else {
for (auto& e : manifest) {
pbox->NewTransfer(e.path);
@@ -396,7 +398,7 @@ auto InstallApp(ProgressBox* pbox, const Entry& entry) -> Result {
api_result = curl::ToMemory(api);
}
R_UNLESS(api_result.success, 0x1);
R_UNLESS(api_result.success, Result_AppstoreFailedZipDownload);
}
ON_SCOPE_EXIT(fs.DeleteFile(zip_out));
@@ -415,7 +417,7 @@ auto InstallApp(ProgressBox* pbox, const Entry& entry) -> Result {
if (strncasecmp(hash_out.data(), entry.md5.data(), entry.md5.length())) {
log_write("bad md5: %.*s vs %.*s\n", 32, hash_out.data(), 32, entry.md5.c_str());
R_THROW(0x1);
R_THROW(Result_AppstoreFailedMd5);
}
}
@@ -430,13 +432,13 @@ auto InstallApp(ProgressBox* pbox, const Entry& entry) -> Result {
// 3. extract the zip
if (!pbox->ShouldExit()) {
auto zfile = unzOpen2_64(zip_out, &file_func);
R_UNLESS(zfile, 0x1);
R_UNLESS(zfile, Result_UnzOpen2_64);
ON_SCOPE_EXIT(unzClose(zfile));
// get manifest
if (UNZ_END_OF_LIST_OF_FILE == unzLocateFile(zfile, "manifest.install", 0)) {
log_write("failed to find manifest.install\n");
R_THROW(0x1);
R_THROW(Result_UnzLocateFile);
}
ManifestEntries new_manifest;
@@ -444,26 +446,26 @@ auto InstallApp(ProgressBox* pbox, const Entry& entry) -> Result {
{
if (UNZ_OK != unzOpenCurrentFile(zfile)) {
log_write("failed to open current file\n");
R_THROW(0x1);
R_THROW(Result_UnzOpenCurrentFile);
}
ON_SCOPE_EXIT(unzCloseCurrentFile(zfile));
unz_file_info64 info;
if (UNZ_OK != unzGetCurrentFileInfo64(zfile, &info, 0, 0, 0, 0, 0, 0)) {
log_write("failed to get current info\n");
R_THROW(0x1);
R_THROW(Result_UnzGetGlobalInfo64);
}
std::vector<char> manifest_data(info.uncompressed_size);
if ((int)info.uncompressed_size != unzReadCurrentFile(zfile, manifest_data.data(), manifest_data.size())) {
log_write("failed to read manifest file\n");
R_THROW(0x1);
R_THROW(Result_UnzReadCurrentFile);
}
new_manifest = ParseManifest(manifest_data);
if (new_manifest.empty()) {
log_write("manifest is empty!\n");
R_THROW(0x1);
R_THROW(Result_AppstoreFailedParseManifest);
}
}
@@ -472,19 +474,19 @@ auto InstallApp(ProgressBox* pbox, const Entry& entry) -> Result {
if (UNZ_END_OF_LIST_OF_FILE == unzLocateFile(zfile, inzip, 0)) {
log_write("failed to find %s\n", inzip.s);
R_THROW(0x1);
R_THROW(Result_UnzLocateFile);
}
if (UNZ_OK != unzOpenCurrentFile(zfile)) {
log_write("failed to open current file\n");
R_THROW(0x1);
R_THROW(Result_UnzOpenCurrentFile);
}
ON_SCOPE_EXIT(unzCloseCurrentFile(zfile));
unz_file_info64 info;
if (UNZ_OK != unzGetCurrentFileInfo64(zfile, &info, 0, 0, 0, 0, 0, 0)) {
log_write("failed to get current info\n");
R_THROW(0x1);
R_THROW(Result_UnzGetCurrentFileInfo64);
}
auto path = output;
@@ -798,6 +800,7 @@ void EntryMenu::UpdateOptions() {
App::Push(std::make_shared<ProgressBox>(m_entry.image.image, "Downloading "_i18n, m_entry.title, [this](auto pbox){
return InstallApp(pbox, m_entry);
}, [this](Result rc){
homebrew::SignalChange();
App::PushErrorBox(rc, "Failed to, TODO: add message here"_i18n);
if (R_SUCCEEDED(rc)) {
@@ -813,6 +816,7 @@ void EntryMenu::UpdateOptions() {
App::Push(std::make_shared<ProgressBox>(m_entry.image.image, "Uninstalling "_i18n, m_entry.title, [this](auto pbox){
return UninstallApp(pbox, m_entry);
}, [this](Result rc){
homebrew::SignalChange();
App::PushErrorBox(rc, "Failed to, TODO: add message here"_i18n);
if (R_SUCCEEDED(rc)) {
@@ -1166,8 +1170,8 @@ void Menu::SetIndex(s64 index) {
}
void Menu::ScanHomebrew() {
appletSetCpuBoostMode(ApmCpuBoostMode_FastLoad);
ON_SCOPE_EXIT(appletSetCpuBoostMode(ApmCpuBoostMode_Normal));
App::SetBoostMode(true);
ON_SCOPE_EXIT(App::SetBoostMode(false));
from_json(REPO_PATH, m_entries);

View File

@@ -44,6 +44,8 @@
namespace sphaira::ui::menu::filebrowser {
namespace {
constinit UEvent g_change_uevent;
constexpr FsEntry FS_ENTRY_DEFAULT{
"microSD card", "/", FsType::Sd, FsEntryFlag_Assoc,
};
@@ -289,6 +291,10 @@ auto GetRomIcon(fs::Fs* fs, ProgressBox* pbox, std::string filename, const RomDa
} // namespace
void SignalChange() {
ueventSignal(&g_change_uevent);
}
FsView::FsView(Menu* menu, const fs::FsPath& path, const FsEntry& entry, ViewSide side) : m_menu{menu}, m_side{side} {
this->SetActions(
std::make_pair(Button::L2, Action{[this](){
@@ -722,10 +728,12 @@ void FsView::UnzipFiles(fs::FsPath dir_path) {
}
App::Push(std::make_shared<ui::ProgressBox>(0, "Extracting "_i18n, "", [this, dir_path, targets](auto pbox) -> Result {
const auto is_hdd_fs = m_fs->Root().starts_with("ums");
for (auto& e : targets) {
pbox->SetTitle(e.GetName());
const auto zip_out = GetNewPath(e);
R_TRY(thread::TransferUnzipAll(pbox, zip_out, m_fs.get(), dir_path));
R_TRY(thread::TransferUnzipAll(pbox, zip_out, m_fs.get(), dir_path, nullptr, is_hdd_fs ? thread::Mode::SingleThreaded : thread::Mode::SingleThreadedIfSmaller));
}
R_SUCCEED();
@@ -780,6 +788,7 @@ void FsView::ZipFiles(fs::FsPath zip_out) {
App::Push(std::make_shared<ui::ProgressBox>(0, "Compressing "_i18n, "", [this, zip_out, targets](auto pbox) -> Result {
const auto t = std::time(NULL);
const auto tm = std::localtime(&t);
const auto is_hdd_fs = m_fs->Root().starts_with("ums");
// pre-calculate the time rather than calculate it in the loop.
zip_fileinfo zip_info{};
@@ -794,7 +803,7 @@ void FsView::ZipFiles(fs::FsPath zip_out) {
mz::FileFuncStdio(&file_func);
auto zfile = zipOpen2_64(zip_out, APPEND_STATUS_CREATE, nullptr, &file_func);
R_UNLESS(zfile, 0x1);
R_UNLESS(zfile, Result_ZipOpen2_64);
ON_SCOPE_EXIT(zipClose(zfile, "sphaira v" APP_VERSION_HASH));
const auto zip_add = [&](const fs::FsPath& file_path) -> Result {
@@ -807,7 +816,7 @@ void FsView::ZipFiles(fs::FsPath zip_out) {
}
// root paths are banned in zips, they will warn when extracting otherwise.
if (file_name_in_zip[0] == '/') {
while (file_name_in_zip[0] == '/') {
file_name_in_zip++;
}
@@ -815,11 +824,11 @@ void FsView::ZipFiles(fs::FsPath zip_out) {
if (ZIP_OK != zipOpenNewFileInZip(zfile, file_name_in_zip, &zip_info, NULL, 0, NULL, 0, NULL, Z_DEFLATED, Z_DEFAULT_COMPRESSION)) {
log_write("failed to add zip for %s\n", file_path.s);
R_THROW(0x1);
R_THROW(Result_ZipOpenNewFileInZip);
}
ON_SCOPE_EXIT(zipCloseFileInZip(zfile));
return thread::TransferZip(pbox, zfile, m_fs.get(), file_path);
return thread::TransferZip(pbox, zfile, m_fs.get(), file_path, nullptr, is_hdd_fs ? thread::Mode::SingleThreaded : thread::Mode::SingleThreadedIfSmaller);
};
for (auto& e : targets) {
@@ -921,7 +930,7 @@ void FsView::UploadFiles() {
}
);
R_UNLESS(result.success, 0x1);
R_UNLESS(result.success, Result_FileBrowserFailedUpload);
R_SUCCEED();
}
);
@@ -962,8 +971,8 @@ void FsView::UploadFiles() {
}
auto FsView::Scan(const fs::FsPath& new_path, bool is_walk_up) -> Result {
appletSetCpuBoostMode(ApmCpuBoostMode_FastLoad);
ON_SCOPE_EXIT(appletSetCpuBoostMode(ApmCpuBoostMode_Normal));
App::SetBoostMode(true);
ON_SCOPE_EXIT(App::SetBoostMode(false));
log_write("new scan path: %s\n", new_path.s);
if (!is_walk_up && !m_path.empty() && !m_entries_current.empty()) {
@@ -1083,13 +1092,17 @@ void FsView::Sort() {
std::sort(m_entries_current.begin(), m_entries_current.end(), sorter);
}
void FsView::SortAndFindLastFile() {
void FsView::SortAndFindLastFile(bool scan) {
std::optional<LastFile> last_file;
if (!m_path.empty() && !m_entries_current.empty()) {
last_file = LastFile(GetEntry().name, m_index, m_list->GetYoff(), m_entries_current.size());
}
Sort();
if (scan) {
Scan(m_path);
} else {
Sort();
}
if (last_file.has_value()) {
SetIndexFromLastFile(*last_file);
@@ -1170,7 +1183,7 @@ void FsView::OnDeleteCallback() {
}
}
return DeleteAllCollections(pbox, src_fs, selected, collections);
return DeleteAllCollectionsWithSelected(pbox, src_fs, selected, collections);
}, [this](Result rc){
App::PushErrorBox(rc, "Failed to, TODO: add message here"_i18n);
@@ -1197,6 +1210,7 @@ void FsView::OnPasteCallback() {
App::Push(std::make_shared<ProgressBox>(0, "Pasting"_i18n, "", [this](auto pbox) -> Result {
auto& selected = m_menu->m_selected;
auto src_fs = selected.m_view->GetFs();
const auto is_same_fs = selected.SameFs(this);
if (selected.SameFs(this) && selected.m_type == SelectedType::Cut) {
for (const auto& p : selected.m_files) {
@@ -1261,7 +1275,7 @@ void FsView::OnPasteCallback() {
} else {
pbox->SetTitle(p.name);
pbox->NewTransfer("Copying "_i18n + src_path);
R_TRY(pbox->CopyFile(src_fs, m_fs.get(), src_path, dst_path));
R_TRY(pbox->CopyFile(src_fs, m_fs.get(), src_path, dst_path, is_same_fs));
R_TRY(on_paste_file(src_path, dst_path));
}
}
@@ -1291,7 +1305,7 @@ void FsView::OnPasteCallback() {
pbox->SetTitle(p.name);
pbox->NewTransfer("Copying "_i18n + src_path);
R_TRY(pbox->CopyFile(src_fs, m_fs.get(), src_path, dst_path));
R_TRY(pbox->CopyFile(src_fs, m_fs.get(), src_path, dst_path, is_same_fs));
R_TRY(on_paste_file(src_path, dst_path));
}
}
@@ -1303,7 +1317,7 @@ void FsView::OnPasteCallback() {
// the folders cannot be deleted until the end as they have to be removed in
// reverse order so that the folder can be deleted (it must be empty).
if (selected.m_type == SelectedType::Cut) {
R_TRY(DeleteAllCollections(pbox, src_fs, selected, collections, FsDirOpenMode_ReadDirs));
R_TRY(DeleteAllCollectionsWithSelected(pbox, src_fs, selected, collections, FsDirOpenMode_ReadDirs));
}
}
@@ -1322,8 +1336,8 @@ void FsView::OnRenameCallback() {
}
auto FsView::CheckIfUpdateFolder() -> Result {
R_UNLESS(IsSd(), FsError_InvalidMountName);
R_UNLESS(m_fs->IsNative(), 0x1);
R_UNLESS(IsSd(), Result_FileBrowserDirNotDaybreak);
R_UNLESS(m_fs->IsNative(), Result_FileBrowserDirNotDaybreak);
// check if we have already tried to find daybreak
if (m_daybreak_path.has_value() && m_daybreak_path.value().empty()) {
@@ -1347,16 +1361,16 @@ auto FsView::CheckIfUpdateFolder() -> Result {
}
// check that we have enough ncas and not too many
R_UNLESS(m_entries.size() > 150 && m_entries.size() < 300, 0x1);
R_UNLESS(m_entries.size() > 150 && m_entries.size() < 300, Result_FileBrowserDirNotDaybreak);
// check that all entries end in .nca
const auto nca_ext = std::string_view{".nca"};
for (auto& e : m_entries) {
// check that we are at the bottom level
R_UNLESS(e.type == FsDirEntryType_File, 0x1);
R_UNLESS(e.type == FsDirEntryType_File, Result_FileBrowserDirNotDaybreak);
const auto ext = std::strrchr(e.name, '.');
R_UNLESS(ext && ext == nca_ext, 0x1);
R_UNLESS(ext && ext == nca_ext, Result_FileBrowserDirNotDaybreak);
}
R_SUCCEED();
@@ -1414,7 +1428,7 @@ auto FsView::get_collections(const fs::FsPath& path, const fs::FsPath& parent_na
return get_collections(m_fs.get(), path, parent_name, out, inc_size);
}
static Result DeleteAllCollections(ProgressBox* pbox, fs::Fs* fs, const SelectedStash& selected, const FsDirCollections& collections, u32 mode = FsDirOpenMode_ReadDirs|FsDirOpenMode_ReadFiles) {
Result FsView::DeleteAllCollections(ProgressBox* pbox, fs::Fs* fs, const FsDirCollections& collections, u32 mode) {
// delete everything in collections, reversed
for (const auto& c : std::views::reverse(collections)) {
const auto delete_func = [&](auto& array) -> Result {
@@ -1443,6 +1457,12 @@ static Result DeleteAllCollections(ProgressBox* pbox, fs::Fs* fs, const Selected
R_TRY(delete_func(c.dirs));
}
R_SUCCEED();
}
static Result DeleteAllCollectionsWithSelected(ProgressBox* pbox, fs::Fs* fs, const SelectedStash& selected, const FsDirCollections& collections, u32 mode = FsDirOpenMode_ReadDirs|FsDirOpenMode_ReadFiles) {
R_TRY(FsView::DeleteAllCollections(pbox, fs, collections, mode));
for (const auto& p : selected.m_files) {
pbox->Yield();
R_TRY(pbox->ShouldExitResult());
@@ -1756,7 +1776,7 @@ void FsView::DisplayAdvancedOptions() {
options->Add(std::make_shared<SidebarEntryArray>("Mount"_i18n, mount_items, [this, fs_entries](s64& index_out){
App::PopToMenu();
SetFs(fs_entries[index_out].root, fs_entries[index_out]);
}, m_fs_entry.name));
}, i18n::get(m_fs_entry.name)));
options->Add(std::make_shared<SidebarEntryCallback>("Create File"_i18n, [this](){
std::string out;
@@ -1851,30 +1871,38 @@ Menu::Menu(u32 flags) : MenuBase{"FileBrowser"_i18n, flags} {
}
view = view_left = std::make_shared<FsView>(this, ViewSide::Left);
ueventCreate(&g_change_uevent, true);
}
Menu::~Menu() {
}
void Menu::Update(Controller* controller, TouchInfo* touch) {
if (R_SUCCEEDED(waitSingle(waiterForUEvent(&g_change_uevent), 0))) {
if (IsSplitScreen()) {
view_left->SortAndFindLastFile(true);
view_right->SortAndFindLastFile(true);
} else {
view->SortAndFindLastFile(true);
}
}
// workaround the buttons not being display properly.
// basically, inherit all actions from the view, draw them,
// then restore state after.
const auto actions_copy = GetActions();
ON_SCOPE_EXIT(m_actions = actions_copy);
m_actions.insert_range(view->GetActions());
const auto view_actions = view->GetActions();
m_actions.insert_range(view_actions);
ON_SCOPE_EXIT(RemoveActions(view_actions));
MenuBase::Update(controller, touch);
view->Update(controller, touch);
}
void Menu::Draw(NVGcontext* vg, Theme* theme) {
// workaround the buttons not being display properly.
// basically, inherit all actions from the view, draw them,
// then restore state after.
const auto actions_copy = GetActions();
ON_SCOPE_EXIT(m_actions = actions_copy);
m_actions.insert_range(view->GetActions());
// see Menu::Update().
const auto view_actions = view->GetActions();
m_actions.insert_range(view_actions);
ON_SCOPE_EXIT(RemoveActions(view_actions));
MenuBase::Draw(vg, theme);

View File

@@ -1,166 +1,25 @@
#include "ui/menus/ftp_menu.hpp"
#include "yati/yati.hpp"
#include "app.hpp"
#include "defines.hpp"
#include "log.hpp"
#include "ui/nvg_util.hpp"
#include "i18n.hpp"
#include "ftpsrv_helper.hpp"
#include <cstring>
#include <algorithm>
namespace sphaira::ui::menu::ftp {
namespace {
constexpr u64 MAX_BUFFER_SIZE = 1024*1024*32;
constexpr u64 SLEEPNS = 1000;
volatile bool IN_PUSH_THREAD{};
bool OnInstallStart(void* user, const char* path) {
auto menu = (Menu*)user;
log_write("[INSTALL] inside OnInstallStart()\n");
for (;;) {
mutexLock(&menu->m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&menu->m_mutex));
if (menu->m_state != State::Progress) {
break;
}
if (menu->GetToken().stop_requested()) {
return false;
}
svcSleepThread(1e+6);
}
log_write("[INSTALL] OnInstallStart() got state: %u\n", (u8)menu->m_state);
if (menu->m_source) {
log_write("[INSTALL] OnInstallStart() we have source\n");
for (;;) {
mutexLock(&menu->m_source->m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&menu->m_source->m_mutex));
if (!IN_PUSH_THREAD) {
break;
}
if (menu->GetToken().stop_requested()) {
return false;
}
svcSleepThread(1e+6);
}
log_write("[INSTALL] OnInstallStart() stopped polling source\n");
}
log_write("[INSTALL] OnInstallStart() doing make_shared\n");
menu->m_source = std::make_shared<StreamFtp>(path, menu->GetToken());
mutexLock(&menu->m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&menu->m_mutex));
menu->m_state = State::Connected;
log_write("[INSTALL] OnInstallStart() done make shared\n");
return true;
}
bool OnInstallWrite(void* user, const void* buf, size_t size) {
auto menu = (Menu*)user;
return menu->m_source->Push(buf, size);
}
void OnInstallClose(void* user) {
auto menu = (Menu*)user;
menu->m_source->Disable();
}
} // namespace
StreamFtp::StreamFtp(const fs::FsPath& path, std::stop_token token) {
m_path = path;
m_token = token;
m_buffer.reserve(MAX_BUFFER_SIZE);
m_active = true;
}
Result StreamFtp::ReadChunk(void* buf, s64 size, u64* bytes_read) {
while (!m_token.stop_requested()) {
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
if (m_buffer.empty()) {
if (!m_active) {
break;
}
svcSleepThread(SLEEPNS);
} else {
size = std::min<s64>(size, m_buffer.size());
std::memcpy(buf, m_buffer.data(), size);
m_buffer.erase(m_buffer.begin(), m_buffer.begin() + size);
*bytes_read = size;
R_SUCCEED();
}
}
return 0x1;
}
bool StreamFtp::Push(const void* buf, s64 size) {
IN_PUSH_THREAD = true;
ON_SCOPE_EXIT(IN_PUSH_THREAD = false);
while (!m_token.stop_requested()) {
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
if (!m_active) {
break;
}
if (m_buffer.size() + size >= MAX_BUFFER_SIZE) {
svcSleepThread(SLEEPNS);
} else {
const auto offset = m_buffer.size();
m_buffer.resize(offset + size);
std::memcpy(m_buffer.data() + offset, buf, size);
return true;
}
}
return false;
}
void StreamFtp::Disable() {
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
m_active = false;
}
Menu::Menu(u32 flags) : MenuBase{"FTP Install (EXPERIMENTAL)"_i18n, flags} {
SetAction(Button::B, Action{"Back"_i18n, [this](){
SetPop();
}});
SetAction(Button::X, Action{"Options"_i18n, [this](){
App::DisplayInstallOptions(false);
}});
App::SetAutoSleepDisabled(true);
mutexInit(&m_mutex);
Menu::Menu(u32 flags) : stream::Menu{"FTP Install"_i18n, flags} {
m_was_ftp_enabled = App::GetFtpEnable();
if (!m_was_ftp_enabled) {
log_write("[FTP] wasn't enabled, forcefully enabling\n");
App::SetFtpEnable(true);
}
ftpsrv::InitInstallMode(this, OnInstallStart, OnInstallWrite, OnInstallClose);
ftpsrv::InitInstallMode(
[this](const char* path){ return OnInstallStart(path); },
[this](const void *buf, size_t size){ return OnInstallWrite(buf, size); },
[this](){ return OnInstallClose(); }
);
m_port = ftpsrv::GetPort();
m_anon = ftpsrv::IsAnon();
@@ -173,71 +32,19 @@ Menu::Menu(u32 flags) : MenuBase{"FTP Install (EXPERIMENTAL)"_i18n, flags} {
Menu::~Menu() {
// signal for thread to exit and wait.
ftpsrv::DisableInstallMode();
m_stop_source.request_stop();
if (m_source) {
m_source->Disable();
}
if (!m_was_ftp_enabled) {
log_write("[FTP] disabling on exit\n");
App::SetFtpEnable(false);
}
App::SetAutoSleepDisabled(false);
log_write("closing data!!!!\n");
}
void Menu::Update(Controller* controller, TouchInfo* touch) {
MenuBase::Update(controller, touch);
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
switch (m_state) {
case State::None:
break;
case State::Connected:
log_write("set to progress\n");
m_state = State::Progress;
log_write("got connection\n");
App::Push(std::make_shared<ui::ProgressBox>(0, "Installing "_i18n, "", [this](auto pbox) -> Result {
log_write("inside progress box\n");
const auto rc = yati::InstallFromSource(pbox, m_source, m_source->m_path);
if (R_FAILED(rc)) {
m_source->Disable();
R_THROW(rc);
}
R_SUCCEED();
}, [this](Result rc){
App::PushErrorBox(rc, "Ftp install failed!"_i18n);
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
if (R_SUCCEEDED(rc)) {
App::Notify("Ftp install success!"_i18n);
m_state = State::Done;
} else {
m_state = State::Failed;
}
}));
break;
case State::Progress:
case State::Done:
case State::Failed:
break;
}
stream::Menu::Update(controller, touch);
}
void Menu::Draw(NVGcontext* vg, Theme* theme) {
MenuBase::Draw(vg, theme);
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
stream::Menu::Draw(vg, theme);
const auto pdata = GetPolledData();
if (pdata.ip) {
@@ -257,6 +64,7 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
float bounds[4];
nvgFontSize(vg, font_size);
nvgTextAlign(vg, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
// note: textbounds strips spaces...todo: use nvgTextGlyphPositions() instead.
#define draw(key, ...) \
@@ -286,31 +94,10 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
}
#undef draw
switch (m_state) {
case State::None:
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Waiting for connection..."_i18n.c_str());
break;
case State::Connected:
break;
case State::Progress:
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Transferring data..."_i18n.c_str());
break;
case State::Done:
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Press B to exit..."_i18n.c_str());
break;
case State::Failed:
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Failed to install via FTP, press B to exit..."_i18n.c_str());
break;
}
}
void Menu::OnFocusGained() {
MenuBase::OnFocusGained();
void Menu::OnDisableInstallMode() {
ftpsrv::DisableInstallMode();
}
} // namespace sphaira::ui::menu::ftp

View File

@@ -5,6 +5,7 @@
#include "defines.hpp"
#include "i18n.hpp"
#include "image.hpp"
#include "swkbd.hpp"
#include "ui/menus/game_menu.hpp"
#include "ui/sidebar.hpp"
@@ -138,11 +139,12 @@ using MetaEntries = std::vector<NsApplicationContentMetaStatus>;
struct ContentInfoEntry {
NsApplicationContentMetaStatus status{};
std::vector<NcmContentInfo> content_infos{};
std::vector<FsRightsId> rights_ids{};
std::vector<NcmRightsId> ncm_rights_id{};
};
struct TikEntry {
FsRightsId id{};
u8 key_gen{};
std::vector<u8> tik_data{};
std::vector<u8> cert_data{};
};
@@ -192,7 +194,7 @@ struct NspEntry {
const auto it = std::ranges::find_if(tickets, [&id](auto& e){
return !std::memcmp(&id, &e.id, sizeof(id));
});
R_UNLESS(it != tickets.end(), 0x1);
R_UNLESS(it != tickets.end(), Result_GameBadReadForDump);
const auto& data = collection.name.ends_with(".tik") ? it->tik_data : it->cert_data;
std::memcpy(buf, data.data() + off, size);
@@ -224,7 +226,7 @@ struct NspSource final : dump::BaseSource {
const auto it = std::ranges::find_if(m_entries, [&path](auto& e){
return path.find(e.path.s) != path.npos;
});
R_UNLESS(it != m_entries.end(), 0x1);
R_UNLESS(it != m_entries.end(), Result_GameBadReadForDump);
const auto rc = it->Read(buf, off, size, bytes_read);
if (m_is_file_based_emummc) {
@@ -359,7 +361,7 @@ Result LoadControlManual(u64 id, ThreadResultData& data) {
MetaEntries entries;
R_TRY(GetMetaEntries(id, entries));
R_UNLESS(!entries.empty(), 0x1);
R_UNLESS(!entries.empty(), Result_GameEmptyMetaEntries);
u64 program_id;
fs::FsPath path;
@@ -499,7 +501,6 @@ auto BuildNspPath(const Entry& e, const NsApplicationContentMetaStatus& status)
utilsReplaceIllegalCharacters(name_buf, true);
char version[sizeof(NacpStruct::display_version) + 1]{};
// status.storageID
if (status.meta_type == NcmContentMetaType_Patch) {
u64 program_id;
fs::FsPath path;
@@ -540,8 +541,8 @@ Result BuildContentEntry(const NsApplicationContentMetaStatus& status, ContentIn
NcmContentMetaKey key;
R_TRY(ncmContentMetaDatabaseList(std::addressof(db), std::addressof(meta_total), std::addressof(meta_entries_written), std::addressof(key), 1, (NcmContentMetaType)status.meta_type, app_id, id_min, id_max, NcmContentInstallType_Full));
log_write("ncmContentMetaDatabaseList(): AppId: %016lX Id: %016lX total: %d written: %d storageID: %u key.id %016lX\n", app_id, status.application_id, meta_total, meta_entries_written, status.storageID, key.id);
R_UNLESS(meta_total == 1, 0x1);
R_UNLESS(meta_entries_written == 1, 0x1);
R_UNLESS(meta_total == 1, Result_GameMultipleKeysFound);
R_UNLESS(meta_entries_written == 1, Result_GameMultipleKeysFound);
std::vector<NcmContentInfo> cnmt_infos;
for (s32 i = 0; ; i++) {
@@ -557,14 +558,13 @@ Result BuildContentEntry(const NsApplicationContentMetaStatus& status, ContentIn
NcmRightsId ncm_rights_id;
R_TRY(ncmContentStorageGetRightsIdFromContentId(std::addressof(cs), std::addressof(ncm_rights_id), std::addressof(info_out.content_id), FsContentAttributes_All));
const auto rights_id = ncm_rights_id.rights_id;
if (isRightsIdValid(rights_id)) {
const auto it = std::ranges::find_if(out.rights_ids, [&rights_id](auto& e){
return !std::memcmp(&e, &rights_id, sizeof(rights_id));
if (isRightsIdValid(ncm_rights_id.rights_id)) {
const auto it = std::ranges::find_if(out.ncm_rights_id, [&ncm_rights_id](auto& e){
return !std::memcmp(&e, &ncm_rights_id, sizeof(ncm_rights_id));
});
if (it == out.rights_ids.end()) {
out.rights_ids.emplace_back(rights_id);
if (it == out.ncm_rights_id.end()) {
out.ncm_rights_id.emplace_back(ncm_rights_id);
}
}
@@ -581,13 +581,27 @@ Result BuildContentEntry(const NsApplicationContentMetaStatus& status, ContentIn
R_SUCCEED();
}
Result BuildNspEntry(const Entry& e, const ContentInfoEntry& info, NspEntry& out) {
Result BuildNspEntry(const Entry& e, const ContentInfoEntry& info, const keys::Keys& keys, NspEntry& out) {
out.application_name = e.GetName();
out.path = BuildNspPath(e, info.status);
s64 offset{};
for (auto& rights_id : info.rights_ids) {
TikEntry entry{rights_id};
for (auto& e : info.content_infos) {
char nca_name[0x200];
std::snprintf(nca_name, sizeof(nca_name), "%s%s", hexIdToStr(e.content_id).str, e.content_type == NcmContentType_Meta ? ".cnmt.nca" : ".nca");
u64 size;
ncmContentInfoSizeToU64(std::addressof(e), std::addressof(size));
out.collections.emplace_back(nca_name, offset, size);
offset += size;
}
for (auto& ncm_rights_id : info.ncm_rights_id) {
const auto rights_id = ncm_rights_id.rights_id;
const auto key_gen = ncm_rights_id.key_generation;
TikEntry entry{rights_id, key_gen};
log_write("rights id is valid, fetching common ticket and cert\n");
u64 tik_size;
@@ -600,6 +614,9 @@ Result BuildNspEntry(const Entry& e, const ContentInfoEntry& info, NspEntry& out
R_TRY(es::GetCommonTicketAndCertificateData(&tik_size, &cert_size, entry.tik_data.data(), entry.tik_data.size(), entry.cert_data.data(), entry.cert_data.size(), &rights_id));
log_write("got tik_data: %zu cert_data: %zu\n", tik_size, cert_size);
// patch fake ticket / convert personalised to common if needed.
R_TRY(es::PatchTicket(entry.tik_data, entry.cert_data, key_gen, keys, App::GetApp()->m_dump_convert_to_common_ticket.Get()));
char tik_name[0x200];
std::snprintf(tik_name, sizeof(tik_name), "%s%s", hexIdToStr(rights_id).str, ".tik");
@@ -615,17 +632,6 @@ Result BuildNspEntry(const Entry& e, const ContentInfoEntry& info, NspEntry& out
out.tickets.emplace_back(entry);
}
for (auto& e : info.content_infos) {
char nca_name[0x200];
std::snprintf(nca_name, sizeof(nca_name), "%s%s", hexIdToStr(e.content_id).str, e.content_type == NcmContentType_Meta ? ".cnmt.nca" : ".nca");
u64 size;
ncmContentInfoSizeToU64(std::addressof(e), std::addressof(size));
out.collections.emplace_back(nca_name, offset, size);
offset += size;
}
out.nsp_data = yati::container::Nsp::Build(out.collections, out.nsp_size);
out.cs = GetNcmCs(info.status.storageID);
@@ -638,16 +644,19 @@ Result BuildNspEntries(Entry& e, u32 flags, std::vector<NspEntry>& out) {
MetaEntries meta_entries;
R_TRY(GetMetaEntries(e, meta_entries, flags));
keys::Keys keys;
R_TRY(keys::parse_keys(keys, true));
for (const auto& status : meta_entries) {
ContentInfoEntry info;
R_TRY(BuildContentEntry(status, info));
NspEntry nsp;
R_TRY(BuildNspEntry(e, info, nsp));
R_TRY(BuildNspEntry(e, info, keys, nsp));
out.emplace_back(nsp).icon = e.image;
}
R_UNLESS(!out.empty(), 0x1);
R_UNLESS(!out.empty(), Result_GameNoNspEntriesBuilt);
R_SUCCEED();
}
@@ -945,6 +954,33 @@ Menu::Menu(u32 flags) : grid::Menu{"Games"_i18n, flags} {
options->Add(std::make_shared<SidebarEntryBool>("Title cache"_i18n, m_title_cache.Get(), [this](bool& v_out){
m_title_cache.Set(v_out);
}));
// todo: impl this.
#if 0
options->Add(std::make_shared<SidebarEntryCallback>("Create save"_i18n, [this](){
ui::PopupList::Items items{};
const auto accounts = App::GetAccountList();
for (auto& p : accounts) {
items.emplace_back(p.nickname);
}
fsCreateSaveDataFileSystem;
App::Push(std::make_shared<ui::PopupList>(
"Select user to create save for"_i18n, items, [accounts](auto op_index){
if (op_index) {
s64 out;
if (R_SUCCEEDED(swkbd::ShowNumPad(out, "Enter the save size"_i18n.c_str()))) {
}
}
}
));
// 1. Select user to create save for.
// 2. Enter the save size.
// 3. Enter the journal size (0 for default).
}));
#endif
}})
);
@@ -1073,8 +1109,8 @@ void Menu::ScanHomebrew() {
const auto hide_forwarders = m_hide_forwarders.Get();
TimeStamp ts;
appletSetCpuBoostMode(ApmCpuBoostMode_FastLoad);
ON_SCOPE_EXIT(appletSetCpuBoostMode(ApmCpuBoostMode_Normal));
App::SetBoostMode(true);
ON_SCOPE_EXIT(App::SetBoostMode(false));
FreeEntries();
m_entries.reserve(ENTRY_CHUNK_COUNT);

View File

@@ -221,7 +221,7 @@ struct XciSource final : dump::BaseSource {
span = initial;
}
R_UNLESS(!span.empty(), 0x1);
R_UNLESS(!span.empty(), Result_GcBadReadForDump);
size = ClipSize(off, size, span.size());
*bytes_read = size;
@@ -376,7 +376,7 @@ Result GcSource::Read(void* buf, s64 off, s64 size, u64* bytes_read) {
}
// this will never fail, unless i break something in yati.
R_UNLESS(found, 0x1);
R_UNLESS(found, Result_GcBadReadForDump);
}
return m_file.Read(off - m_offset, buf, size, 0, bytes_read);
@@ -405,6 +405,55 @@ auto ApplicationEntry::GetSize() const -> s64 {
Menu::Menu(u32 flags) : MenuBase{"GameCard"_i18n, flags} {
this->SetActions(
std::make_pair(Button::A, Action{"OK"_i18n, [this](){
if (m_option_index == 2) {
SetPop();
} else {
if (!m_mounted) {
return;
}
if (m_option_index == 0) {
if (!App::GetInstallEnable()) {
App::Push(std::make_shared<ui::OptionBox>(
"Install disabled...\n"
"Please enable installing via the install options."_i18n,
"OK"_i18n
));
} else {
log_write("[GC] doing install A\n");
App::Push(std::make_shared<ui::ProgressBox>(m_icon, "Installing "_i18n, m_entries[m_entry_index].lang_entry.name, [this](auto pbox) -> Result {
auto source = std::make_shared<GcSource>(m_entries[m_entry_index], m_fs.get());
return yati::InstallFromCollections(pbox, source, source->m_collections, source->m_config);
}, [this](Result rc){
App::PushErrorBox(rc, "Gc install failed!"_i18n);
if (R_SUCCEEDED(rc)) {
App::Notify("Gc install success!"_i18n);
}
}));
}
} else {
auto options = std::make_shared<Sidebar>("Select content to dump"_i18n, Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(options));
const auto add = [&](const std::string& name, u32 flags){
options->Add(std::make_shared<SidebarEntryCallback>(name, [this, flags](){
DumpGames(flags);
m_dirty = true;
}, true));
};
add("Dump All"_i18n, DumpFileFlag_All);
add("Dump All Bins"_i18n, DumpFileFlag_AllBin);
add("Dump XCI"_i18n, DumpFileFlag_XCI);
add("Dump Card ID Set"_i18n, DumpFileFlag_Set);
add("Dump Card UID"_i18n, DumpFileFlag_UID);
add("Dump Certificate"_i18n, DumpFileFlag_Cert);
add("Dump Initial Data"_i18n, DumpFileFlag_Initial);
}
}
}}),
std::make_pair(Button::B, Action{"Back"_i18n, [this](){
SetPop();
}}),
@@ -593,7 +642,7 @@ Result Menu::GcMount() {
return !std::strncmp(str.str, e.name, std::strlen(str.str));
});
R_UNLESS(it != buf.cend(), yati::Result_NcaNotFound);
R_UNLESS(it != buf.cend(), Result_YatiNcaNotFound);
collections.emplace_back(it->name, it->file_size, info.content_type, info.id_offset);
}
@@ -624,7 +673,7 @@ Result Menu::GcMount() {
}
}
R_UNLESS(m_entries.size(), 0x1);
R_UNLESS(m_entries.size(), Result_GcEmptyGamecard);
// append tickets to every application, yati will ignore if undeeded.
for (auto& e : m_entries) {
@@ -643,57 +692,6 @@ Result Menu::GcMount() {
}
}
SetAction(Button::A, Action{"OK"_i18n, [this](){
if (m_option_index == 2) {
SetPop();
} else {
log_write("[GC] pressed A\n");
if (!m_mounted) {
return;
}
if (m_option_index == 0) {
if (!App::GetInstallEnable()) {
App::Push(std::make_shared<ui::OptionBox>(
"Install disabled...\n"
"Please enable installing via the install options."_i18n,
"OK"_i18n
));
} else {
log_write("[GC] doing install A\n");
App::Push(std::make_shared<ui::ProgressBox>(m_icon, "Installing "_i18n, m_entries[m_entry_index].lang_entry.name, [this](auto pbox) -> Result {
auto source = std::make_shared<GcSource>(m_entries[m_entry_index], m_fs.get());
return yati::InstallFromCollections(pbox, source, source->m_collections, source->m_config);
}, [this](Result rc){
App::PushErrorBox(rc, "Gc install failed!"_i18n);
if (R_SUCCEEDED(rc)) {
App::Notify("Gc install success!"_i18n);
}
}));
}
} else {
auto options = std::make_shared<Sidebar>("Select content to dump"_i18n, Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(options));
const auto add = [&](const std::string& name, u32 flags){
options->Add(std::make_shared<SidebarEntryCallback>(name, [this, flags](){
DumpGames(flags);
m_dirty = true;
}, true));
};
add("Dump All"_i18n, DumpFileFlag_All);
add("Dump All Bins"_i18n, DumpFileFlag_AllBin);
add("Dump XCI"_i18n, DumpFileFlag_XCI);
add("Dump Card ID Set"_i18n, DumpFileFlag_Set);
add("Dump Card UID"_i18n, DumpFileFlag_UID);
add("Dump Certificate"_i18n, DumpFileFlag_Cert);
add("Dump Initial Data"_i18n, DumpFileFlag_Initial);
}
}
}});
if (m_entries.size() > 1) {
SetAction(Button::L2, Action{"Prev"_i18n, [this](){
if (m_entry_index != 0) {
@@ -742,12 +740,12 @@ Result Menu::GcMountStorage() {
std::memcpy(&trim_size, header + 0x118, sizeof(trim_size));
std::memcpy(&m_package_id, header + 0x110, sizeof(m_package_id));
std::memcpy(m_initial_data_hash, header + 0x160, sizeof(m_initial_data_hash));
R_UNLESS(magic == XCI_MAGIC, 0x1);
R_UNLESS(magic == XCI_MAGIC, Result_GcBadXciMagic);
// calculate the reported size, error if not found.
m_storage_full_size = GetXciSizeFromRomSize(rom_size);
log_write("[GC] m_storage_full_size: %zd rom_size: 0x%X\n", m_storage_full_size, rom_size);
R_UNLESS(m_storage_full_size > 0, 0x1);
R_UNLESS(m_storage_full_size > 0, Result_GcBadXciRomSize);
R_TRY(fsStorageGetSize(&m_storage, &m_parition_normal_size));
R_TRY(GcMountPartition(FsGameCardPartitionRaw_Secure));
@@ -982,8 +980,8 @@ Result Menu::DumpGames(u32 flags) {
R_TRY(GcMountStorage());
const auto do_dump = [this](u32 flags) -> Result {
appletSetCpuBoostMode(ApmCpuBoostMode_FastLoad);
ON_SCOPE_EXIT(appletSetCpuBoostMode(ApmCpuBoostMode_Normal));
App::SetBoostMode(true);
ON_SCOPE_EXIT(App::SetBoostMode(false));
u32 location_flags = dump::DumpLocationFlag_All;
@@ -1141,7 +1139,7 @@ Result Menu::GcGetSecurityInfo(GameCardSecurityInformation& out) {
}
}
R_THROW(0x1);
R_THROW(Result_GcFailedToGetSecurityInfo);
}
} // namespace sphaira::ui::menu::gc

View File

@@ -1,13 +1,15 @@
#include "ui/menus/ghdl.hpp"
#include "ui/menus/homebrew.hpp"
#include "ui/sidebar.hpp"
#include "ui/option_box.hpp"
#include "ui/popup_list.hpp"
#include "ui/progress_box.hpp"
#include "ui/error_box.hpp"
#include "ui/nvg_util.hpp"
#include "log.hpp"
#include "app.hpp"
#include "ui/nvg_util.hpp"
#include "fs.hpp"
#include "defines.hpp"
#include "image.hpp"
@@ -88,7 +90,7 @@ auto DownloadApp(ProgressBox* pbox, const GhApiAsset& gh_asset, const AssetEntry
R_TRY(fs.GetFsOpenResult());
ON_SCOPE_EXIT(fs.DeleteFile(temp_file));
R_UNLESS(!gh_asset.browser_download_url.empty(), 0x1);
R_UNLESS(!gh_asset.browser_download_url.empty(), Result_GhdlEmptyAsset);
// 2. download the asset
if (!pbox->ShouldExit()) {
@@ -101,7 +103,7 @@ auto DownloadApp(ProgressBox* pbox, const GhApiAsset& gh_asset, const AssetEntry
curl::OnProgress{pbox->OnDownloadProgressCallback()}
);
R_UNLESS(result.success, 0x1);
R_UNLESS(result.success, Result_GhdlFailedToDownloadAsset);
}
fs::FsPath root_path{"/"};
@@ -141,11 +143,11 @@ auto DownloadAssetJson(ProgressBox* pbox, const std::string& url, GhApiEntry& ou
}
);
R_UNLESS(result.success, 0x1);
R_UNLESS(result.success, Result_GhdlFailedToDownloadAssetJson);
from_json(result.path, out);
}
R_UNLESS(!out.assets.empty(), 0x1);
R_UNLESS(!out.assets.empty(), Result_GhdlEmptyAsset);
R_SUCCEED();
}
@@ -217,6 +219,7 @@ Menu::Menu(u32 flags) : MenuBase{"GitHub"_i18n, flags} {
App::Push(std::make_shared<ProgressBox>(0, "Downloading "_i18n, GetEntry().repo, [this, &asset_entry, ptr](auto pbox) -> Result {
return DownloadApp(pbox, asset_entry, ptr);
}, [this, ptr](Result rc){
homebrew::SignalChange();
App::PushErrorBox(rc, "Failed to download app!"_i18n);
if (R_SUCCEEDED(rc)) {

View File

@@ -20,6 +20,7 @@ namespace sphaira::ui::menu::homebrew {
namespace {
Menu* g_menu{};
constinit UEvent g_change_uevent;
auto GenerateStarPath(const fs::FsPath& nro_path) -> fs::FsPath {
fs::FsPath out{};
@@ -35,6 +36,10 @@ void FreeEntry(NVGcontext* vg, NroEntry& e) {
} // namespace
void SignalChange() {
ueventSignal(&g_change_uevent);
}
auto GetNroEntries() -> std::span<const NroEntry> {
if (!g_menu) {
return {};
@@ -139,6 +144,7 @@ Menu::Menu() : grid::Menu{"Homebrew"_i18n, MenuFlag_Tab} {
);
OnLayoutChange();
ueventCreate(&g_change_uevent, true);
}
Menu::~Menu() {
@@ -147,6 +153,14 @@ Menu::~Menu() {
}
void Menu::Update(Controller* controller, TouchInfo* touch) {
if (R_SUCCEEDED(waitSingle(waiterForUEvent(&g_change_uevent), 0))) {
m_dirty = true;
}
if (m_dirty) {
SortAndFindLastFile(true);
}
MenuBase::Update(controller, touch);
m_list->OnUpdate(controller, touch, m_index, m_entries.size(), [this](bool touch, auto i) {
if (touch && m_index == i) {
@@ -178,7 +192,7 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
const auto icon = nro_get_icon(e.path, e.icon_size, e.icon_offset);
TimeStamp ts;
if (!icon.empty()) {
const auto image = ImageLoadFromMemory(icon, ImageFlag_None);
const auto image = ImageLoadFromMemory(icon, ImageFlag_JPEG);
if (!image.data.empty()) {
e.image = nvgCreateImageRGBA(vg, image.w, image.h, 0, image.data.data());
log_write("\t[image load] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs());
@@ -297,6 +311,7 @@ void Menu::ScanHomebrew() {
this->Sort();
SetIndex(0);
m_dirty = false;
}
void Menu::Sort() {
@@ -391,9 +406,13 @@ void Menu::Sort() {
std::sort(m_entries.begin(), m_entries.end(), sorter);
}
void Menu::SortAndFindLastFile() {
void Menu::SortAndFindLastFile(bool scan) {
const auto path = m_entries[m_index].path;
Sort();
if (scan) {
ScanHomebrew();
} else {
Sort();
}
SetIndex(0);
s64 index = -1;

View File

@@ -0,0 +1,261 @@
#include "ui/menus/install_stream_menu_base.hpp"
#include "yati/yati.hpp"
#include "app.hpp"
#include "defines.hpp"
#include "log.hpp"
#include "ui/nvg_util.hpp"
#include "i18n.hpp"
#include <cstring>
namespace sphaira::ui::menu::stream {
namespace {
enum class InstallState {
None,
Progress,
Finished,
};
constexpr u64 MAX_BUFFER_SIZE = 1024ULL*1024ULL*8ULL;
constexpr u64 MAX_BUFFER_RESERVE_SIZE = 1024ULL*1024ULL*32ULL;
volatile InstallState INSTALL_STATE{InstallState::None};
} // namespace
Stream::Stream(const fs::FsPath& path, std::stop_token token) {
m_path = path;
m_token = token;
m_active = true;
m_buffer.reserve(MAX_BUFFER_RESERVE_SIZE);
mutexInit(&m_mutex);
condvarInit(&m_can_read);
}
Result Stream::ReadChunk(void* buf, s64 size, u64* bytes_read) {
log_write("[Stream::ReadChunk] inside\n");
ON_SCOPE_EXIT(
log_write("[Stream::ReadChunk] exiting\n");
);
while (!m_token.stop_requested()) {
SCOPED_MUTEX(&m_mutex);
if (m_active && m_buffer.empty()) {
condvarWait(&m_can_read, &m_mutex);
}
if ((!m_active && m_buffer.empty()) || m_token.stop_requested()) {
break;
}
size = std::min<s64>(size, m_buffer.size());
std::memcpy(buf, m_buffer.data(), size);
m_buffer.erase(m_buffer.begin(), m_buffer.begin() + size);
*bytes_read = size;
R_SUCCEED();
}
log_write("[Stream::ReadChunk] failed to read\n");
R_THROW(Result_TransferCancelled);
}
bool Stream::Push(const void* buf, s64 size) {
log_write("[Stream::Push] inside\n");
ON_SCOPE_EXIT(
log_write("[Stream::Push] exiting\n");
);
while (!m_token.stop_requested()) {
if (INSTALL_STATE == InstallState::Finished) {
log_write("[Stream::Push] install has finished\n");
return true;
}
// don't use condivar here as windows mtp is very broken.
// stalling for too longer (3s+) and having too varied transfer speeds
// results in windows stalling the transfer for 1m until it kills it via timeout.
// the workaround is to always accept new data, but stall for 1s.
SCOPED_MUTEX(&m_mutex);
if (m_active && m_buffer.size() >= MAX_BUFFER_SIZE) {
// unlock the mutex and wait for 1s to bring transfer speed down to 1MiB/s.
log_write("[Stream::Push] buffer is full, delaying\n");
mutexUnlock(&m_mutex);
ON_SCOPE_EXIT(mutexLock(&m_mutex));
svcSleepThread(1e+9);
}
if (!m_active) {
log_write("[Stream::Push] file not active\n");
break;
}
const auto offset = m_buffer.size();
m_buffer.resize(offset + size);
std::memcpy(m_buffer.data() + offset, buf, size);
condvarWakeOne(&m_can_read);
return true;
}
log_write("[Stream::Push] failed to push\n");
return false;
}
void Stream::Disable() {
log_write("[Stream::Disable] disabling file\n");
SCOPED_MUTEX(&m_mutex);
m_active = false;
condvarWakeOne(&m_can_read);
}
Menu::Menu(const std::string& title, u32 flags) : MenuBase{title, flags} {
SetAction(Button::B, Action{"Back"_i18n, [this](){
SetPop();
}});
SetAction(Button::X, Action{"Options"_i18n, [this](){
App::DisplayInstallOptions(false);
}});
App::SetAutoSleepDisabled(true);
mutexInit(&m_mutex);
INSTALL_STATE = InstallState::None;
}
Menu::~Menu() {
// signal for thread to exit and wait.
m_stop_source.request_stop();
if (m_source) {
m_source->Disable();
}
App::SetAutoSleepDisabled(false);
}
void Menu::Update(Controller* controller, TouchInfo* touch) {
MenuBase::Update(controller, touch);
SCOPED_MUTEX(&m_mutex);
if (m_state == State::Connected) {
m_state = State::Progress;
App::Push(std::make_shared<ui::ProgressBox>(0, "Installing "_i18n, m_source->GetPath(), [this](auto pbox) -> Result {
INSTALL_STATE = InstallState::Progress;
const auto rc = yati::InstallFromSource(pbox, m_source, m_source->GetPath());
INSTALL_STATE = InstallState::Finished;
if (R_FAILED(rc)) {
m_source->Disable();
R_THROW(rc);
}
R_SUCCEED();
}, [this](Result rc){
App::PushErrorBox(rc, "Install failed!"_i18n);
SCOPED_MUTEX(&m_mutex);
if (R_SUCCEEDED(rc)) {
App::Notify("Install success!"_i18n);
m_state = State::Done;
} else {
m_state = State::Failed;
OnDisableInstallMode();
}
}));
}
}
void Menu::Draw(NVGcontext* vg, Theme* theme) {
MenuBase::Draw(vg, theme);
SCOPED_MUTEX(&m_mutex);
switch (m_state) {
case State::None:
case State::Done:
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Drag'n'Drop (NSP, XCI, NSZ, XCZ) to the install folder"_i18n.c_str());
break;
case State::Connected:
case State::Progress:
break;
case State::Failed:
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Failed to install, press B to exit..."_i18n.c_str());
break;
}
}
bool Menu::OnInstallStart(const char* path) {
log_write("[Menu::OnInstallStart] inside\n");
for (;;) {
{
SCOPED_MUTEX(&m_mutex);
if (m_state != State::Progress) {
break;
}
if (GetToken().stop_requested()) {
return false;
}
}
svcSleepThread(1e+6);
}
log_write("[Menu::OnInstallStart] got state: %u\n", (u8)m_state);
if (m_source) {
log_write("[Menu::OnInstallStart] we have source\n");
for (;;) {
{
SCOPED_MUTEX(&m_source->m_mutex);
if (!m_source->m_active && INSTALL_STATE != InstallState::Progress) {
break;
}
if (GetToken().stop_requested()) {
return false;
}
}
svcSleepThread(1e+6);
}
log_write("[Menu::OnInstallStart] stopped polling source\n");
}
SCOPED_MUTEX(&m_mutex);
m_source = std::make_shared<Stream>(path, GetToken());
INSTALL_STATE = InstallState::None;
m_state = State::Connected;
log_write("[Menu::OnInstallStart] exiting\n");
return true;
}
bool Menu::OnInstallWrite(const void* buf, size_t size) {
log_write("[Menu::OnInstallWrite] inside\n");
return m_source->Push(buf, size);
}
void Menu::OnInstallClose() {
log_write("[Menu::OnInstallClose] inside\n");
m_source->Disable();
// wait until the install has finished before returning.
while (INSTALL_STATE == InstallState::Progress) {
svcSleepThread(1e+7);
}
}
} // namespace sphaira::ui::menu::stream

View File

@@ -13,6 +13,7 @@
#include "ui/menus/ghdl.hpp"
#include "ui/menus/usb_menu.hpp"
#include "ui/menus/ftp_menu.hpp"
#include "ui/menus/mtp_menu.hpp"
#include "ui/menus/gc_menu.hpp"
#include "ui/menus/game_menu.hpp"
#include "ui/menus/save_menu.hpp"
@@ -54,6 +55,7 @@ const MiscMenuEntry MISC_MENU_ENTRIES[] = {
{ .name = "Themezer", .title = "Themezer", .func = MiscMenuFuncGenerator<ui::menu::themezer::Menu>, .flag = MiscMenuFlag_Shortcut },
{ .name = "GitHub", .title = "GitHub", .func = MiscMenuFuncGenerator<ui::menu::gh::Menu>, .flag = MiscMenuFlag_Shortcut },
{ .name = "FTP", .title = "FTP Install", .func = MiscMenuFuncGenerator<ui::menu::ftp::Menu>, .flag = MiscMenuFlag_Install },
{ .name = "MTP", .title = "MTP Install", .func = MiscMenuFuncGenerator<ui::menu::mtp::Menu>, .flag = MiscMenuFlag_Install },
{ .name = "USB", .title = "USB Install", .func = MiscMenuFuncGenerator<ui::menu::usb::Menu>, .flag = MiscMenuFlag_Install },
{ .name = "GameCard", .title = "GameCard", .func = MiscMenuFuncGenerator<ui::menu::gc::Menu>, .flag = MiscMenuFlag_Shortcut },
{ .name = "IRS", .title = "IRS (Infrared Joycon Camera)", .func = MiscMenuFuncGenerator<ui::menu::irs::Menu>, .flag = MiscMenuFlag_Shortcut },
@@ -76,7 +78,7 @@ auto InstallUpdate(ProgressBox* pbox, const std::string url, const std::string v
curl::OnProgress{pbox->OnDownloadProgressCallback()}
);
R_UNLESS(result.success, 0x1);
R_UNLESS(result.success, Result_MainFailedToDownloadUpdate);
}
ON_SCOPE_EXIT(fs.DeleteFile(zip_out));

View File

@@ -73,7 +73,7 @@ void MenuBase::Draw(NVGcontext* vg, Theme* theme) {
if (fixed) { \
start_x -= fixed; \
} else { \
gfx::textBounds(vg, 0, 0, bounds, __VA_ARGS__); \
gfx::textBoundsArgs(vg, 0, 0, bounds, __VA_ARGS__); \
start_x -= spacing + (bounds[2] - bounds[0]); \
}

View File

@@ -0,0 +1,89 @@
#include "ui/menus/mtp_menu.hpp"
#include "usb/usbds.hpp"
#include "app.hpp"
#include "defines.hpp"
#include "log.hpp"
#include "ui/nvg_util.hpp"
#include "i18n.hpp"
#include "haze_helper.hpp"
namespace sphaira::ui::menu::mtp {
namespace {
auto GetUsbStateStr(UsbState state) -> const char* {
switch (state) {
case UsbState_Detached: return "Detached";
case UsbState_Attached: return "Attached";
case UsbState_Powered: return "Powered";
case UsbState_Default: return "Default";
case UsbState_Address: return "Address";
case UsbState_Configured: return "Configured";
case UsbState_Suspended: return "Suspended";
}
return "Unknown";
}
auto GetUsbSpeedStr(UsbDeviceSpeed speed) -> const char* {
// todo: remove this cast when libnx pr is merged.
switch ((u32)speed) {
case UsbDeviceSpeed_None: return "None";
case UsbDeviceSpeed_Low: return "USB 1.0 Low Speed";
case UsbDeviceSpeed_Full: return "USB 1.1 Full Speed";
case UsbDeviceSpeed_High: return "USB 2.0 High Speed";
case UsbDeviceSpeed_Super: return "USB 3.0 Super Speed";
}
return "Unknown";
}
} // namespace
Menu::Menu(u32 flags) : stream::Menu{"MTP Install"_i18n, flags} {
m_was_mtp_enabled = App::GetMtpEnable();
if (!m_was_mtp_enabled) {
log_write("[MTP] wasn't enabled, forcefully enabling\n");
App::SetMtpEnable(true);
}
haze::InitInstallMode(
[this](const char* path){ return OnInstallStart(path); },
[this](const void *buf, size_t size){ return OnInstallWrite(buf, size); },
[this](){ return OnInstallClose(); }
);
}
Menu::~Menu() {
// signal for thread to exit and wait.
haze::DisableInstallMode();
if (!m_was_mtp_enabled) {
log_write("[MTP] disabling on exit\n");
App::SetMtpEnable(false);
}
}
void Menu::Update(Controller* controller, TouchInfo* touch) {
stream::Menu::Update(controller, touch);
static TimeStamp poll_ts;
if (poll_ts.GetSeconds() >= 1) {
poll_ts.Update();
UsbState state{UsbState_Detached};
usbDsGetState(&state);
UsbDeviceSpeed speed{(UsbDeviceSpeed)UsbDeviceSpeed_None};
usbDsGetSpeed(&speed);
char buf[128];
std::snprintf(buf, sizeof(buf), "State: %s | Speed: %s", i18n::get(GetUsbStateStr(state)).c_str(), i18n::get(GetUsbSpeedStr(speed)).c_str());
SetSubHeading(buf);
}
}
void Menu::OnDisableInstallMode() {
haze::DisableInstallMode();
}
} // namespace sphaira::ui::menu::mtp

File diff suppressed because it is too large Load Diff

View File

@@ -238,7 +238,7 @@ auto InstallTheme(ProgressBox* pbox, const PackListEntry& entry) -> Result {
if (!result.success || result.data.empty()) {
log_write("error with download: %s\n", url.c_str());
R_THROW(0x1);
R_THROW(Result_ThemezerFailedToDownloadThemeMeta);
}
from_json(result.data, download_pack);
@@ -255,7 +255,7 @@ auto InstallTheme(ProgressBox* pbox, const PackListEntry& entry) -> Result {
curl::OnProgress{pbox->OnDownloadProgressCallback()}
);
R_UNLESS(result.success, 0x1);
R_UNLESS(result.success, Result_ThemezerFailedToDownloadTheme);
}
ON_SCOPE_EXIT(fs.DeleteFile(zip_out));
@@ -581,8 +581,8 @@ void Menu::PackListDownload() {
curl::Flags{curl::Flag_Cache},
curl::StopToken{this->GetToken()},
curl::OnComplete{[this, page_index](auto& result){
appletSetCpuBoostMode(ApmCpuBoostMode_FastLoad);
ON_SCOPE_EXIT(appletSetCpuBoostMode(ApmCpuBoostMode_Normal));
App::SetBoostMode(true);
ON_SCOPE_EXIT(App::SetBoostMode(false));
log_write("got themezer data\n");
if (!result.success) {

View File

@@ -23,7 +23,7 @@ void thread_func(void* user) {
}
const auto rc = app->m_usb_source->IsUsbConnected(CONNECTION_TIMEOUT);
if (rc == ::sphaira::usb::UsbDs::Result_Cancelled) {
if (rc == Result_UsbCancelled) {
break;
}

View File

@@ -230,13 +230,17 @@ void drawTextBox(NVGcontext* vg, float x, float y, float size, float bound, cons
nvgTextBox(vg, x, y, bound, str, end);
}
void textBounds(NVGcontext* vg, float x, float y, float *bounds, const char* str, ...) {
void textBounds(NVGcontext* vg, float x, float y, float *bounds, const char* str) {
nvgTextBounds(vg, x, y, str, nullptr, bounds);
}
void textBoundsArgs(NVGcontext* vg, float x, float y, float *bounds, const char* str, ...) {
char buf[0x100];
va_list v;
va_start(v, str);
std::vsnprintf(buf, sizeof(buf), str, v);
va_end(v);
nvgTextBounds(vg, x, y, buf, nullptr, bounds);
textBounds(vg, x, y, bounds, buf);
}
// NEW-----------

View File

@@ -21,7 +21,7 @@ void threadFunc(void* arg) {
ProgressBox::ProgressBox(int image, const std::string& action, const std::string& title, ProgressBoxCallback callback, ProgressBoxDoneCallback done, int cpuid, int prio, int stack_size) {
if (App::GetApp()->m_progress_boost_mode.Get()) {
appletSetCpuBoostMode(ApmCpuBoostMode_FastLoad);
App::SetBoostMode(true);
}
SetAction(Button::B, Action{"Back"_i18n, [this](){
@@ -67,7 +67,7 @@ ProgressBox::~ProgressBox() {
FreeImage();
m_done(m_thread_data.result);
appletSetCpuBoostMode(ApmCpuBoostMode_Normal);
App::SetBoostMode(false);
}
auto ProgressBox::Update(Controller* controller, TouchInfo* touch) -> void {
@@ -258,12 +258,12 @@ auto ProgressBox::ShouldExit() -> bool {
auto ProgressBox::ShouldExitResult() -> Result {
if (ShouldExit()) {
R_THROW(0xFFFF);
R_THROW(Result_TransferCancelled);
}
R_SUCCEED();
}
auto ProgressBox::CopyFile(fs::Fs* fs_src, fs::Fs* fs_dst, const fs::FsPath& src_path, const fs::FsPath& dst_path) -> Result {
auto ProgressBox::CopyFile(fs::Fs* fs_src, fs::Fs* fs_dst, const fs::FsPath& src_path, const fs::FsPath& dst_path, bool single_threaded) -> Result {
const auto is_file_based_emummc = App::IsFileBaseEmummc();
const auto is_both_native = fs_src->IsNative() && fs_dst->IsNative();
@@ -300,20 +300,20 @@ auto ProgressBox::CopyFile(fs::Fs* fs_src, fs::Fs* fs_dst, const fs::FsPath& src
}
return rc;
}
}, single_threaded ? thread::Mode::SingleThreaded : thread::Mode::MultiThreaded
));
R_SUCCEED();
}
auto ProgressBox::CopyFile(fs::Fs* fs, const fs::FsPath& src_path, const fs::FsPath& dst_path) -> Result {
return CopyFile(fs, fs, src_path, dst_path);
auto ProgressBox::CopyFile(fs::Fs* fs, const fs::FsPath& src_path, const fs::FsPath& dst_path, bool single_threaded) -> Result {
return CopyFile(fs, fs, src_path, dst_path, single_threaded);
}
auto ProgressBox::CopyFile(const fs::FsPath& src_path, const fs::FsPath& dst_path) -> Result {
auto ProgressBox::CopyFile(const fs::FsPath& src_path, const fs::FsPath& dst_path, bool single_threaded) -> Result {
fs::FsNativeSd fs;
R_TRY(fs.GetFsOpenResult());
return CopyFile(&fs, src_path, dst_path);
return CopyFile(&fs, src_path, dst_path, single_threaded);
}
void ProgressBox::Yield() {

View File

@@ -67,14 +67,14 @@ Result Usb::WaitForConnection(u64 timeout, u8 flags, std::span<const std::string
Result Usb::PollCommands() {
tinfoil::USBCmdHeader header;
R_TRY(m_usb->TransferAll(true, &header, sizeof(header)));
R_UNLESS(header.magic == tinfoil::Magic_Command0, Result_BadMagic);
R_UNLESS(header.magic == tinfoil::Magic_Command0, Result_UsbUploadBadMagic);
if (header.cmdId == tinfoil::USBCmdId::EXIT) {
return Result_Exit;
R_THROW(Result_UsbUploadExit);
} else if (header.cmdId == tinfoil::USBCmdId::FILE_RANGE) {
return FileRangeCmd(header.dataSize);
} else {
return Result_BadCommand;
R_THROW(Result_UsbUploadBadCommand);
}
}

View File

@@ -4,22 +4,6 @@
#include <ranges>
#include <cstring>
namespace sphaira::usb {
namespace {
// TODO: pr missing speed fields to libnx.
enum { UsbDeviceSpeed_None = 0x0 };
enum { UsbDeviceSpeed_Low = 0x1 };
constexpr u16 DEVICE_SPEED[] = {
[UsbDeviceSpeed_None] = 0x0,
[UsbDeviceSpeed_Low] = 0x0,
[UsbDeviceSpeed_Full] = 0x40,
[UsbDeviceSpeed_High] = 0x200,
[UsbDeviceSpeed_Super] = 0x400,
};
// TODO: pr this to libnx.
Result usbDsGetSpeed(UsbDeviceSpeed *out) {
if (hosversionBefore(8,0,0)) {
return MAKERESULT(Module_Libnx, LibnxError_IncompatSysVer);
@@ -29,6 +13,17 @@ Result usbDsGetSpeed(UsbDeviceSpeed *out) {
return serviceDispatchOut(usbDsGetServiceSession(), hosversionAtLeast(11,0,0) ? 11 : 12, *out);
}
namespace sphaira::usb {
namespace {
constexpr u16 DEVICE_SPEED[] = {
[UsbDeviceSpeed_None] = 0x0,
[UsbDeviceSpeed_Low] = 0x0,
[UsbDeviceSpeed_Full] = 0x40,
[UsbDeviceSpeed_High] = 0x200,
[UsbDeviceSpeed_Super] = 0x400,
};
} // namespace
UsbDs::~UsbDs() {
@@ -216,7 +211,7 @@ Result UsbDs::WaitUntilConfigured(u64 timeout) {
// check if we got one of the cancel events.
if (R_SUCCEEDED(rc) && idx == waiters.size() - 1) {
rc = Result_Cancelled;
rc = Result_UsbCancelled;
break;
}
@@ -254,7 +249,7 @@ Result UsbDs::GetSpeed(UsbDeviceSpeed* out, u16* max_packet_size) {
}
*max_packet_size = DEVICE_SPEED[*out];
R_UNLESS(*max_packet_size > 0, 0x1);
R_UNLESS(*max_packet_size > 0, Result_UsbDsBadDeviceSpeed);
R_SUCCEED();
}
@@ -275,7 +270,7 @@ Result UsbDs::WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) {
// check if we got one of the cancel events.
if (R_SUCCEEDED(rc) && idx == waiters.size() - 1) {
log_write("got usb cancel event\n");
rc = Result_Cancelled;
rc = Result_UsbCancelled;
} else if (R_SUCCEEDED(rc) && idx == waiters.size() - 2) {
log_write("got usbDsGetStateChangeEvent() event\n");
m_max_packet_size = 0;

View File

@@ -76,7 +76,7 @@ Result UsbHs::IsUsbConnected(u64 timeout) {
R_TRY(waitObjects(&idx, waiters.data(), waiters.size(), timeout));
if (idx == waiters.size() - 1) {
return Result_Cancelled;
return Result_UsbCancelled;
}
return Connect();
@@ -168,7 +168,7 @@ Result UsbHs::WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) {
// check if we got one of the cancel events.
if (R_SUCCEEDED(rc) && idx == waiters.size() - 1) {
log_write("got usb cancel event\n");
rc = Result_Cancelled;
rc = Result_UsbCancelled;
} else if (R_SUCCEEDED(rc) && idx == waiters.size() - 2) {
log_write("got usb timeout event\n");
rc = KERNELRESULT(TimedOut);

View File

@@ -64,7 +64,7 @@ Result Nsp::GetCollections(Collections& out) {
// get header
Pfs0Header header{};
R_TRY(m_source->Read(std::addressof(header), off, sizeof(header), std::addressof(bytes_read)));
R_UNLESS(header.magic == PFS0_MAGIC, 0x1);
R_UNLESS(header.magic == PFS0_MAGIC, Result_NspBadMagic);
off += bytes_read;
// get file table
@@ -120,9 +120,9 @@ auto Nsp::Build(std::span<CollectionEntry> entries, s64& size) -> std::vector<u8
if (padded_string_table_size == string_table.size()) {
padded_string_table_size += 0x20;
}
string_table.resize(padded_string_table_size);
header.magic = PFS0_MAGIC;
header.total_files = entries.size();
header.string_table_size = string_table.size();

View File

@@ -37,7 +37,7 @@ Result Hfs0GetPartition(source::Base* source, s64 off, Hfs0& out) {
// get header
R_TRY(source->Read(std::addressof(out.header), off, sizeof(out.header), std::addressof(bytes_read)));
R_UNLESS(out.header.magic == HFS0_MAGIC, 0x1);
R_UNLESS(out.header.magic == HFS0_MAGIC, Result_XciBadMagic);
off += bytes_read;
// get file table

View File

@@ -36,6 +36,13 @@ Result ListTicket(u32 cmd_id, s32 *out_entries_written, FsRightsId* out_ids, s32
return rc;
}
Result EncyrptDecryptTitleKey(keys::KeyEntry& out, u8 key_gen, const keys::Keys& keys, bool is_encryptor) {
keys::KeyEntry title_kek;
R_TRY(keys.GetTitleKek(std::addressof(title_kek), key_gen));
crypto::cryptoAes128(std::addressof(out), std::addressof(out), std::addressof(title_kek), is_encryptor);
R_SUCCEED();
}
} // namespace
Result Initialize() {
@@ -130,63 +137,53 @@ Result GetCommonTicketAndCertificateData(u64 *tik_size_out, u64 *cert_size_out,
return rc;
}
typedef enum {
TikPropertyMask_None = 0,
TikPropertyMask_PreInstallation = BIT(0), ///< Determines if the title comes pre-installed on the device. Most likely unused -- a remnant from previous ticket formats.
TikPropertyMask_SharedTitle = BIT(1), ///< Determines if the title holds shared contents only. Most likely unused -- a remnant from previous ticket formats.
TikPropertyMask_AllContents = BIT(2), ///< Determines if the content index mask shall be bypassed. Most likely unused -- a remnant from previous ticket formats.
TikPropertyMask_DeviceLinkIndepedent = BIT(3), ///< Determines if the console should *not* connect to the Internet to verify if the title's being used by the primary console.
TikPropertyMask_Volatile = BIT(4), ///< Determines if the ticket copy inside ticket.bin is available after reboot. Can be encrypted.
TikPropertyMask_ELicenseRequired = BIT(5), ///< Determines if the console should connect to the Internet to perform license verification.
TikPropertyMask_Count = 6 ///< Total values supported by this enum.
} TikPropertyMask;
Result GetTicketDataOffset(std::span<const u8> ticket, u64& out) {
Result GetTicketDataOffset(std::span<const u8> ticket, u64& out, bool is_cert) {
log_write("inside es\n");
u32 signature_type;
std::memcpy(std::addressof(signature_type), ticket.data(), sizeof(signature_type));
u32 signature_size;
switch (signature_type) {
case es::TicketSigantureType_RSA_4096_SHA1: log_write("RSA-4096 PKCS#1 v1.5 with SHA-1\n"); signature_size = 0x200; break;
case es::TicketSigantureType_RSA_2048_SHA1: log_write("RSA-2048 PKCS#1 v1.5 with SHA-1\n"); signature_size = 0x100; break;
case es::TicketSigantureType_ECDSA_SHA1: log_write("ECDSA with SHA-1\n"); signature_size = 0x3C; break;
case es::TicketSigantureType_RSA_4096_SHA256: log_write("RSA-4096 PKCS#1 v1.5 with SHA-256\n"); signature_size = 0x200; break;
case es::TicketSigantureType_RSA_2048_SHA256: log_write("RSA-2048 PKCS#1 v1.5 with SHA-256\n"); signature_size = 0x100; break;
case es::TicketSigantureType_ECDSA_SHA256: log_write("ECDSA with SHA-256\n"); signature_size = 0x3C; break;
case es::TicketSigantureType_HMAC_SHA1_160: log_write("HMAC-SHA1-160\n"); signature_size = 0x14; break;
default: log_write("unknown ticket\n"); return 0x1;
if (is_cert) {
signature_type = std::byteswap(signature_type);
}
switch (signature_type) {
case SigType_Rsa4096Sha1: log_write("RSA-4096 PKCS#1 v1.5 with SHA-1\n"); out = sizeof(SignatureBlockRsa4096); break;
case SigType_Rsa2048Sha1: log_write("RSA-2048 PKCS#1 v1.5 with SHA-1\n"); out = sizeof(SignatureBlockRsa2048); break;
case SigType_Ecc480Sha1: log_write("ECDSA with SHA-1\n"); out = sizeof(SignatureBlockEcc480); break;
case SigType_Rsa4096Sha256: log_write("RSA-4096 PKCS#1 v1.5 with SHA-256\n"); out = sizeof(SignatureBlockRsa4096); break;
case SigType_Rsa2048Sha256: log_write("RSA-2048 PKCS#1 v1.5 with SHA-256\n"); out = sizeof(SignatureBlockRsa2048); break;
case SigType_Ecc480Sha256: log_write("ECDSA with SHA-256\n"); out = sizeof(SignatureBlockEcc480); break;
case SigType_Hmac160Sha1: log_write("HMAC-SHA1-160\n"); out = sizeof(SignatureBlockHmac160); break;
default: log_write("unknown ticket: %u\n", signature_type); R_THROW(Result_EsBadTitleKeyType);
}
// align-up to 0x40.
out = ((signature_size + sizeof(signature_type)) + 0x3F) & ~0x3F;
R_SUCCEED();
}
Result GetTicketData(std::span<const u8> ticket, es::TicketData* out) {
u64 data_off;
R_TRY(GetTicketDataOffset(ticket, data_off));
if (ticket.size() < data_off + sizeof(*out)) {
log_write("[ES] invalid ticket size: %zu vs %zu\n", ticket.size(), data_off + sizeof(*out));
R_THROW(Result_EsBadTicketSize);
}
std::memcpy(out, ticket.data() + data_off, sizeof(*out));
// validate ticket data.
R_UNLESS(out->ticket_version1 == 0x2, Result_InvalidTicketVersion); // must be version 2.
R_UNLESS(out->title_key_type == es::TicketTitleKeyType_Common || out->title_key_type == es::TicketTitleKeyType_Personalized, Result_InvalidTicketKeyType);
R_UNLESS(out->master_key_revision <= 0x20, Result_InvalidTicketKeyRevision);
log_write("[ES] validating ticket data\n");
R_UNLESS(out->format_version == 0x2, Result_EsInvalidTicketFromatVersion); // must be version 2.
R_UNLESS(out->title_key_type == es::TitleKeyType_Common || out->title_key_type == es::TitleKeyType_Personalized, Result_EsInvalidTicketKeyType);
R_UNLESS(out->master_key_revision <= 0x20, Result_EsInvalidTicketKeyRevision);
log_write("[ES] valid ticket data\n");
R_SUCCEED();
}
Result SetTicketData(std::span<u8> ticket, const es::TicketData* in) {
u64 data_off;
R_TRY(GetTicketDataOffset(ticket, data_off));
std::memcpy(ticket.data() + data_off, in, sizeof(*in));
R_SUCCEED();
}
Result GetTitleKey(keys::KeyEntry& out, const TicketData& data, const keys::Keys& keys) {
if (data.title_key_type == es::TicketTitleKeyType_Common) {
if (data.title_key_type == es::TitleKeyType_Common) {
std::memcpy(std::addressof(out), data.title_key_block, sizeof(out));
} else if (data.title_key_type == es::TicketTitleKeyType_Personalized) {
} else if (data.title_key_type == es::TitleKeyType_Personalized) {
auto rsa_key = (const es::EticketRsaDeviceKey*)keys.eticket_device_key.key;
log_write("personalised ticket\n");
log_write("master_key_revision: %u\n", data.master_key_revision);
@@ -194,41 +191,141 @@ Result GetTitleKey(keys::KeyEntry& out, const TicketData& data, const keys::Keys
log_write("properties_bitfield: 0x%X\n", data.properties_bitfield);
log_write("device_id: 0x%lX vs 0x%lX\n", data.device_id, std::byteswap(rsa_key->device_id));
R_UNLESS(data.device_id == std::byteswap(rsa_key->device_id), 0x1);
R_UNLESS(data.device_id == std::byteswap(rsa_key->device_id), Result_EsPersonalisedTicketDeviceIdMissmatch);
log_write("device id is same\n");
u8 out_keydata[RSA2048_BYTES]{};
size_t out_keydata_size;
R_UNLESS(rsa2048OaepDecrypt(out_keydata, sizeof(out_keydata), data.title_key_block, rsa_key->modulus, &rsa_key->public_exponent, sizeof(rsa_key->public_exponent), rsa_key->private_exponent, sizeof(rsa_key->private_exponent), NULL, 0, &out_keydata_size), 0x1);
R_UNLESS(out_keydata_size >= sizeof(out), 0x1);
R_UNLESS(rsa2048OaepDecrypt(out_keydata, sizeof(out_keydata), data.title_key_block, rsa_key->modulus, &rsa_key->public_exponent, sizeof(rsa_key->public_exponent), rsa_key->private_exponent, sizeof(rsa_key->private_exponent), NULL, 0, &out_keydata_size), Result_EsFailedDecryptPersonalisedTicket);
R_UNLESS(out_keydata_size >= sizeof(out), Result_EsBadDecryptedPersonalisedTicketSize);
std::memcpy(std::addressof(out), out_keydata, sizeof(out));
} else {
R_THROW(0x1);
R_THROW(Result_EsBadTitleKeyType);
}
R_SUCCEED();
}
Result DecryptTitleKey(keys::KeyEntry& out, u8 key_gen, const keys::Keys& keys) {
keys::KeyEntry title_kek;
R_TRY(keys.GetTitleKek(std::addressof(title_kek), key_gen));
crypto::cryptoAes128(std::addressof(out), std::addressof(out), std::addressof(title_kek), false);
R_SUCCEED();
return EncyrptDecryptTitleKey(out, key_gen, keys, false);
}
// todo: i thought i already wrote the code for this??
// todo: patch the ticket.
Result PatchTicket(std::span<u8> ticket, const keys::Keys& keys) {
Result EncryptTitleKey(keys::KeyEntry& out, u8 key_gen, const keys::Keys& keys) {
return EncyrptDecryptTitleKey(out, key_gen, keys, true);
}
// this function is taken from nxdumptool
Result ShouldPatchTicket(const TicketData& data, std::span<const u8> ticket, std::span<const u8> cert_chain, bool patch_personalised, bool& should_patch) {
should_patch = false;
if (data.title_key_type == es::TitleKeyType_Common) {
SigType tik_sig_type;
std::memcpy(&tik_sig_type, ticket.data(), sizeof(tik_sig_type));
if (tik_sig_type != SigType_Rsa2048Sha256) {
R_SUCCEED();
}
const auto cert_name = std::strrchr(data.issuer, '-') + 1;
R_UNLESS(cert_name, Result_EsBadTitleKeyType);
const auto cert_name_span = std::span{(const u8*)cert_name, std::strlen(cert_name)};
// find the cert from inside the cert chain.
const auto it = std::ranges::search(cert_chain, cert_name_span);
R_UNLESS(!it.empty(), Result_EsBadTitleKeyType);
const auto cert = cert_chain.subspan(std::distance(cert_chain.begin(), it.begin()) - offsetof(CertHeader, subject));
const auto cert_header = (const CertHeader*)cert.data();
const auto pub_key_type = std::byteswap<u32>(cert_header->pub_key_type);
log_write("[ES] cert_header->issuer: %s\n", cert_header->issuer);
log_write("[ES] cert_header->pub_key_type: %u\n", pub_key_type);
log_write("[ES] cert_header->subject: %s\n", cert_header->subject);
std::span<const u8> public_key{};
u32 public_exponent{};
switch (pub_key_type) {
case PubKeyType_Rsa4096: {
auto pub_key = (const PublicKeyBlockRsa4096*)(cert.data() + sizeof(CertHeader));
public_key = pub_key->public_key;
public_exponent = pub_key->public_exponent;
} break;
case PubKeyType_Rsa2048: {
auto pub_key = (const PublicKeyBlockRsa2048*)(cert.data() + sizeof(CertHeader));
public_key = pub_key->public_key;
public_exponent = pub_key->public_exponent;
} break;
case PubKeyType_Ecc480: {
R_SUCCEED();
} break;
default:
R_THROW(Result_EsBadTitleKeyType);
}
const auto tik = (const TicketRsa2048*)ticket.data();
const auto check_data = ticket.subspan(offsetof(TicketRsa2048, data));
if (rsa2048VerifySha256BasedPkcs1v15Signature(check_data.data(), check_data.size(), tik->signature_block.sign, public_key.data(), &public_exponent, sizeof(public_exponent))) {
log_write("[ES] common ticket is same\n");
} else {
log_write("[ES] common ticket is modified\n");
should_patch = true;
}
R_SUCCEED();
} else if (data.title_key_type == es::TitleKeyType_Personalized) {
if (patch_personalised) {
log_write("[ES] patching personalised ticket\n");
} else {
log_write("[ES] keeping personalised ticket\n");
}
should_patch = patch_personalised;
R_SUCCEED();
} else {
R_THROW(Result_EsBadTitleKeyType);
}
}
Result ShouldPatchTicket(std::span<const u8> ticket, std::span<const u8> cert_chain, bool patch_personalised, bool& should_patch) {
TicketData data;
R_TRY(GetTicketData(ticket, &data));
if (data.title_key_type == es::TicketTitleKeyType_Common) {
// todo: verify common signature
} else if (data.title_key_type == es::TicketTitleKeyType_Personalized) {
return ShouldPatchTicket(data, ticket, cert_chain, patch_personalised, should_patch);
}
Result PatchTicket(std::vector<u8>& ticket, std::span<const u8> cert_chain, u8 key_gen, const keys::Keys& keys, bool patch_personalised) {
TicketData data;
R_TRY(GetTicketData(ticket, &data));
// check if we should create a fake common ticket.
bool should_patch;
R_TRY(ShouldPatchTicket(data, ticket, cert_chain, patch_personalised, should_patch));
if (!should_patch) {
R_SUCCEED();
}
// store copy of rights id an title key.
keys::KeyEntry title_key;
R_TRY(GetTitleKey(title_key, data, keys));
const auto rights_id = data.rights_id;
// following StandardNSP format.
TicketRsa2048 out{};
out.signature_block.sig_type = SigType_Rsa2048Sha256;
std::memset(out.signature_block.sign, 0xFF, sizeof(out.signature_block.sign));
std::strcpy(out.data.issuer, "Root-CA00000003-XS00000020");
std::memcpy(out.data.title_key_block, title_key.key, sizeof(title_key.key));
out.data.format_version = 0x2;
out.data.master_key_revision = key_gen;
out.data.rights_id = rights_id;
out.data.sect_hdr_offset = ticket.size();
// overwrite old ticket with new fake ticket data.
ticket.resize(sizeof(out));
std::memcpy(ticket.data(), &out, sizeof(out));
R_SUCCEED();
}

View File

@@ -117,7 +117,7 @@ Result parse_keys(Keys& out, bool read_from_file) {
if (public_exponent == 0) {
log_write("eticket device id is NULL\n");
}
R_THROW(0x1);
R_THROW(Result_KeyFailedDecyptETicketDeviceKey);
} else {
log_write("eticket match\n");
}

View File

@@ -131,7 +131,7 @@ Result EncryptKeak(const keys::Keys& keys, Header& header, u8 key_generation) {
}
Result VerifyFixedKey(const Header& header) {
R_UNLESS(header.sig_key_gen < std::size(nca_hdr_fixed_key_moduli_retail), 0x1);
R_UNLESS(header.sig_key_gen < std::size(nca_hdr_fixed_key_moduli_retail), Result_NcaBadSigKeyGen);
auto mod = nca_hdr_fixed_key_moduli_retail[header.sig_key_gen];
const u8 E[3] = { 1, 0, 1 };
@@ -141,7 +141,7 @@ Result VerifyFixedKey(const Header& header) {
new_header.distribution_type ^= 1;
if (!rsa2048VerifySha256BasedPssSignature(&new_header.magic, 0x200, new_header.rsa_fixed_key, mod, E, sizeof(E))) {
log_write("FAILED nca header hash\n");
R_THROW(0x1);
R_THROW(Result_NcaFailedNcaHeaderHashVerify);
} else {
log_write("WARNING! nca is converted! distribution_type: %u\n", new_header.distribution_type);
R_SUCCEED();

View File

@@ -6,7 +6,7 @@ namespace sphaira::yati::source {
Result Stream::Read(void* _buf, s64 off, s64 size, u64* bytes_read_out) {
// streams don't allow for random access (seeking backwards).
R_UNLESS(off >= m_offset, 0x1);
R_UNLESS(off >= m_offset, Result_StreamBadSeek);
auto buf = static_cast<u8*>(_buf);
*bytes_read_out = 0;

View File

@@ -23,8 +23,8 @@ Usb::~Usb() {
Result Usb::WaitForConnection(u64 timeout, std::vector<std::string>& out_names) {
tinfoil::TUSHeader header;
R_TRY(m_usb->TransferAll(true, &header, sizeof(header), timeout));
R_UNLESS(header.magic == tinfoil::Magic_List0, Result_BadMagic);
R_UNLESS(header.nspListSize > 0, Result_BadCount);
R_UNLESS(header.magic == tinfoil::Magic_List0, Result_UsbBadMagic);
R_UNLESS(header.nspListSize > 0, Result_UsbBadCount);
m_flags = header.flags;
log_write("[USB] got header, flags: 0x%X\n", m_flags);
@@ -42,7 +42,7 @@ Result Usb::WaitForConnection(u64 timeout, std::vector<std::string>& out_names)
log_write("got name: %s\n", name.c_str());
}
R_UNLESS(!out_names.empty(), Result_BadCount);
R_UNLESS(!out_names.empty(), Result_UsbBadCount);
log_write("USB SUCCESS\n");
R_SUCCEED();
}

View File

@@ -65,8 +65,12 @@ struct TikCollection {
std::vector<u8> cert{};
// set via the name of the ticket.
FsRightsId rights_id{};
// retrieved via the master key set in nca.
u8 key_gen{};
// set if ticket is required by an nca.
bool required{};
// set if ticket has already been patched.
bool patched{};
};
struct Yati;
@@ -287,7 +291,7 @@ struct Yati {
};
auto ThreadData::GetResults() -> Result {
R_UNLESS(!yati->pbox->ShouldExit(), Result_Cancelled);
R_TRY(yati->pbox->ShouldExitResult());
R_TRY(read_result);
R_TRY(decompress_result);
R_TRY(write_result);
@@ -307,8 +311,10 @@ void ThreadData::WakeAllThreads() {
Result ThreadData::Read(void* buf, s64 size, u64* bytes_read) {
size = std::min<s64>(size, nca->size - read_offset);
const auto rc = yati->source->Read(buf, nca->offset + read_offset, size, bytes_read);
R_TRY(rc);
R_UNLESS(size == *bytes_read, Result_YatiInvalidNcaReadSize);
read_offset += *bytes_read;
R_UNLESS(size == *bytes_read, Result_InvalidNcaReadSize);
return rc;
}
@@ -317,10 +323,6 @@ auto isRightsIdValid(FsRightsId id) -> bool {
return 0 != std::memcmp(std::addressof(id), std::addressof(empty_id), sizeof(id));
}
auto getKeyGenFromRightsId(FsRightsId id) -> u8 {
return id.c[sizeof(id) - 1];
}
struct HashStr {
char str[0x21];
};
@@ -333,6 +335,38 @@ HashStr hexIdToStr(auto id) {
return str;
}
auto GetTicketCollection(const nca::Header& header, std::span<TikCollection> tik) -> TikCollection* {
TikCollection* ticket{};
if (isRightsIdValid(header.rights_id)) {
auto it = std::ranges::find_if(tik, [&header](auto& e){
return !std::memcmp(&header.rights_id, &e.rights_id, sizeof(e.rights_id));
});
if (it != tik.end()) {
it->required = true;
it->key_gen = header.key_gen;
ticket = &(*it);
}
}
return ticket;
}
Result HasRequiredTicket(const nca::Header& header, TikCollection* ticket) {
if (isRightsIdValid(header.rights_id)) {
log_write("looking for ticket %s\n", hexIdToStr(header.rights_id).str);
R_UNLESS(ticket, Result_YatiTicketNotFound);
log_write("ticket found\n");
}
R_SUCCEED();
}
Result HasRequiredTicket(const nca::Header& header, std::span<TikCollection> tik) {
auto ticket = GetTicketCollection(header, tik);
return HasRequiredTicket(header, ticket);
}
// read thread reads all data from the source, it also handles
// parsing ncz headers, sections and reading ncz blocks
Result Yati::readFuncInternal(ThreadData* t) {
@@ -374,7 +408,7 @@ Result Yati::readFuncInternal(ThreadData* t) {
std::memcpy(std::addressof(header), buf.data() + 0x4000, sizeof(header));
if (header.magic == NCZ_SECTION_MAGIC) {
// validate section header.
R_UNLESS(header.total_sections, Result_InvalidNczSectionCount);
R_UNLESS(header.total_sections, Result_YatiInvalidNczSectionCount);
buf_size = 0x4000;
log_write("found ncz, total number of sections: %zu\n", header.total_sections);
@@ -390,10 +424,10 @@ Result Yati::readFuncInternal(ThreadData* t) {
log_write("storing temp data of size: %zu\n", temp_buf.size());
} else {
// validate block header.
R_UNLESS(t->ncz_block_header.version == 0x2, Result_InvalidNczBlockVersion);
R_UNLESS(t->ncz_block_header.type == 0x1, Result_InvalidNczBlockType);
R_UNLESS(t->ncz_block_header.total_blocks, Result_InvalidNczBlockTotal);
R_UNLESS(t->ncz_block_header.block_size_exponent >= 14 && t->ncz_block_header.block_size_exponent <= 32, Result_InvalidNczBlockSizeExponent);
R_UNLESS(t->ncz_block_header.version == 0x2, Result_YatiInvalidNczBlockVersion);
R_UNLESS(t->ncz_block_header.type == 0x1, Result_YatiInvalidNczBlockType);
R_UNLESS(t->ncz_block_header.total_blocks, Result_YatiInvalidNczBlockTotal);
R_UNLESS(t->ncz_block_header.block_size_exponent >= 14 && t->ncz_block_header.block_size_exponent <= 32, Result_YatiInvalidNczBlockSizeExponent);
// read blocks (array of block sizes).
std::vector<ncz::Block> blocks(t->ncz_block_header.total_blocks);
@@ -460,7 +494,7 @@ Result Yati::decompressFuncInternal(ThreadData* t) {
return e.InRange(written);
});
R_UNLESS(it != t->ncz_sections.cend(), Result_NczSectionNotFound);
R_UNLESS(it != t->ncz_sections.cend(), Result_YatiNczSectionNotFound);
ncz_section = &(*it);
log_write("[NCZ] found new section: %zu\n", written);
@@ -515,7 +549,7 @@ Result Yati::decompressFuncInternal(ThreadData* t) {
nca::Header header{};
crypto::cryptoAes128Xts(buf.data(), std::addressof(header), keys.header_key, 0, 0x200, sizeof(header), false);
log_write("verifying nca header magic\n");
R_UNLESS(header.magic == 0x3341434E, Result_InvalidNcaMagic);
R_UNLESS(header.magic == 0x3341434E, Result_YatiInvalidNcaMagic);
log_write("nca magic is ok! type: %u\n", header.content_type);
// store the unmodified header.
@@ -530,33 +564,24 @@ Result Yati::decompressFuncInternal(ThreadData* t) {
}
t->write_size = header.size;
log_write("setting placeholder size: %zu\n", header.size);
R_TRY(ncmContentStorageSetPlaceHolderSize(std::addressof(cs), std::addressof(t->nca->placeholder_id), header.size));
log_write("setting placeholder size: %zu\n", t->write_size);
R_TRY(ncmContentStorageSetPlaceHolderSize(std::addressof(cs), std::addressof(t->nca->placeholder_id), t->write_size));
if (!config.ignore_distribution_bit && header.distribution_type == nca::DistributionType_GameCard) {
header.distribution_type = nca::DistributionType_System;
t->nca->modified = true;
}
TikCollection* ticket = nullptr;
if (isRightsIdValid(header.rights_id)) {
auto it = std::ranges::find_if(t->tik, [&header](auto& e){
return !std::memcmp(&header.rights_id, &e.rights_id, sizeof(e.rights_id));
});
// try and get the ticket, if the nca requires it.
auto ticket = GetTicketCollection(header, t->tik);
R_TRY(HasRequiredTicket(header, ticket));
log_write("looking for ticket %s\n", hexIdToStr(header.rights_id).str);
R_UNLESS(it != t->tik.end(), Result_TicketNotFound);
log_write("ticket found\n");
it->required = true;
ticket = &(*it);
}
if ((config.convert_to_standard_crypto && isRightsIdValid(header.rights_id)) || config.lower_master_key) {
if ((config.convert_to_standard_crypto && ticket) || config.lower_master_key) {
t->nca->modified = true;
u8 keak_generation;
if (isRightsIdValid(header.rights_id)) {
const auto key_gen = getKeyGenFromRightsId(header.rights_id);
if (ticket) {
const auto key_gen = header.key_gen;
log_write("converting to standard crypto: 0x%X 0x%X\n", key_gen, header.key_gen);
// fetch ticket data block.
@@ -564,20 +589,7 @@ Result Yati::decompressFuncInternal(ThreadData* t) {
R_TRY(es::GetTicketData(ticket->ticket, std::addressof(ticket_data)));
// validate that this indeed the correct ticket.
R_UNLESS(!std::memcmp(std::addressof(header.rights_id), std::addressof(ticket_data.rights_id), sizeof(header.rights_id)), Result_InvalidTicketBadRightsId);
// some scene releases use buggy software which set the master key
// revision in the properties bitfield...lol, still happens in 2025.
// to fix this, get mkey rev from the rights id
// todo: verify this code.
if (ticket_data.title_key_type == es::TicketTitleKeyType_Common) {
if (!ticket_data.master_key_revision && ticket_data.master_key_revision != getKeyGenFromRightsId(ticket_data.rights_id) && ticket_data.properties_bitfield) {
// get the actual mkey
ticket_data.master_key_revision = getKeyGenFromRightsId(ticket_data.rights_id);
// unset the properties
ticket_data.properties_bitfield = 0;
}
}
R_UNLESS(!std::memcmp(std::addressof(header.rights_id), std::addressof(ticket_data.rights_id), sizeof(header.rights_id)), Result_YatiInvalidTicketBadRightsId);
// decrypt title key.
keys::KeyEntry title_key;
@@ -624,7 +636,7 @@ Result Yati::decompressFuncInternal(ThreadData* t) {
return e.InRange(decompress_buf_off);
});
R_UNLESS(it != t->ncz_blocks.cend(), Result_NczBlockNotFound);
R_UNLESS(it != t->ncz_blocks.cend(), Result_YatiNczBlockNotFound);
log_write("[NCZ] found new block: %zu off: %zd size: %zd\n", decompress_buf_off, it->offset, it->size);
ncz_block = &(*it);
}
@@ -657,7 +669,7 @@ Result Yati::decompressFuncInternal(ThreadData* t) {
if (ZSTD_isError(res)) {
log_write("[NCZ] ZSTD_decompressStream() pos: %zu size: %zu res: %zd msg: %s\n", input.pos, input.size, res, ZSTD_getErrorName(res));
}
R_UNLESS(!ZSTD_isError(res), Result_InvalidNczZstdError);
R_UNLESS(!ZSTD_isError(res), Result_YatiInvalidNczZstdError);
t->decompress_offset += output.pos;
inflate_offset += output.pos;
@@ -813,6 +825,7 @@ Result Yati::Setup(const ConfigOverride& override) {
config.skip_rsa_header_fixed_key_verify = override.skip_rsa_header_fixed_key_verify.value_or(App::GetApp()->m_skip_rsa_header_fixed_key_verify.Get());
config.skip_rsa_npdm_fixed_key_verify = override.skip_rsa_npdm_fixed_key_verify.value_or(App::GetApp()->m_skip_rsa_npdm_fixed_key_verify.Get());
config.ignore_distribution_bit = override.ignore_distribution_bit.value_or(App::GetApp()->m_ignore_distribution_bit.Get());
config.convert_to_common_ticket = override.convert_to_common_ticket.value_or(App::GetApp()->m_convert_to_common_ticket.Get());
config.convert_to_standard_crypto = override.convert_to_standard_crypto.value_or(App::GetApp()->m_convert_to_standard_crypto.Get());
config.lower_master_key = override.lower_master_key.value_or(App::GetApp()->m_lower_master_key.Get());
config.lower_system_version = override.lower_system_version.value_or(App::GetApp()->m_lower_system_version.Get());
@@ -837,12 +850,14 @@ Result Yati::Setup(const ConfigOverride& override) {
}
Result Yati::InstallNcaInternal(std::span<TikCollection> tickets, NcaCollection& nca) {
if (config.skip_if_already_installed) {
if (config.skip_if_already_installed || config.ticket_only) {
R_TRY(ncmContentStorageHas(std::addressof(cs), std::addressof(nca.skipped), std::addressof(nca.content_id)));
if (nca.skipped) {
log_write("\tskipped nca as it's already installed ncmContentStorageHas()\n");
R_TRY(ncmContentStorageReadContentIdFile(std::addressof(cs), std::addressof(nca.header), sizeof(nca.header), std::addressof(nca.content_id), 0));
crypto::cryptoAes128Xts(std::addressof(nca.header), std::addressof(nca.header), keys.header_key, 0, 0x200, sizeof(nca.header), false);
R_TRY(HasRequiredTicket(nca.header, tickets));
R_SUCCEED();
}
}
@@ -921,7 +936,7 @@ Result Yati::InstallNcaInternal(std::span<TikCollection> tickets, NcaCollection&
if (!config.skip_nca_hash_verify && !nca.modified) {
if (std::memcmp(&nca.content_id, nca.hash, sizeof(nca.content_id))) {
log_write("nca hash is invalid!!!!\n");
R_UNLESS(!std::memcmp(&nca.content_id, nca.hash, sizeof(nca.content_id)), Result_InvalidNcaSha256);
R_UNLESS(!std::memcmp(&nca.content_id, nca.hash, sizeof(nca.content_id)), Result_YatiInvalidNcaSha256);
} else {
log_write("nca hash is valid!\n");
}
@@ -987,7 +1002,7 @@ Result Yati::InstallCnmtNca(std::span<TikCollection> tickets, CnmtCollection& cn
return e.name.find(str.str) != e.name.npos;
});
R_UNLESS(it != collections.cend(), Result_NcaNotFound);
R_UNLESS(it != collections.cend(), Result_YatiNcaNotFound);
log_write("found: %s\n", str.str);
cnmt.infos.emplace_back(packed_info);
@@ -1044,7 +1059,7 @@ Result Yati::ParseTicketsIntoCollection(std::vector<TikCollection>& tickets, con
return e.name.find(str) != e.name.npos;
});
R_UNLESS(cert != collections.cend(), Result_CertNotFound);
R_UNLESS(cert != collections.cend(), Result_YatiCertNotFound);
entry.ticket.resize(collection.size);
entry.cert.resize(cert->size);
@@ -1103,23 +1118,7 @@ Result Yati::GetLatestVersion(const CnmtCollection& cnmt, u32& version_out, bool
}
Result Yati::ShouldSkip(const CnmtCollection& cnmt, bool& skip) {
// skip invalid types
if (!(cnmt.key.type & 0x80)) {
log_write("\tskipping: invalid: %u\n", cnmt.key.type);
skip = true;
} else if (config.skip_base && cnmt.key.type == NcmContentMetaType_Application) {
log_write("\tskipping: [NcmContentMetaType_Application]\n");
skip = true;
} else if (config.skip_patch && cnmt.key.type == NcmContentMetaType_Patch) {
log_write("\tskipping: [NcmContentMetaType_Application]\n");
skip = true;
} else if (config.skip_addon && cnmt.key.type == NcmContentMetaType_AddOnContent) {
log_write("\tskipping: [NcmContentMetaType_AddOnContent]\n");
skip = true;
} else if (config.skip_data_patch && cnmt.key.type == NcmContentMetaType_DataPatch) {
log_write("\tskipping: [NcmContentMetaType_DataPatch]\n");
skip = true;
} else if (config.skip_if_already_installed) {
if (!skip && config.skip_if_already_installed) {
bool has;
R_TRY(ncmContentMetaDatabaseHas(std::addressof(db), std::addressof(has), std::addressof(cnmt.key)));
if (has) {
@@ -1128,17 +1127,41 @@ Result Yati::ShouldSkip(const CnmtCollection& cnmt, bool& skip) {
}
}
// skip invalid types
if (!skip) {
if (!(cnmt.key.type & 0x80)) {
log_write("\tskipping: invalid: %u\n", cnmt.key.type);
skip = true;
} else if (config.skip_base && cnmt.key.type == NcmContentMetaType_Application) {
log_write("\tskipping: [NcmContentMetaType_Application]\n");
skip = true;
} else if (config.skip_patch && cnmt.key.type == NcmContentMetaType_Patch) {
log_write("\tskipping: [NcmContentMetaType_Application]\n");
skip = true;
} else if (config.skip_addon && cnmt.key.type == NcmContentMetaType_AddOnContent) {
log_write("\tskipping: [NcmContentMetaType_AddOnContent]\n");
skip = true;
} else if (config.skip_data_patch && cnmt.key.type == NcmContentMetaType_DataPatch) {
log_write("\tskipping: [NcmContentMetaType_DataPatch]\n");
skip = true;
}
}
R_SUCCEED();
}
Result Yati::ImportTickets(std::span<TikCollection> tickets) {
for (auto& ticket : tickets) {
if (ticket.required) {
if (ticket.required || config.ticket_only) {
if (config.skip_ticket) {
log_write("WARNING: skipping ticket install, but it's required!\n");
} else {
log_write("patching ticket\n");
R_TRY(es::PatchTicket(ticket.ticket, keys));
if (!ticket.patched) {
log_write("patching ticket\n");
R_TRY(es::PatchTicket(ticket.ticket, ticket.cert, ticket.key_gen, keys, config.convert_to_common_ticket));
ticket.patched = true;
}
log_write("installing ticket\n");
R_TRY(es::ImportTicket(ticket.ticket.data(), ticket.ticket.size(), ticket.cert.data(), ticket.cert.size()));
ticket.required = false;
@@ -1179,22 +1202,31 @@ Result Yati::RemoveInstalledNcas(const CnmtCollection& cnmt) {
}
}
for (auto& key : keys) {
for (const auto& key : keys) {
log_write("found key: 0x%016lX type: %u version: %u\n", key.id, key.type, key.version);
NcmContentMetaHeader header;
u64 out_size;
log_write("trying to get from db\n");
R_TRY(ncmContentMetaDatabaseGet(std::addressof(db), std::addressof(key), std::addressof(out_size), std::addressof(header), sizeof(header)));
R_UNLESS(out_size == sizeof(header), Result_NcmDbCorruptHeader);
R_UNLESS(out_size == sizeof(header), Result_YatiNcmDbCorruptHeader);
log_write("trying to list infos\n");
std::vector<NcmContentInfo> infos(header.content_count);
s32 content_info_out;
R_TRY(ncmContentMetaDatabaseListContentInfo(std::addressof(db), std::addressof(content_info_out), infos.data(), infos.size(), std::addressof(key), 0));
R_UNLESS(content_info_out == infos.size(), Result_NcmDbCorruptInfos);
R_UNLESS(content_info_out == infos.size(), Result_YatiNcmDbCorruptInfos);
log_write("size matches\n");
for (auto& info : infos) {
for (const auto& info : infos) {
const auto it = std::ranges::find_if(cnmt.ncas, [&info](auto& e){
return !std::memcmp(&e.content_id, &info.content_id, sizeof(e.content_id));
});
// don't delete the nca if we skipped the install.
if ((it != cnmt.ncas.cend() && it->skipped) || (!std::memcmp(&cnmt.content_id, &info.content_id, sizeof(cnmt.content_id)) && cnmt.skipped)) {
continue;
}
R_TRY(ncm::Delete(std::addressof(cs), std::addressof(info.content_id)));
}
@@ -1213,9 +1245,11 @@ Result Yati::RegisterNcasAndPushRecord(const CnmtCollection& cnmt, u32 latest_ve
const auto app_id = ncm::GetAppId(cnmt.key);
// register all nca's
log_write("registering cnmt nca\n");
R_TRY(ncm::Register(std::addressof(cs), std::addressof(cnmt.content_id), std::addressof(cnmt.placeholder_id)));
log_write("registered cnmt nca\n");
if (!cnmt.skipped) {
log_write("registering cnmt nca\n");
R_TRY(ncm::Register(std::addressof(cs), std::addressof(cnmt.content_id), std::addressof(cnmt.placeholder_id)));
log_write("registered cnmt nca\n");
}
for (auto& nca : cnmt.ncas) {
if (!nca.skipped && nca.type != NcmContentType_DeltaFragment) {
@@ -1237,7 +1271,7 @@ Result Yati::RegisterNcasAndPushRecord(const CnmtCollection& cnmt, u32 latest_ve
buf.write(std::addressof(info.info), sizeof(info.info));
}
pbox->NewTransfer("Updating ncm databse"_i18n);
pbox->NewTransfer("Updating ncm database"_i18n);
R_TRY(ncmContentMetaDatabaseSet(std::addressof(db), std::addressof(cnmt.key), buf.buf.data(), buf.tell()));
R_TRY(ncmContentMetaDatabaseCommit(std::addressof(db)));
@@ -1362,7 +1396,7 @@ Result InstallInternalStream(ui::ProgressBox* pbox, std::shared_ptr<source::Base
});
// this will never fail...but just in case.
R_UNLESS(entry != tickets.end(), Result_CertNotFound);
R_UNLESS(entry != tickets.end(), Result_YatiCertNotFound);
u64 bytes_read;
if (collection.name.ends_with(".tik")) {
@@ -1380,7 +1414,7 @@ Result InstallInternalStream(ui::ProgressBox* pbox, std::shared_ptr<source::Base
return e.name == cnmt_nca.name;
});
R_UNLESS(it != ncas.cend(), Result_NczSectionNotFound);
R_UNLESS(it != ncas.cend(), Result_YatiNczSectionNotFound);
const auto type = cnmt_nca.type;
cnmt_nca = *it;
cnmt_nca.type = type;
@@ -1414,7 +1448,7 @@ Result InstallFromFile(ui::ProgressBox* pbox, fs::Fs* fs, const fs::FsPath& path
Result InstallFromSource(ui::ProgressBox* pbox, std::shared_ptr<source::Base> source, const fs::FsPath& path, const ConfigOverride& override) {
const auto ext = std::strrchr(path.s, '.');
R_UNLESS(ext, Result_ContainerNotFound);
R_UNLESS(ext, Result_YatiContainerNotFound);
if (!strcasecmp(ext, ".nsp") || !strcasecmp(ext, ".nsz")) {
return InstallFromContainer(pbox, std::make_unique<container::Nsp>(source), override);
@@ -1422,7 +1456,7 @@ Result InstallFromSource(ui::ProgressBox* pbox, std::shared_ptr<source::Base> so
return InstallFromContainer(pbox, std::make_unique<container::Xci>(source), override);
}
R_THROW(Result_ContainerNotFound);
R_THROW(Result_YatiContainerNotFound);
}
Result InstallFromContainer(ui::ProgressBox* pbox, std::shared_ptr<container::Base> container, const ConfigOverride& override) {