Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e02538405 | ||
|
|
928da0cbda | ||
|
|
267693c6ab | ||
|
|
3f99afaa38 | ||
|
|
8e67e5f0fc | ||
|
|
cb1508e6d5 | ||
|
|
070be1ff94 | ||
|
|
7730eacea8 | ||
|
|
c5e3373fe1 | ||
|
|
d7ec620173 | ||
|
|
1c72350d4a | ||
|
|
4ef15f8b81 | ||
|
|
8fc7b614a0 | ||
|
|
0789a69975 | ||
|
|
b405a816c9 | ||
|
|
99c1db3655 | ||
|
|
6b099de63c | ||
|
|
275707fe27 | ||
|
|
c535b96b12 | ||
|
|
6b77cbb0c0 | ||
|
|
a33d8e1061 | ||
|
|
aaf11211dc | ||
|
|
83b2aca942 | ||
|
|
fbae286dff | ||
|
|
ba9b6b54bf | ||
|
|
1677514355 | ||
|
|
ec1042efa3 | ||
|
|
b03ad4ade3 | ||
|
|
04f6e5d2a8 | ||
|
|
16c074db1a | ||
|
|
8d958a2d1d | ||
|
|
74c1cd3be0 | ||
|
|
0fd5f348e2 | ||
|
|
0c9433d0d3 | ||
|
|
8fb34d42dc | ||
|
|
be831eb04a |
@@ -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)
|
||||
|
||||
@@ -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!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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!": "ページのロードエラー"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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!": "페이지 로딩 오류!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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!": ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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!": "Ошибка загрузки страницы!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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!": "Помилка завантаження сторінки!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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传输(Switch对Switch)",
|
||||
"/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!": "页面加载失败!"
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
>;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
17
sphaira/include/haze_helper.hpp
Normal file
17
sphaira/include/haze_helper.hpp
Normal 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
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -16,6 +16,7 @@ public:
|
||||
private:
|
||||
std::optional<Result> m_code{};
|
||||
std::string m_message{};
|
||||
std::string m_code_message{};
|
||||
};
|
||||
|
||||
} // namespace sphaira::ui
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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{};
|
||||
|
||||
@@ -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};
|
||||
|
||||
64
sphaira/include/ui/menus/install_stream_menu_base.hpp
Normal file
64
sphaira/include/ui/menus/install_stream_menu_base.hpp
Normal 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
|
||||
19
sphaira/include/ui/menus/mtp_menu.hpp
Normal file
19
sphaira/include/ui/menus/mtp_menu.hpp
Normal 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
|
||||
@@ -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{};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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 don’t
|
||||
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{};
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
603
sphaira/source/haze_helper.cpp
Normal file
603
sphaira/source/haze_helper.cpp
Normal 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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
261
sphaira/source/ui/menus/install_stream_menu_base.cpp
Normal file
261
sphaira/source/ui/menus/install_stream_menu_base.cpp
Normal 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
|
||||
@@ -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));
|
||||
|
||||
@@ -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]); \
|
||||
}
|
||||
|
||||
|
||||
89
sphaira/source/ui/menus/mtp_menu.cpp
Normal file
89
sphaira/source/ui/menus/mtp_menu.cpp
Normal 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
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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-----------
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user