Esempio n. 1
0
        public static NSP FromDirectory(string path)
        {
            DirectoryInfo directory = new DirectoryInfo(path);

            if (directory.Exists)
            {
                NSP nsp = new NSP(path);
                nsp.CnmtXML = directory.EnumerateFiles("*.cnmt.xml").SingleOrDefault().FullName;
                if (nsp.CnmtXML == null)
                {
                    return(null);
                }

                CNMT cnmt     = nsp.CNMT = CNMT.FromXml(nsp.CnmtXML);
                var  cnmtNcas = cnmt.ParseContent();
                foreach (var e in cnmtNcas)
                {
                    string ncaid = e.Key;
                    var    entry = e.Value;
                    nsp.AddNCAByID(entry.Type, ncaid);
                }

                foreach (var jpeg in directory.EnumerateFiles("*.jpg"))
                {
                    nsp.AddImage(jpeg.FullName);
                }

                nsp.ControlXML     = directory.EnumerateFiles("*.nacp.xml")?.SingleOrDefault()?.FullName;
                nsp.LegalinfoXML   = directory.EnumerateFiles("*.legalinfo.xml")?.SingleOrDefault()?.FullName;
                nsp.PrograminfoXML = directory.EnumerateFiles("*.programinfo.xml")?.SingleOrDefault()?.FullName;
                nsp.Certificate    = directory.EnumerateFiles("*.cert").SingleOrDefault()?.FullName;
                nsp.Ticket         = directory.EnumerateFiles("*.tik")?.SingleOrDefault()?.FullName;
                return(nsp);
            }

            return(null);
        }
Esempio n. 2
0
        /// <summary>
        /// </summary>
        /// <param name="nspPath">Path to the NSP file you want to extract.</param>
        /// <param name="outputDirectory">Path to a directory where the extracted file will be placed.
        /// If null, the NSP will be extracted to a new directory named after the NSP file and with the same parent as the NSP file. </param>
        /// <param name="specificFile">A specific file within the NSP to extract, or null to extract all files.</param>
        public static async Task <NSP> ParseNSP(string nspPath, string outputDirectory = null, string specificFile = null)
        {
            if (string.IsNullOrWhiteSpace(nspPath))
            {
                logger.Error("Empty path passed to NSP.Unpack.");
                return(null);
            }

            FileInfo finfo = new FileInfo(nspPath);

            if (!finfo.Exists)
            {
                logger.Error($"Non-existent file passed to NSP.Unpack: {nspPath}");
                return(null);
            }

            using (JobFileStream nspReadStream = new JobFileStream(nspPath, "NSP unpack of " + nspPath, finfo.Length, 0))
            {
                using (BinaryReader br = new BinaryReader(nspReadStream))
                {
                    if (br.ReadChar() != 'P')
                    {
                        throw new InvalidNspException("Wrong header");
                    }
                    if (br.ReadChar() != 'F')
                    {
                        throw new InvalidNspException("Wrong header");
                    }
                    if (br.ReadChar() != 'S')
                    {
                        throw new InvalidNspException("Wrong header");
                    }
                    if (br.ReadChar() != '0')
                    {
                        throw new InvalidNspException("Wrong header");
                    }

                    // 0x4 + 0x4 number of files
                    int numFiles = br.ReadInt32();
                    if (numFiles < 1)
                    {
                        throw new InvalidNspException("No files inside NSP");
                    }

                    // 0x8 + 0x4  size of string table (plus remainder so it reaches a multple of 0x10)
                    int stringTableSize = br.ReadInt32();
                    if (stringTableSize < 1)
                    {
                        throw new InvalidNspException("Invalid or zero string table size");
                    }

                    // 0xC + 0x4 Zero/Reserved
                    br.ReadUInt32();

                    long[] fileOffsets        = new long[numFiles];
                    long[] fileSizes          = new long[numFiles];
                    int[]  stringTableOffsets = new int[numFiles];

                    // 0x10 + 0x18 * nFiles File Entry Table
                    // One File Entry for each file
                    for (int i = 0; i < numFiles; i++)
                    {
                        // 0x0 + 0x8 Offset of this file from start of file data block
                        fileOffsets[i] = br.ReadInt64();

                        // 0x8 + 0x8 Size of this specific file within the file data block
                        fileSizes[i] = br.ReadInt64();

                        // 0x10 + 0x4 Offset of this file's filename within the string table
                        stringTableOffsets[i] = br.ReadInt32();

                        // 0x14 + 0x4 Zero?
                        br.ReadInt32();
                    }

                    // (0x10 + X) + Y string table, where X is file table size and Y is string table size
                    // Encode every string in UTF8, then terminate with a 0
                    byte[] strBytes = br.ReadBytes(stringTableSize);
                    var    files    = new string[numFiles];
                    for (int i = 0; i < numFiles; i++)
                    {
                        // Start of the string is in the string table offsets table
                        int thisOffset = stringTableOffsets[i];

                        // Decode UTF8 string and assign to files array
                        string name = strBytes.DecodeUTF8NullTerminated(thisOffset);
                        //string name = Encoding.UTF8.GetString(strBytes, thisOffset, thisLength);
                        files[i] = name;
                    }

                    // The header is always aligned to a multiple of 0x10 bytes
                    // It is padded with 0s until the header size is a multiple of 0x10.
                    // However, these 0s are INCLUDED as part of the string table. Thus, they've already been
                    // read (and skipped)

                    if (outputDirectory == null)
                    {
                        // Create a directory right next to the NSP, using the NSP's file name (no extension)
                        DirectoryInfo parentDir = finfo.Directory;
                        DirectoryInfo nspDir    = parentDir.CreateSubdirectory(Path.GetFileNameWithoutExtension(finfo.Name));
                        outputDirectory = nspDir.FullName;
                    }

                    NSP           nsp  = new NSP(outputDirectory);
                    List <string> ncas = new List <string>();
                    List <string> nczs = new List <string>();

                    long dataPosition = nspReadStream.Position;

                    // Copy each file in the NSP to a new file.
                    for (int i = 0; i < files.Length; i++)
                    {
                        string currentFile = files[i];

                        // If specificFile is null, extract all, otherwise only extract matching filename
                        if (specificFile == null || currentFile.ToLower().Contains(specificFile.ToLower()))
                        {
                            // NSPs are just groups of files, but switch titles have very specific files in them
                            // So we allow quick reference to these files
                            string filePath = FileUtils.BuildPath(outputDirectory, currentFile);
                            if (filePath.ToLower().EndsWith(".cnmt.xml"))
                            {
                                nsp.CnmtXML = filePath;
                            }
                            else if (filePath.ToLower().EndsWith(".programinfo.xml"))
                            {
                                nsp.PrograminfoXML = filePath;
                            }
                            else if (filePath.ToLower().EndsWith(".legalinfo.xml"))
                            {
                                nsp.LegalinfoXML = filePath;
                            }
                            else if (filePath.ToLower().EndsWith(".nacp.xml"))
                            {
                                nsp.ControlXML = filePath;
                            }
                            else if (filePath.ToLower().EndsWith(".cert"))
                            {
                                nsp.Certificate = filePath;
                            }
                            else if (filePath.ToLower().EndsWith(".tik"))
                            {
                                nsp.Ticket = filePath;
                            }
                            else if (filePath.ToLower().StartsWith("icon_") || filePath.ToLower().EndsWith(".jpg"))
                            {
                                nsp.AddImage(filePath);
                            }
                            else if (filePath.ToLower().EndsWith(".nca"))
                            {
                                if (filePath.ToLower().EndsWith(".cnmt.nca"))
                                {
                                    nsp.CnmtNCA = filePath;
                                }
                                ncas.Add(filePath);
                            }
                            else if (filePath.ToLower().EndsWith(".ncz"))
                            {
                                nczs.Add(filePath);
                            }
                            else
                            {
                                logger.Warn($"Unknown file type found in NSP, {filePath}");
                                nsp.AddFile(filePath);
                            }

                            using (FileStream fs = FileUtils.OpenWriteStream(filePath))
                            {
                                logger.Info($"Unpacking NSP from file {nspPath}.");

                                long fileOffset = fileOffsets[i];
                                long fileSize   = fileSizes[i];
                                nspReadStream.Seek(dataPosition + fileOffset, SeekOrigin.Begin);
                                await nspReadStream.CopyToAsync(fs, fileSize).ConfigureAwait(false);

                                logger.Info($"Copied NSP contents to file {filePath}");
                            }
                        }
                    }
                    CNMT cnmt = nsp.CNMT = await nsp.ReadCNMT().ConfigureAwait(false);

                    var cnmtNcas = cnmt.ParseContent();
                    foreach (var ncafile in ncas)
                    {
                        // Intentionally two calls, .cnmt.nca is two extensions
                        string ncaid = Path.GetFileNameWithoutExtension(Path.GetFileNameWithoutExtension(ncafile));
                        var    entry = cnmtNcas[ncaid];
                        nsp.AddNCAByID(entry.Type, ncaid);
                    }

                    // Ugh, handle compressed NCZ files by decrypting them and deleting the original
                    foreach (var nczfile in nczs)
                    {
                        string nczFullPath = FileUtils.BuildPath(outputDirectory, nczfile);
                        string ncaid       = Path.GetFileNameWithoutExtension(nczfile);
                        string ncafile     = await Compression.UnpackNCZ(nczFullPath, outputDirectory).ConfigureAwait(false);

                        if (!string.IsNullOrWhiteSpace(ncafile))
                        {
                            FileUtils.DeleteFile(nczFullPath);
                            var entry = cnmtNcas[ncaid];
                            nsp.AddNCAByID(entry.Type, ncaid);
                        }
                    }
                    return(nsp);
                }
            }
        }