/// <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); }
/// <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()); } } }
/// <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); } } }