Beispiel #1
0
        /// <summary>
        /// Repacks this NSP from a set of files into a single file and writes the file out to the given path.
        /// </summary>
        /// <param name="path"></param>
        public async Task <bool> Repack(string path)
        {
            logger.Info($"Repacking to NSP file {path}.");

            string[] files  = this.Files.ToArray();
            int      nFiles = files.Length;


            var hd = GenerateHeader(files);

            // Use lambda to sum sizes of all files in files array
            long totalSize = this.FilesSize + hd.Length;

            FileInfo finfo = new FileInfo(path);

            if (finfo.Exists && finfo.Length == totalSize)
            {
                logger.Warn($"NSP already exists {path}. Delete and try again.");
                return(true);
            }

            using (JobFileStream str = new JobFileStream(path, "NSP repack of " + Title.ToString(), totalSize, 0))
            {
                await str.WriteAsync(hd, 0, hd.Length).ConfigureAwait(false);

                // Copy each file to the end of the NSP in sequence. Nothing special here just copy them all.
                foreach (var file in files)
                {
                    using (FileStream fs = File.OpenRead(file))
                        await str.CopyFromAsync(fs).ConfigureAwait(false);
                }
            }

            finfo.Refresh();
            if (finfo.Exists && finfo.Length == totalSize)
            {
                logger.Info($"Successfully repacked NSP to {path}");
                return(true);
            }

            logger.Error($"Failed to repack NSP to {path}");
            return(false);
        }
Beispiel #2
0
        /// <summary>
        /// </summary>
        /// <param name="path"></param>
        public static NSO ParseNSO(string path)
        {
            if (string.IsNullOrWhiteSpace(path))
            {
                logger.Error("Empty path passed to ParseNSO.");
                return(null);
            }

            FileInfo finfo = new FileInfo(path);

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

            using (JobFileStream nspReadStream = new JobFileStream(path, "Parsing NSO at " + path, finfo.Length, 0))
            {
                using (BinaryReader br = new BinaryReader(nspReadStream))
                {
                    if (br.ReadChar() != 'N')
                    {
                        throw new InvalidNspException("Wrong header");
                    }
                    if (br.ReadChar() != 'S')
                    {
                        throw new InvalidNspException("Wrong header");
                    }
                    if (br.ReadChar() != 'O')
                    {
                        throw new InvalidNspException("Wrong header");
                    }
                    if (br.ReadChar() != '0')
                    {
                        throw new InvalidNspException("Wrong header");
                    }

                    // 0x4 + 0x4 version (0?)
                    int version = br.ReadInt32();

                    // 0x8 + 0x4  reserved/unused
                    int reserved = br.ReadInt32();

                    // 0xC + 0x4 Flags, bit 0-2: (.text, .rodata and .data) section is compressed, bit 3-5: check section hash when loading
                    uint flags = br.ReadUInt32();

                    // 0x10 + 0xC .text SegmentHeader
                    uint textFileOffset       = br.ReadUInt32();
                    uint textMemoryOffset     = br.ReadUInt32();
                    uint textDecompressedSize = br.ReadUInt32();

                    // 0x1C + 0x4 Module offset (calculated by sizeof(header))
                    int moduleOffset = br.ReadInt32();

                    // 0x20 + 0xC .rodata SegmentHeader
                    uint rodataFileOffset       = br.ReadUInt32();
                    uint rodataMemoryOffset     = br.ReadUInt32();
                    uint rodataDecompressedSize = br.ReadUInt32();

                    // 0x2C + 0x4 Module file size
                    uint moduleFileSize = br.ReadUInt32();

                    // 0x30 + 0xC .data SegmentHeader
                    uint dataFileOffset       = br.ReadUInt32();
                    uint dataMemoryOffset     = br.ReadUInt32();
                    uint dataDecompressedSize = br.ReadUInt32();

                    // 0x3C + 0x4 bssSize
                    uint bssSize = br.ReadUInt32();

                    // 0x40 + 0x20 Value of "build id" from ELF's GNU .note section. Contains variable sized digest, up to 32bytes.
                    byte[] buildId = br.ReadBytes(0x20);

                    // 0x60 + 0x4   .text compressed size
                    uint textCompressedSize = br.ReadUInt32();

                    // 0x64 + 0x4   .rodata compressed size
                    uint rodataCompressedSize = br.ReadUInt32();

                    // 0x68 + 0x4   .data compressed size
                    uint dataCompressedSize = br.ReadUInt32();

                    // 0x6C + 0x1C Reserved (Padding)
                    br.ReadBytes(0x1C);

                    // 0x88 + 0x8 .rodata - relative extents of .api_info
                    uint apiInfoRegionRoDataOffset = br.ReadUInt32();
                    uint apiInfoRegionSize         = br.ReadUInt32();

                    // 0x90 + 0x8 .rodata - relative extents of .dynstr
                    uint dynStrRegionRoDataOffset = br.ReadUInt32();
                    uint dynStrRegionSize         = br.ReadUInt32();

                    // 0x98 + 0x8 .rodata - relative extents of .dynsym
                    uint dynSymRegionRoDataOffset = br.ReadUInt32();
                    uint dynSymRegionSize         = br.ReadUInt32();

                    // 0xA0 + 0x20 * 3 SHA256 hashes over the decompressed sections using the above byte-sizes: .text, .rodata, and .data.
                    byte[] textHash   = br.ReadBytes(0x20);
                    byte[] rodataHash = br.ReadBytes(0x20);
                    byte[] dataHash   = br.ReadBytes(0x20);

                    // 0x100 compressed sections

                    return(new NSO());
                }
            }
        }
Beispiel #3
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);
                }
            }
        }