Installer un NSP sur Nintendo Switch via GoldLeaf

Présentations

Qu'est-ce que la Nintendo Switch ?

La Nintendo Switch est un ordinateur tout intégré. Il est conçu comme une console de jeu, à la fois portable et de salon. Il fait suite à la Nintendo 2DS/3DS et à la Nintendo Wii U (ou Wii 2), respectivement des ordinateurs vendus comme console portable et console de salon. Enfin, faisons remarquer pour l'anecdote qu'il est possible d'y faire tourner Android et des distributions GNU/Linux, donc qu'il est possible d'en avoir un autre usage que console de jeux vidéos.

Qu'est-ce qu'un CFW ?

La Nintendo Switch embarque de base un système d'exploitation. Celui-ci est fait par Nintendo et a pour non de code Horizon. Il est prévu pour ne permettre de faire que ce que Nintendo veut bien y autoriser. Pour sortir du joug de Nintendo, on peut vouloir utiliser une version dérivée de son système mais évidemment sans ses restrictions (à différencier d'un système totalement autre, comme Android ou une distribution GNU/Linux). C'est ce qu'on appelle ici un CFW pour custom firmware. En mai 2024, le plus connu pour Nintendo Switch est Atmosphere. À titre indicatif, car ce n'est pas le sujet du présent article, il existe au moins 3 méthodes pour installer et exécuter un CFW pour Nintendo Switch : RCM (malheureusement seulement pour le premier modèle), faille dans navigateur web (mais faut-il encore que n'ait pas été appliqué un correctif), puce (qui nécessite de bidouiller ou faire bidouiller l'intérieur de la machine).

Qu'est-ce qu'un NSP ?

NSP est un acronyme pour Nintendo Submission Package. Plus concrètement, ça contient soit un jeu, soit une mise à jour de jeu, soit du contenu supplémentaire pour un jeu (dit DLC pour downloadable content). Contrairement au format XCI (NX Cart Image), un fichier NSP ne peut donc contenir qu'une seule chose.

Au-delà du NSP et du XCI, il existe d'autres formats spécifiques à la Nintendo Switch. Par exemple, si vous êtes amené à manipuler du NSP et/ou du XCI, vous tomberez sur le format NCA (Nintendo Content Archive) qui fait conteneur de fichiers, comme un tarball. Mais surtout, si vous avez une Nintendo Switch sous un CFW (comme Atmosphere), vous devriez couramment utiliser des applications non-officielles au format NRO comme GoldLeaf. Et il y en a d'autres encore, mais c'est là les plus courants et ça déborde déjà du sujet du présent article.

Qu'est-ce que GoldLeaf ?

GoldLeaf est une application libre pour dérivés d'Horizon qui permettent d'y exécuter n'importe quoi (comme Atmosphere). Pour utiliser GoldLeaf, ça suppose donc de pouvoir démarrer sa Nintendo Switch sur un CFW comme Atmosphere, ce qui n'est pas l'objet du présent article. Enfin, GoldLeaf a essentiellemnt pour fonction de permettre d'explorer de gérer la mémoire permanente d'un CFW pour Nintendo Switch. Il permet donc notamment d'explorer le contenu de l'éventuelle partition montée de l'éventuelle carte microSD (à condition toutefois que le système de fichiers soit reconnu) et d'en installer les fichiers NSP sur le système d'exploitation.

L'installation de GoldLeaf est très simple. Il faut tout d'abord en télércharger l'exécutable (dont l'extension est nro). Ensuite il faut le transférer sur la carte microSD, voire sur la mémoire interne de la Nintendo Switch en cas de sysNAND (mais c'est fortement déconseillé) et vous avez de toute façon probablement fait une installation en emuNAND (donc sur la carte microSD, afin de ne pas prendre le risque de casser le système officiel sur la mémoire interne de la Nintendo Switch). Enfin, pour l'exécuter, ça dépend de l'environnement de bureau de votre CFW (custom firmware).

Qu'est-ce que Quark ?

Quark est un logiciel écrit en Java (à ne pas confondre avec JavaScript) pour permettre d'avoir accès à l'arborescence de fichiers du système exécutant Quark depuis la Nintendo Switch via GoldLeaf . Il fait suite à GoldTree qu'il a rendu obsolète.

Installer un NSP via la carte microSD

Installer un NSP via un fichier réel

Si la partition de votre carte microSD pour votre CFW (custom firmware) est dans le système de fichiers exFAT (ce qui est déconseillé, car il y a un risque de corruption de données, qui est à priori bien moins probable sur d'autres systèmes d'exploitation), vous pouvez toujours utiliser cette méthode pour installer un NSP depuis la carte microSD, ce qui tombe bien puis c'est la plus simple. En revanche, dans le cas du FAT32 (qui est le système de fichiers conseillé, car le risque de corruption de données est à peu près nul avec lui), vous ne pourrez un installer un NSP depuis la carte microSD que pour les fichiers NSP dont la taille est suffisamment petite pour être gérée par FAT32, et le cas échéant vous pourrez en passer par un fichier virtuel ou par USB.

  1. Démarrez la Nintendo Switch sur un CFW (via RCM ou toute autre méthode)
  2. Démarrez GoldLeaf
  3. Parcourez la mémoire jusqu'au fichier NSP
  4. Cliquez sur le fichier NSP
  5. Acceptez d'installez le contenu du NSP (et ce sur la carte microSD, à moins le cas échéant que vous sachiez ce que vous faites)

Installer un NSP via un fichier virtuel

Si la partition de votre carte microSD pour votre CFW (custom firmware) est dans le système de fichiers exFAT (ce qui est déconseillé, car il y a un risque de corruption de données), la méthode pour fichier virtuel vous est inutile, car la taille maximale pour un fichier avec exFAT ne vous posera jamais problème, donc en ce cas utilisez plutôt la méthode par fichier réel. En revanche, il n'en est pas de même pour FAT32 (qui est toutefois conseillé, car avec lui le risque de corruption de données est à peu près nul). Si vous utilisez FAT32 (et vous devriez, du moins dans le cas d'un CFW pour Nintendo Switch), il va donc falloir en passer par un fichier virtuel quand le fichier NSP est trop gros pour FAT32.

Ce que nous entendons ici par fichier virtuel est un dossier indiqué comme étant une archive et contenant dans le bon ordre plusieurs fichiers assez petits pour FAT32 et qui sont les bouts ordonnés du NSP complet. Comme ça, ça peut paraitre simple. Mais il faut tout de même savoir faire quelques opérations : d'abord découper le NSP complet en plusieurs bouts assez petits et avec des noms qui font qu'ils sont dans le bon ordre (par exemple 00 puis 01 et ainsi de suite), ensuite les mettre seul dans un dossier, puis mettre le dossier sur la carte microSD et indiquer dans le système de fichiers FAT32 qu'il faut considérer le dossier comme une archive, enfin installer le NSP mis sous forme d'un dossier-archive contenant l'ensemble ordonné des bouts et rien d'autre que ça.

Découper le fichier NSP

Découper le fichier NSP avec un script Python

AnalogMan151 / DocKlokMan a fait un petit script pour découper un NSP, dont la seule interface est la ligne de commande et qui n'a donc pas d'interface graphique. Il est écrit en langage Python, donc il fonctionne sur les systèmes d'exploitation génériques classiques (GNU/Linux, *BSD, Windows, macOS). Évidemment, il vous faut donc de quoi exécuter du Python, ce qui peut se faire en ligne de commande avec apt install python sous Debian et ses dérivés (dont Trisquel, qui est totalement libre et donc recommandé, et Ubuntu, qui est problématique, mais l'est beaucoup moins que Windows et macOS).

Découper le fichier NSP avec la commande split

Sous les systèmes POSIX ou à peu près (comme les distributions GNU/Linux et les systèmes *BSD, mais également macOS), il y a normalement la commande textuelle split (et le projet GNU en fournit une implémentation). Elle permet de découper un fichier en plusieurs morceaux. Puisque FAT32 ne gère pas les fichiers de 4Go et plus, on a envie de lui faire faire des fichiers de 3Go, ce qui se fait avec elle via split -b3G fichier.nsp. Mais pour lui faire faire des bouts avec des noms plus classiquement reconnus, on peut la compliquer un peu comme suit : split -b3G --numeric-suffixes fichier.nsp ''.

Déplacer sur la carte microSD

Passer le dossier en archive pour FAT32

Passer le dossier en archive via la Nintendo Switch

Sur la Nintendo Switch sous un dérivé d'Horizon (comme Atmosphere), au moins 2 logiciels permettent d'indiquer au système de fichiers FAT32 qu'un dossier sur la carte microSD doit être considéré comme une archive : GoldLeaf et NX Shell. Pour en exécuter un, il faut d'abord en télécharger un sous forme binaire au format NRO, ensuite le mettre sur la carte microSD, et enfin le lancer, ce qui se fait avec Atmosphere par défaut depuis l'album (pour le mode restreint) ou depuis un jeu (d'une cartouche ou installé) en maintenant le bouton R et en appuyant sur A (pour accorder le maximum de ressources à l'application lancée de la sorte). Une fois le logiciel lancé, il suffit d'aller à travers lui jusqu'au dossier voulu et de lui faire indiquer dans le système de fichiers FAT32 qu'il faut qu'il soit considéré comme une dossier.

Passer le dossier en archive sous GNU/Linux

Sous GNU/Linux (à contrario du privateur Microsoft Windows), il est malheureusement à priori probable que vous ne trouviez pas comment graphiquement passer un dossier comme une archive du point de vue du système de fichiers FAT32 (et il faut donc que ce ne soit abstrait par MTP). Et s'il peut tout simplement que votre interface graphique ne le permette pas, car c'est un besoin pour le moins rare. Heureusement, grâce au projet GNU, il y a le petit utilitaire en ligne de commande mattrib.

Alternativement, il y en a un autre du même genre : fatattr. Comme le nom de ce dernier l'indique en partie, il permet de gérer les attributs d'un noeud d'un système de fichiers FAT32. En l'occurrence, il suffit de faire fatattr +A dossier pour passer le dit dossier en mode archive. Mais avant ça, il faut pouvoir exécuter fatattr et qu'il gère bien l'option (ce qui peut ne pas être le cas par la version fournie par votre distribution).

Pour passer du code source à un exécutable, ce qu'on appelle compilation, ça se fait en l'occurrence très simplement avec scons à condition d'avoir aussi préalablement installé clang, via apt install scons clang sous Debian GNU/Linux et ses dérivés (Trisquel, Ubuntu, Mint, etc.). Mais il se pourrait que ça échoue à cause d'un print sans parenthèses d'un fichier Python d'une veille époque (quand Python 2 régnait et que Python 3 était encore peu utilisé ou n'existait encore tout simplement pas), mais il se corrige très simplement en ajoutant les parenthèses autour du paramètre de la fonction print.

Installer un NSP via USB

Installer un NSP par support USB

  1. Démarrez la Nintendo Switch sur un CFW (via RCM ou toute autre méthode)
  2. Branchez votre support USB à la Nintendo Switch via son port microUSB (qui sert usuellement pour la recharge et l'éventuelle connexion au dock)
  3. Démarrez GoldLeaf
  4. Passez le sélecteur sur Explorer le contenu et appuyer sur le bouton A
  5. Parcourez le support USB jusqu'au fichier NSP voulu
  6. Sélectionner le fichier NSP et appuyez sur le bouton A

Installer un NSP par transfert USB

  1. Installez un environnement d'exécution Java si vous n'en avez pas encore un (nous recommandons OpenJDK et il y a fort probablement un paquet fourni par votre distribution dans le cas où vous utilisez une distribution GNU/Linux ou un système *BSD)
  2. Télécharger Quark si vous ne l'avez pas déjà
  3. Démarrez Quark via Java (avec un système POSIX, comme les GNU/Linux et les *BSD mais aussi le privateur Apple macOS, ça peut se faire dans un terminal via java -jar Quark.jar)
  4. Démarrez la Nintendo Switch sur un CFW (via RCM ou toute autre méthode)
  5. Branchez votre support USB à la Nintendo Switch via son port microUSB (qui sert usuellement pour la recharge et l'éventuelle connexion au dock)
  6. Démarrez GoldLeaf
  7. Passez le sélecteur sur Explorer le contenu et appuyer sur le bouton A
    • Depuis la Nintendo Switch
      1. Parcourez le support USB jusqu'au fichier NSP voulu
      2. Sélectionner le fichier NSP et appuyez sur le bouton A
    • Depuis le PC avec Quark
      1. Dans la modale qui a dû s'ouvrir, allez jusqu'à l'emplacement du fichier
      2. Sélectionnez le fichier et faites comprendre à ce qui gère la modale que c'est ce fichier que vous voulez et alors la modale devrait se fermer

Annexes

Problème à l'installation d'un NSP

Si l'installation d'un NSP ne fonctionne pas, c'est probablement parce qu'il n'est pas signé ou qu'il a été modifié. En ce cas, vous devez avoir confiance en sa source. Si c'est le cas, vous pouvez l'installer après avoir enlevé cette sécurité via les signature patches dits sigpatches.

Si Atmosphere ne les propose pas, c'est parce qu'ils peuvent permettre d'enfreindre la loi de certaines juridictions et faire des choses qu'on peut trouver immorales, dont évidemment l'installation d'un NSP pour un jeu ou un DLC qu'on n'a pas obtenu légalement. Évidemment, nous ne pouvons donc pas vous encourager à les installer et les utiliser à cette fin, mais nous vous informons du risque juridique et moral, car ça nécessite du travail et les sociétés qui le font ont besoin de payer les travailleurs et travailleuses et illes ont évidemment envie de vivre décemment (donc d'avoir le fruit de leur travail collectif, dont une partie est malheureusement dérobée dans le cadre du système capitaliste).

Problème après l'installation d'une mise à jour ou d'un DLC

Il se peut que l'installation du jeu de base se soit bien passée et qu'après elle vous puissiez jouer au jeu. Mais après l'installation d'une mise à jour de ce jeu ou l'ajout d'un contenu supplémentaire (dit DLC) pour ce jeu, le jeu pourrait refuser de se lancer. Cela peut être dur au respect strict de la procédure normale de l'installation d'un NSP, ce que ferait GoldLeaf.

En effet, un NSP peut ne pas demander de ticket. En ce cas, GoldLeaf installe le NSP, mais il ne ferait pas de ticket associé. Le NSP étant sans ticket, le jeu installé à travers lui se lance alors sans problème. Mais le NSP d'une mise à jour ou d'un DLC peut lui aussi être source d'un ticket. Or s'il le jeu de base a été installé sans ticket et qu'une mise à jour ou un DLC pour celui-ci a été installé avec ticket, cela est bizarre, donc le système trouverait qu'il y a une erreur et en conséquence refuserait de lancer le jeu.

Via GoldLeaf, vous pouvez finement désinstaller ce qui a été installé dans le système par NSP ou XCI. Par conséquent, vous pouvez désinstaller la mise à jour d'un jeu ou le DLC d'un jeu sans pour autant désinstaller le jeu de base. Et en cas de mise à jour, vous pouvez réinitialiser la version de lancement. Cela permet de revenir en arrière, mais pas d'installer correctement tout et donc que le jeu fonctionne avec tout.

GoldLeaf ayant apparemment une politique stricte, il ne prendrait pas de précaution, probablement dans une volonté de ne pas faciliter l'illégalisme. Si effectivement GoldLeaf ne produit un ticket que quand c'est demandé par le NSP, la solution à ça est tout simplement d'en créer un systématiquement. C'est par exemple ce que ferait Awoo Installer.

C'est là de l'information et non une incitation à potentiellement contrevenir à la juridiction sous laquelle vous êtes. Les gens qui produisent ce que vous consommez ont besoin de manger, s'habiller, se loger, etc., ce qui passe bien souvent par l'argent dans une société ultra-marchande et il faut bien obtenir cet argent et de préférence sans nuire à autrui ou le moins possible le cas échéant. Nous ne pouvons donc pas trouver moralement acceptable de ne pas rétribuer par principe le travail d'autrui qui nous est utile. Et toute infraction à la loi, qu'elle soit juste ou non, peut vous conduire à être puni·e. Nous espérons donc que ce que nous pouvons vous avoir appris ne vous sert pas à contribuer négativement au monde. Et si vous vous servez de ce que nous pouvons vous avoir appris d'une manière qui est illégale, cela serait un usage illégal de ce que nous vous aurions transmis et vous seriez donc unique responsable d'avoir fait un usage illégal du savoir que nous aurions pu vous transmettre.

Installer un XCI

Un XCI (NX Cart Image) est copie brute (dump) d'une cartouche de jeu Nintendo Switch. Ça ne peut donc servir à représenter qu'un jeu avec une ou des éventuelles mises à jour. Malheureusement, au moins jusqu'en avril 2024 et donc au moins jusqu'à sa version 1.0.0, GoldLeaf ne gère pas le format XCI.

Il vous faut donc manipuler le XCI pour en faire un ou plusieurs NSP. Alternativement vous pouvez utiliser un autre logiciel que GoldLeaf comme TinWoo ou Awoo Installer.

Annexes

Code source C de fatattr

/**
 * Copyright 2013 David Caro Martinez
 * Copyright 2024 Nicola Spanti
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

// https://gitlab.com/Terseus/fatattr/
// cc fatattr.c -o fatattr


#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <getopt.h>
#include <errno.h>
#include <assert.h>

#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/msdos_fs.h>


#define PROGRAM_NAME        "fatattr"

#ifndef DIR_ENTRY_SIZE
#define DIR_ENTRY_SIZE 256
#endif
#ifndef REAL_DIR_ENTRY_SIZE
#define REAL_DIR_ENTRY_SIZE 1025
#endif
#define ERRMSG_MAX 1025

#define DIRENT_SIZE 2


/* Redefinition of attribute macros, so we don't depend on msdos_fs.h macros. */
enum {
    DOSFS_ENOERR = 0,
    DOSFS_EOPEN,
    DOSFS_EIOCTL_GET_ATTRIBUTES,
    DOSFS_EIOCTL_SET_ATTRIBUTES,
    DOSFS_EIOCTL_READDIR_BOTH,
    DOSFS_EBUFFER,
};
/* Attributes checks for lazyness. */
#define DOSFS_HAS_ATTR(x, a)        ((x) & (a))
#define DOSFS_HAS_ATTR_RO(x)        DOSFS_HAS_ATTR(x, ATTR_RO)
#define DOSFS_HAS_ATTR_HIDDEN(x)    DOSFS_HAS_ATTR(x, ATTR_HIDDEN)
#define DOSFS_HAS_ATTR_SYS(x)       DOSFS_HAS_ATTR(x, ATTR_SYS)
#define DOSFS_HAS_ATTR_VOLUME(x)    DOSFS_HAS_ATTR(x, ATTR_VOLUME)
#define DOSFS_HAS_ATTR_DIR(x)       DOSFS_HAS_ATTR(x, ATTR_DIR)
#define DOSFS_HAS_ATTR_ARCH(x)      DOSFS_HAS_ATTR(x, ATTR_ARCH)

typedef enum {
    MADD,
    MREMOVE,
} tDosfsModifyType;

enum {
    MAIN_ENOERR = 0,
    MAIN_EALLOC,
};

enum {
    FLAG_VERBOSE = 0x01,
    FLAG_RECURSIVE = 0x02,
    FLAG_HELP = 0x04,
    FLAG_COPYRIGHT = 0x08,
};

struct programArgs {
	char **fileList;
	size_t fileListSize;
	uint32_t attrsToAdd;
	uint32_t attrsToRemove;
	unsigned int flags;
};

static char errmsg[ERRMSG_MAX] = {0};


/**
 * Modify the attributes of a file descriptor, 'modifyType' specifies if the
 * attributes are added (MADD) or removed (MREMOVE).
 * Returns 0 on success, !0 if an error happens.
 */
static
int
dosfsModifyAttributes
(int fd, uint32_t attrs,
 tDosfsModifyType modifyType)
{
	assert(fd != -1);
	uint32_t currentAttrs = 0;
	int ioctlRet = ioctl(fd, FAT_IOCTL_GET_ATTRIBUTES, &currentAttrs);
	if (ioctlRet < 0) {
		return DOSFS_EIOCTL_GET_ATTRIBUTES;
	}
	uint32_t newAttrs = 0;
	switch (modifyType) {
	case MADD:
		newAttrs = currentAttrs | attrs;
		break;
	case MREMOVE:
		newAttrs = (~attrs) & currentAttrs;
		break;
	}
	ioctlRet = ioctl(fd, FAT_IOCTL_SET_ATTRIBUTES, &newAttrs);
	if (ioctlRet < 0) {
		return DOSFS_EIOCTL_SET_ATTRIBUTES;
	}
	return DOSFS_ENOERR;
}

/**
 * Returns a descriptive message associated with an error code.
 */
static
const char *
dosfsGetError
(const int err)
{
	switch (err) {
	case DOSFS_ENOERR:
		snprintf(errmsg, ERRMSG_MAX,
		         "No error occurred");
		break;
	case DOSFS_EOPEN:
		snprintf(errmsg, ERRMSG_MAX,
		         "Error opening file: %s",
		         strerror(errno));
		break;
	case DOSFS_EIOCTL_GET_ATTRIBUTES:
		snprintf(errmsg, ERRMSG_MAX,
		         "Error in ioctl call 'FAT_IOCTL_GET_ATTRIBUTES': %s",
		         strerror(errno));
		break;
	case DOSFS_EIOCTL_SET_ATTRIBUTES:
		snprintf(errmsg, ERRMSG_MAX,
		         "Error in ioctl call 'FAT_IOCTL_SET_ATTRIBUTES': %s",
		         strerror(errno));
		break;
	case DOSFS_EIOCTL_READDIR_BOTH:
		snprintf(errmsg, ERRMSG_MAX,
		         "Error in ioctl call 'VFAT_IOCTL_READDIR_BOTH': %s",
		         strerror(errno));
		break;
	case DOSFS_EBUFFER:
		snprintf(errmsg, ERRMSG_MAX,
		         "The entry name is bigger than the read buffer");
		break;
	default:
		snprintf(errmsg, ERRMSG_MAX,
		         "Unknown error");
	}
	return errmsg;
}

/**
 * Open a file and save its file descriptor in fd.
 * Returns 0 on success, !0 if an error happens.
 */
static
int
dosfsOpen(const char *file, int *fd)
{
	assert(file != NULL);
	assert(fd != NULL);
	/* O_RDONLY works with files and directories (write doesn't) and let us
	   modify FAT attributes. */
	*fd = open(file, O_RDONLY);
	if (*fd == -1) {
		return DOSFS_EOPEN;
	}
	return DOSFS_ENOERR;
}

/**
 * Close a file descriptor.
 * Returns 0 on success, !0 if an error happens.
 */
static
int
dosfsClose(int fd)
{
	assert(fd != -1);
	close(fd);
	return DOSFS_ENOERR;
}

/**
 * Get the FAT attributes from a file descriptor and save them in 'attrs'.
 * Returns 0 on success, !0 if an error happens.
 */
static
int
dosfsGetAttributes(int fd, uint32_t *attrs)
{
	assert(attrs != NULL);
	int ioctlRet = ioctl(fd, FAT_IOCTL_GET_ATTRIBUTES, attrs);
	if (ioctlRet < 0) {
		return DOSFS_EIOCTL_GET_ATTRIBUTES;
	}
	return DOSFS_ENOERR;
}

/**
 * Add FAT attributes to a file descriptor.
 * Returns 0 on success, !0 if an error happens.
 */
static
int
dosfsAddAttributes(int fd, uint32_t attrs)
{
	return dosfsModifyAttributes(fd, attrs, MADD);
}

/**
 * Remove FAT attributes from a file descriptor.
 * Returns 0 on success, !0 if an error happens.
 */
static
int
dosfsRemoveAttributes(int fd, uint32_t attrs)
{
	return dosfsModifyAttributes(fd, attrs, MREMOVE);
}

/**
 * Read the next entry of the directory associated with a file descriptor.
 * Write the long name entry in 'entry', writing at most 'pathSize' - 1
 * characters.
 * Returns 0 on success, !0 if an error happens or if the complete long name
 * doesn't fit in 'entry'.
 */
static
int
dosfsReadDir
(int fd, char *entry, size_t pathSize)
{
	assert(entry != NULL);
	assert(fd != -1);
	/* VFAT_IOCTL_READDIR_BOTH expects 2 __fat_dirent objects, one for the
	   short name entry an one for the long name entry. */
	struct __fat_dirent dirEnt[DIRENT_SIZE];
	int ioctlRet = ioctl(fd, VFAT_IOCTL_READDIR_BOTH, dirEnt);
	if (ioctlRet < 0) {
		close(fd);
		return DOSFS_EIOCTL_READDIR_BOTH;
	} if (ioctlRet == 0) {
		memset(entry, '\0', pathSize);
		return DOSFS_ENOERR;
	}
	/* If the *real* file name have 8 characters or less, the long name entry
	   will have d_name empty. */
	int dirEntRealName = strlen(dirEnt[1].d_name) == 0 ? 0 : 1;
	if (strlen(dirEnt[dirEntRealName].d_name) >= pathSize) {
		return DOSFS_EBUFFER;
	}
	strncpy(entry, dirEnt[dirEntRealName].d_name, pathSize - 1);
	return DOSFS_ENOERR;
}


/**
 * Appends a file to the file list of 'args'.
 * Returns 0 on success, !0 if an error happens.
 */
static
int
appendFileToList
(struct programArgs *args,
 char *file)
{
	args->fileList = realloc(args->fileList,
	                         sizeof(char *) * (++(args->fileListSize)));
	if (args->fileList == NULL) {
		return MAIN_EALLOC;
	}
	args->fileList[args->fileListSize - 1] = file;
	return MAIN_ENOERR;
}

/**
 * Print FAT attributes in a readable format.
 */
static
void
printAttrs
(const uint32_t attrs)
{
	printf("%c%c%c%c%c%c",
	       DOSFS_HAS_ATTR_RO(attrs) ? 'R' : '-',
	       DOSFS_HAS_ATTR_HIDDEN(attrs) ? 'H' : '-',
	       DOSFS_HAS_ATTR_SYS(attrs) ? 'S' : '-',
	       DOSFS_HAS_ATTR_ARCH(attrs) ? 'A' : '-',
	       DOSFS_HAS_ATTR_DIR(attrs) ? 'D' : '-',
	       DOSFS_HAS_ATTR_VOLUME(attrs) ? 'V' : '-'
	      );
}


/**
 * Internal function, sub of processPrintAttributes.
 * Returns 0 on success, !0 if an error happens.
 */
static
int
processPrintAttributesFd
(const struct programArgs *const args,
 char *file,
 int fd,
 int processDir);


/**
 * Print a file's attributes with the configuration saved in 'args'.
 * If 'processDir' != 0 and 'file' is a directory, process the files inside it.
 * Returns 0 on success, !0 if an error happens.
 */
static
int
processPrintAttributes
(const struct programArgs *const args,
 char *file,
 int processDir);

static
int
processPrintAttributesFd
(const struct programArgs *const args,
 char *file,
 int fd,
 int processDir)
{
	uint32_t fileAttrs = 0;
	int dosfsErrno = dosfsGetAttributes(fd, &fileAttrs);
	if (dosfsErrno) {
		return dosfsErrno;
	}
	printAttrs(fileAttrs);
	printf("  %s\n", file);
	if (DOSFS_HAS_ATTR_DIR(fileAttrs) && processDir) {
		char dirEntry[DIR_ENTRY_SIZE] = {0};
		char realDirEntry[REAL_DIR_ENTRY_SIZE] = {0};
		while (!(dosfsErrno = dosfsReadDir(fd, dirEntry, DIR_ENTRY_SIZE)) &&
		        strlen(dirEntry) > 0) {
			snprintf(realDirEntry, REAL_DIR_ENTRY_SIZE,
			         "%s/%s",
			         file, dirEntry);
			int recursive = (args->flags & FLAG_RECURSIVE) &&
			                strcmp(dirEntry, ".") != 0 &&
			                strcmp(dirEntry, "..") != 0;
			dosfsErrno = processPrintAttributes(args, realDirEntry, recursive);
			if (dosfsErrno) {
				fprintf(stderr, "Error processing file '%s': %s\n",
				        realDirEntry, dosfsGetError(dosfsErrno));
			}
		}
	}
	return MAIN_ENOERR;
}

static
int
processPrintAttributes
(const struct programArgs *const args,
 char *file,
 int processDir)
{
	int fd = 0;
	int dosfsErrno = dosfsOpen(file, &fd);
	if (dosfsErrno) {
		return dosfsErrno;
	}
	dosfsErrno = processPrintAttributesFd(args, file, fd, processDir);
	dosfsClose(fd);
	return dosfsErrno;
}


/**
 * Internal function, sub of processModifyAttributes.
 * Returns 0 on success, !0 if an error happens.
 */
static
int
processModifyAttributesFd
(const struct programArgs *const args,
 char *file,
 int fd,
 int processDir);

/**
 * Modify the attributes of a file based on the configuration saved in 'args'.
 * If 'processDir' != 0 and 'file' is a directory, process the files inside it.
 * Returns 0 on success, !0 if an error happens.
 */
int
processModifyAttributes
(const struct programArgs *const args,
 char *file,
 int processDir);

static
int
processModifyAttributesFd
(const struct programArgs *const args,
 char *file,
 int fd,
 int processDir)
{
	uint32_t fileAttrs = 0;
	int dosfsErrno = dosfsGetAttributes(fd, &fileAttrs);
	if (dosfsErrno) {
		return dosfsErrno;
	}
	if (args->attrsToAdd != 0 &&
	        (args->attrsToAdd & fileAttrs) != args->attrsToAdd) {
		dosfsErrno = dosfsAddAttributes(fd, args->attrsToAdd);
		if (dosfsErrno) {
			return dosfsErrno;
		}
	}
	if (args->attrsToRemove != 0 &&
	        (args->attrsToRemove & fileAttrs) != 0) {
		dosfsErrno = dosfsRemoveAttributes(fd, args->attrsToRemove);
		if (dosfsErrno) {
			return dosfsErrno;
		}
	}
	uint32_t newAttrs = 0;
	dosfsErrno = dosfsGetAttributes(fd, &newAttrs);
	if (dosfsErrno) {
		return dosfsErrno;
	}
	if (args->flags & FLAG_VERBOSE) {
		printAttrs(fileAttrs);
		printf(" => ");
		printAttrs(newAttrs);
		printf("  %s\n", file);
	}
	if (DOSFS_HAS_ATTR_DIR(newAttrs) && processDir) {
		char dirEntry[DIR_ENTRY_SIZE] = {0};
		char realDirEntry[REAL_DIR_ENTRY_SIZE] = {0};
		while (!(dosfsErrno = dosfsReadDir(fd, dirEntry, DIR_ENTRY_SIZE)) &&
		        strlen(dirEntry) > 0) {
			snprintf(realDirEntry, REAL_DIR_ENTRY_SIZE,
			         "%s/%s",
			         file, dirEntry);
			int recursive = (args->flags & FLAG_RECURSIVE) &&
			                strcmp(dirEntry, ".") != 0 &&
			                strcmp(dirEntry, "..") != 0;
			dosfsErrno = processModifyAttributes(args, realDirEntry, recursive);
			if (dosfsErrno) {
				fprintf(stderr, "Error processing file '%s': %s\n",
				        realDirEntry, dosfsGetError(dosfsErrno));
			}
		}
	}
	return MAIN_ENOERR;
}

int
processModifyAttributes
(const struct programArgs *const args,
 char *file,
 int processDir)
{
	int fd = 0;
	int dosfsErrno = dosfsOpen(file, &fd);
	if (dosfsErrno) {
		return dosfsErrno;
	}
	dosfsErrno = processModifyAttributesFd(args, file, fd, processDir);
	dosfsClose(fd);
	return dosfsErrno;
}


/**
 * Prints the program name and copyright/license notices.
 */
static
void
showCopyright
(void)
{
	puts("FAT attributes utility\n"
	     "https://gitlab.com/Terseus/fatattr/"
	     "Copyright 2013 David Caro Martinez\n"
	     "Copyright 2024 Nicola Spanti\n"
	     "\n"
	     "This software comes with ABSOLUTELY NO WARRANTY.\n"
	     "This is free software, and you are welcome to redistribute it \n"
	     "under certain conditions. See the GNU General Public License \n"
	     "for details."
	     );
}

/**
 * Prints the same as showCopyright along with the program help.
 */
static
void
showHelp
(void)
{
	showCopyright();
	printf("Usage: %s [options] FILE ...\n", PROGRAM_NAME);
	puts("Accepted options:\n"
	     "\t+R: Sets the read-only attribute.\n"
	     "\t-R: Remove the read-only attribute.\n"
	     "\t+A: Sets the archive attribute.\n"
	     "\t-A: Remove the archive attribute.\n"
	     "\t+S: Sets the system attribute.\n"
	     "\t-S: Remove the system attribute.\n"
	     "\t+H: Sets the hidden attribute.\n"
	     "\t-H: Remove the system attribute.\n"
	     "\t+D: Sets the directory attribute (warning! see below).\n"
	     "\t-D: Remove the directory attribute (warning! see below).\n"
	     "\t+V: Sets the volume label attribute (warning! see below).\n"
	     "\t-V: Remove the volume label attribute (warning! see below).\n"
	     "\t--recursive: If FILE is a directory, process it recursively.\n"
	     "\t--verbose: Verbose attribute changes.\n"
	     "\t--help: Show this help.\n"
	     "\t--copyright: Show only the program name and credits.\n"
	     "\t--: Forces all arguments past this one to be interpreted as "
	     "files.\n"
	     "If no attribute change is specified, the program prints the "
	     "file's attributes.\n"
	     "Do NOT use the +D, -D, +V and -V options if you don't know "
	     "EXACTLY what you are doing."
	     );
}


static
void
processAttributesArg
(uint32_t *attrs,
 const char *arg)
{
	assert(attrs != NULL);
	assert(arg != NULL);

	const size_t argLen = strlen(arg);
	for (size_t i = 0; i < argLen; i++) {
		switch (arg[i]) {
		case 'R':
			*attrs |= ATTR_RO;
			break;
		case 'A':
			*attrs |= ATTR_ARCH;
			break;
		case 'S':
			*attrs |= ATTR_SYS;
			break;
		case 'H':
			*attrs |= ATTR_HIDDEN;
			break;
		case 'D':
			*attrs |= ATTR_DIR;
			break;
		case 'V':
			*attrs |= ATTR_VOLUME;
			break;
		default:
			fprintf(stderr, "Invalid attribute '%c' in '%s'\n",
			        arg[i], arg);
			exit(EXIT_FAILURE);
		}
	}
}

/**
 * Process the program's arguments and saved the readed values in 'result'.
 * Returns 0 on success, !0 if an error happens.
 */
static
int
processArgs
(const int argc,
 char **argv,
 struct programArgs *result)
{
	result->fileList = NULL;
	result->fileListSize = 0;
	result->attrsToAdd = 0;
	result->attrsToRemove = 0;
	result->flags = 0;
	bool skipArgs = false;
	int mainErrno = 0;
	for (int i = 1; i < argc; i++) {
		if (skipArgs || (argv[i][0] != '-' && argv[i][0] != '+')) {
			mainErrno = appendFileToList(result, argv[i]);
			if (mainErrno) {
				return mainErrno;
			}
		} else if (argv[i][0] == '-') {
			if (argv[i][1] == '\0') {
				fprintf(stderr, "Missing attribute in argument '%s'\n", argv[i]);
				exit(EXIT_FAILURE);
			} else if (argv[i][1] == '-') {
				if (argv[i][2] == '\0') {
					skipArgs = true;
					continue;
				} else if (strcmp(argv[i], "--recursive") == 0) {
					result->flags |= FLAG_RECURSIVE;
					continue;
				} else if (strcmp(argv[i], "--verbose") == 0) {
					result->flags |= FLAG_VERBOSE;
					continue;
				} else if (strcmp(argv[i], "--help") == 0) {
					result->flags |= FLAG_HELP;
					continue;
				} else if (strcmp(argv[i], "--copyright") == 0) {
					result->flags |= FLAG_COPYRIGHT;
					continue;
				} else {
					fprintf(stderr, "Invalid option '%s'\n", argv[i]);
					exit(EXIT_FAILURE);
				}
			} else {
				processAttributesArg(&result->attrsToRemove,
				                     argv[i] + 1);
			}
		} else if (argv[i][0] == '+') {
			if (argv[i][1] == '\0') {
				fprintf(stderr, "Missing attribute in argument '%s'\n", argv[i]);
				exit(EXIT_FAILURE);
			}
			processAttributesArg(&result->attrsToAdd,
				             argv[i] + 1);
		}
	}
	return MAIN_ENOERR;
}

/**
 * Returns a descriptive message associated with an error code.
 * This is only used for the errors that happens purely in this source file,
 * like a memory alloc error.
 */
static
const char *
mainGetError
(const int err)
{
	switch (err) {
	case MAIN_ENOERR:
		snprintf(errmsg, ERRMSG_MAX,
		         "No error occurred");
		break;
	case MAIN_EALLOC:
		snprintf(errmsg, ERRMSG_MAX,
		         "Error allocating memory: %s",
		         strerror(errno));
		break;
	default:
		snprintf(errmsg, ERRMSG_MAX,
		         "Unknown error");
	}
	return errmsg;
}

int
main
(int argc,
 char **argv)
{
	if (argc < 2) {
		showHelp();
		exit(EXIT_SUCCESS);
	}
	
	struct programArgs args;
	int mainErrno = processArgs(argc, argv, &args);
	if (mainErrno) {
		fprintf(stderr, "Error processing arguments: %s\n",
		        mainGetError(mainErrno));
		exit(EXIT_FAILURE);
	}
	if (args.flags & FLAG_HELP) {
		showHelp();
		exit(EXIT_SUCCESS);
	}
	if (args.flags & FLAG_COPYRIGHT) {
		showCopyright();
		exit(0);
	}
	if (args.fileListSize == 0) {
		fputs("Error processing arguments: No file(s) specified\n",
		      stderr);
		showHelp();
		exit(EXIT_FAILURE);
	}
	if ((args.attrsToAdd & args.attrsToRemove) != 0) {
		fputs("Error processing arguments: Overlapping attribute changes\n",
		      stderr);
		exit(EXIT_FAILURE);
	}
	
	int dosfsErrno = 0;
	if (args.attrsToRemove == 0 && args.attrsToAdd == 0) {
		for (size_t i = 0; i < args.fileListSize; i++) {
			dosfsErrno = processPrintAttributes(&args, args.fileList[i], true);
			if (dosfsErrno) {
				fprintf(stderr, "Error processing file '%s': %s\n",
				        args.fileList[i], dosfsGetError(dosfsErrno));
			}
		}
	} else {
		for (size_t i = 0; i < args.fileListSize; i++) {
			dosfsErrno = processModifyAttributes(&args, args.fileList[i],
			                                     args.flags & FLAG_RECURSIVE);
			if (dosfsErrno) {
				fprintf(stderr, "Error processing file '%s': %s\n",
				        args.fileList[i], dosfsGetError(dosfsErrno));
			}
		}
	}
	free(args.fileList);
	exit(dosfsErrno);
}

Code source Python de splitNSP

#!/usr/bin/env python3
# License: https://unlicense.org/
# Source: https://github.com/AnalogMan151/splitNSP
# Author: AnalogMan
# Modified Date: 2018-10-08
# Purpose: Splits Nintendo Switch NSP files into parts for installation on FAT32

import os
import argparse
import shutil

splitSize = 0xFFFF0000 # 4,294,901,760 bytes
chunkSize = 0x8000 # 32,768 bytes

def splitQuick(filepath):
    fileSize = os.path.getsize(filepath)
    info = shutil.disk_usage(os.path.dirname(os.path.abspath(filepath)))
    if info.free < splitSize:
        print('Not enough temporary space. Needs 4GiB of free space\n')
        return
    print('Calculating number of splits…\n')
    splitNum = int(fileSize / splitSize)
    if splitNum == 0:
        print('This NSP is under 4GiB and does not need to be split.\n')
        return

    print('Splitting NSP into {0} parts…\n'.format(splitNum + 1))

    # Create directory, delete if already exists
    dir = filepath[:-4] + '_split.nsp'
    if os.path.exists(dir):
        shutil.rmtree(dir)
    os.makedirs(dir)

    # Move input file to directory and rename it to first part
    filename = os.path.basename(filepath)
    shutil.move(filepath, os.path.join(dir, '00'))
    filepath = os.path.join(dir, '00')

    # Calculate size of final part to copy first
    finalSplitSize = fileSize - (splitSize * splitNum)

    # Copy final part and trim from main file
    with open(filepath, 'r+b') as nspFile:
        nspFile.seek(finalSplitSize * -1, os.SEEK_END)
        outFile = os.path.join(dir, '{:02}'.format(splitNum))
        partSize = 0
        print('Starting part {:02}'.format(splitNum))
        with open(outFile, 'wb') as splitFile:
            while partSize < finalSplitSize:
                splitFile.write(nspFile.read(chunkSize))
                partSize += chunkSize
        nspFile.seek(finalSplitSize * -1, os.SEEK_END)
        nspFile.truncate()
        print('Part {:02} complete'.format(splitNum))

    # Loop through additional parts and trim
    with open(filepath, 'r+b') as nspFile:
        for i in range(splitNum - 1):
            nspFile.seek(splitSize * -1, os.SEEK_END)
            outFile = os.path.join(dir, '{:02}'.format(splitNum - (i + 1)))
            partSize = 0
            print('Starting part {:02}'.format(splitNum - (i + 1)))
            with open(outFile, 'wb') as splitFile:
                 while partSize < splitSize:
                    splitFile.write(nspFile.read(chunkSize))
                    partSize += chunkSize
            nspFile.seek(splitSize * -1, os.SEEK_END)
            nspFile.truncate()
            print('Part {:02} complete'.format(splitNum - (i + 1)))

    # Print assurance statement for user
    print('Starting part 00\nPart 00 complete')

    print('\nNSP successfully split!\n')

def splitCopy(filepath, output_dir=""):
    fileSize = os.path.getsize(filepath)
    info = shutil.disk_usage(os.path.dirname(os.path.abspath(filepath)))
    if info.free < fileSize*2:
        print('Not enough free space to run. Will require twice the space as the NSP file\n')
        return
    print('Calculating number of splits…\n')
    splitNum = int(fileSize/splitSize)
    if splitNum == 0:
        print('This NSP is under 4GiB and does not need to be split.\n')
        return

    print('Splitting NSP into {0} parts…\n'.format(splitNum + 1))

    # Create directory, delete if already exists
    if output_dir == "":
        dir = filepath[:-4] + '_split.nsp'
    else:
        if output_dir[-4:] != '.nsp':
            output_dir+= ".nsp"
        dir = output_dir
    if os.path.exists(dir):
        shutil.rmtree(dir)
    os.makedirs(dir)

    remainingSize = fileSize

    # Open source file and begin writing to output files stoping at splitSize
    with open(filepath, 'rb') as nspFile:
        for i in range(splitNum + 1):
            partSize = 0
            print('Starting part {:02}'.format(i))
            outFile = os.path.join(dir, '{:02}'.format(i))
            with open(outFile, 'wb') as splitFile:
                if remainingSize > splitSize:
                    while partSize < splitSize:
                        splitFile.write(nspFile.read(chunkSize))
                        partSize += chunkSize
                    remainingSize -= splitSize
                else:
                    while partSize < remainingSize:
                        splitFile.write(nspFile.read(chunkSize))
                        partSize += chunkSize
            print('Part {:02} complete'.format(i))
    print('\nNSP successfully split!\n')

def main():
    print('\n========== NSP Splitter ==========\n')

    # Arg parser for program options
    parser = argparse.ArgumentParser(description='Split NSP files into FAT32 compatible sizes')
    parser.add_argument('filepath', help='Path to NSP file')
    group = parser.add_mutually_exclusive_group()
    group.add_argument('-q', '--quick', action='store_true',
                       help=(
                           'Splits file in-place without creating a copy. '
                           'Only requires 4GiB free space to run'))
    group.add_argument('-o', '--output-dir', type=str, default="",
                       help="Set alternative output dir")

    # Check passed arguments
    args = parser.parse_args()

    filepath = args.filepath

    # Check if required files exist
    if not os.path.isfile(filepath):
        print('NSP cannot be found!\n')
        return 1

    # Split NSP file
    if args.quick:
        splitQuick(filepath)
    else:
        splitCopy(filepath, args.output_dir)

if __name__ == "__main__":
    main()