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