diff --git a/hactoolnet/CliParser.cs b/hactoolnet/CliParser.cs index 99540ccc..f9adabe8 100644 --- a/hactoolnet/CliParser.cs +++ b/hactoolnet/CliParser.cs @@ -1,5 +1,7 @@ using System; +using System.Globalization; using System.Linq; +using System.Text; namespace hactoolnet { @@ -26,6 +28,10 @@ namespace hactoolnet new CliOption("outdir", 1, (o, a) => o.OutDir = a[0]), new CliOption("sdseed", 1, (o, a) => o.SdSeed = a[0]), new CliOption("sdpath", 1, (o, a) => o.SdPath = a[0]), + new CliOption("listapps", 0, (o, a) => o.ListApps = true), + new CliOption("listtitles", 0, (o, a) => o.ListTitles = true), + new CliOption("listromfs", 0, (o, a) => o.ListRomFs = true), + new CliOption("title", 1, (o, a) => o.TitleId = ParseTitleId(a[0])), }; public static Options Parse(string[] args) @@ -49,7 +55,7 @@ namespace hactoolnet { if (inputSpecified) { - Console.WriteLine($"Unable to parse option {args[i]}"); + PrintWithUsage($"Unable to parse option {args[i]}"); return null; } @@ -61,13 +67,13 @@ namespace hactoolnet var option = CliOptions.FirstOrDefault(x => x.Long == arg || x.Short == arg); if (option == null) { - Console.WriteLine($"Unknown option {args[i]}"); + PrintWithUsage($"Unknown option {args[i]}"); return null; } if (i + option.ArgsNeeded >= args.Length) { - Console.WriteLine($"Need {option.ArgsNeeded} parameter{(option.ArgsNeeded == 1 ? "" : "s")} after {args[i]}"); + PrintWithUsage($"Need {option.ArgsNeeded} parameter{(option.ArgsNeeded == 1 ? "" : "s")} after {args[i]}"); return null; } @@ -80,7 +86,7 @@ namespace hactoolnet if (!inputSpecified) { - Console.WriteLine("Input file must be specified"); + PrintWithUsage("Input file must be specified"); return null; } @@ -97,12 +103,58 @@ namespace hactoolnet return type; } + private static ulong ParseTitleId(string input) + { + if (input.Length != 16) + { + PrintWithUsage("Title ID must be 16 hex characters long"); + } + + if (!ulong.TryParse(input, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var id)) + { + PrintWithUsage("Could not parse title ID"); + } + + return id; + } + private static void PrintWithUsage(string toPrint) { Console.WriteLine(toPrint); + Console.WriteLine(GetUsage()); // PrintUsage(); } + private static string GetUsage() + { + var sb = new StringBuilder(); + + sb.AppendLine("Usage: hactoolnet.exe [options...] "); + sb.AppendLine("Options:"); + sb.AppendLine(" -r, --raw Keep raw data, don\'t unpack."); + sb.AppendLine(" -k, --keyset Load keys from an external file."); + sb.AppendLine(" -t, --intype=type Specify input file type [nca, switchfs]"); + sb.AppendLine(" --titlekeys Load title keys from an external file."); + sb.AppendLine("NCA options:"); + sb.AppendLine(" --section0 Specify Section 0 file path."); + sb.AppendLine(" --section1 Specify Section 1 file path."); + sb.AppendLine(" --section2 Specify Section 2 file path."); + sb.AppendLine(" --section3 Specify Section 3 file path."); + sb.AppendLine(" --section0dir Specify Section 0 directory path."); + sb.AppendLine(" --section1dir Specify Section 1 directory path."); + sb.AppendLine(" --section2dir Specify Section 2 directory path."); + sb.AppendLine(" --section3dir Specify Section 3 directory path."); + sb.AppendLine(" --listromfs List files in RomFS."); + sb.AppendLine("Switch FS options:"); + sb.AppendLine(" --sdseed Set console unique seed for SD card NAX0 encryption."); + sb.AppendLine(" --listapps List application info."); + sb.AppendLine(" --listtitles List title info for all titles."); + sb.AppendLine(" --title Specify title ID to use."); + sb.AppendLine(" --romfsdir <dir> Specify RomFS directory path."); + + return sb.ToString(); + } + private class CliOption { public CliOption(string longName, char shortName, int argsNeeded, Action<Options, string[]> assigner) diff --git a/hactoolnet/Options.cs b/hactoolnet/Options.cs index 90966995..e77c69fe 100644 --- a/hactoolnet/Options.cs +++ b/hactoolnet/Options.cs @@ -18,6 +18,11 @@ namespace hactoolnet public string OutDir; public string SdSeed; public string SdPath; + public bool ListApps; + public bool ListTitles; + public bool ListRomFs; + public ulong TitleId; + } internal enum FileType diff --git a/hactoolnet/Program.cs b/hactoolnet/Program.cs index b0d9b27e..3e065a3c 100644 --- a/hactoolnet/Program.cs +++ b/hactoolnet/Program.cs @@ -2,6 +2,7 @@ using System.Diagnostics; using System.IO; using System.Linq; +using System.Text; using libhac; namespace hactoolnet @@ -10,6 +11,7 @@ namespace hactoolnet { static void Main(string[] args) { + Console.OutputEncoding = Encoding.UTF8; var ctx = new Context(); ctx.Options = CliParser.Parse(args); if (ctx.Options == null) return; @@ -31,6 +33,7 @@ namespace hactoolnet case FileType.Nax0: break; case FileType.SwitchFs: + ProcessSwitchFs(ctx); break; default: throw new ArgumentOutOfRangeException(); @@ -50,31 +53,94 @@ namespace hactoolnet { var nca = new Nca(ctx.Keyset, file, false); - if (ctx.Options.RomfsOut != null && nca.Sections[1] != null) + for (int i = 0; i < 3; i++) { - var romfs = nca.OpenSection(1, false); - - using (var outFile = new FileStream(ctx.Options.RomfsOut, FileMode.Create, FileAccess.ReadWrite)) + if (ctx.Options.SectionOut[i] != null) { - romfs.CopyStream(outFile, romfs.Length, ctx.Logger); + nca.ExportSection(i, ctx.Options.SectionOut[i], ctx.Options.Raw, ctx.Logger); + } + + if (ctx.Options.SectionOutDir[i] != null) + { + nca.ExtractSection(i, ctx.Options.SectionOutDir[i], ctx.Logger); } } - if (ctx.Options.SectionOut[0] != null && nca.Sections[0] != null) + if (ctx.Options.ListRomFs && nca.Sections[1] != null) { - var romfs = nca.OpenSection(0, false); + var romfs = new Romfs(nca.OpenSection(1, false)); - using (var outFile = new FileStream(ctx.Options.SectionOut[0], FileMode.Create, FileAccess.ReadWrite)) + foreach (var romfsFile in romfs.Files) { - romfs.CopyStream(outFile, romfs.Length, ctx.Logger); + ctx.Logger.LogMessage(romfsFile.FullPath); } } } } + private static void ProcessSwitchFs(Context ctx) + { + var switchFs = new SdFs(ctx.Keyset, ctx.Options.InFile); + + if (ctx.Options.ListTitles) + { + ListTitles(switchFs); + } + + if (ctx.Options.ListApps) + { + ctx.Logger.LogMessage(ListApplications(switchFs)); + } + + if (ctx.Options.RomfsOutDir != null) + { + var id = ctx.Options.TitleId; + if (id == 0) + { + ctx.Logger.LogMessage("Title ID must be specified to dump RomFS"); + return; + } + + if (!switchFs.Titles.TryGetValue(id, out var title)) + { + ctx.Logger.LogMessage($"Could not find title {id:X16}"); + return; + } + + if (title.ProgramNca == null) + { + ctx.Logger.LogMessage($"Could not find main program data for title {id:X16}"); + return; + } + + var romfs = new Romfs(title.ProgramNca.OpenSection(1, false)); + romfs.Extract(ctx.Options.RomfsOutDir, ctx.Logger); + } + } + private static void OpenKeyset(Context ctx) { - ctx.Keyset = ExternalKeys.ReadKeyFile(ctx.Options.Keyfile, ctx.Options.TitleKeyFile, ctx.Logger); + var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + var homeKeyFile = Path.Combine(home, ".switch", "prod.keys"); + var homeTitleKeyFile = Path.Combine(home, ".switch", "titlekeys.txt"); + var keyFile = ctx.Options.Keyfile; + var titleKeyFile = ctx.Options.TitleKeyFile; + + if (keyFile == null && File.Exists(homeKeyFile)) + { + keyFile = homeKeyFile; + } + + if (titleKeyFile == null && File.Exists(homeTitleKeyFile)) + { + titleKeyFile = homeTitleKeyFile; + } + + ctx.Keyset = ExternalKeys.ReadKeyFile(keyFile, titleKeyFile, ctx.Logger); + if (ctx.Options.SdSeed != null) + { + ctx.Keyset.SetSdSeed(ctx.Options.SdSeed.ToBytes()); + } } private static void ListSdfs(string[] args) @@ -215,36 +281,40 @@ namespace hactoolnet } } - static void ListApplications(SdFs sdfs) + static string ListApplications(SdFs sdfs) { + var sb = new StringBuilder(); + foreach (var app in sdfs.Applications.Values.OrderBy(x => x.Name)) { - Console.WriteLine($"{app.Name} v{app.DisplayVersion}"); + sb.AppendLine($"{app.Name} v{app.DisplayVersion}"); if (app.Main != null) { - Console.WriteLine($"Software: {Util.GetBytesReadable(app.Main.GetSize())}"); + sb.AppendLine($"Software: {Util.GetBytesReadable(app.Main.GetSize())}"); } if (app.Patch != null) { - Console.WriteLine($"Update Data: {Util.GetBytesReadable(app.Patch.GetSize())}"); + sb.AppendLine($"Update Data: {Util.GetBytesReadable(app.Patch.GetSize())}"); } if (app.AddOnContent.Count > 0) { - Console.WriteLine($"DLC: {Util.GetBytesReadable(app.AddOnContent.Sum(x => x.GetSize()))}"); + sb.AppendLine($"DLC: {Util.GetBytesReadable(app.AddOnContent.Sum(x => x.GetSize()))}"); } if (app.Nacp?.UserTotalSaveDataSize > 0) - Console.WriteLine($"User save: {Util.GetBytesReadable(app.Nacp.UserTotalSaveDataSize)}"); + sb.AppendLine($"User save: {Util.GetBytesReadable(app.Nacp.UserTotalSaveDataSize)}"); if (app.Nacp?.DeviceTotalSaveDataSize > 0) - Console.WriteLine($"System save: {Util.GetBytesReadable(app.Nacp.DeviceTotalSaveDataSize)}"); + sb.AppendLine($"System save: {Util.GetBytesReadable(app.Nacp.DeviceTotalSaveDataSize)}"); if (app.Nacp?.BcatSaveDataSize > 0) - Console.WriteLine($"BCAT save: {Util.GetBytesReadable(app.Nacp.BcatSaveDataSize)}"); + sb.AppendLine($"BCAT save: {Util.GetBytesReadable(app.Nacp.BcatSaveDataSize)}"); - Console.WriteLine(""); + sb.AppendLine(); } + + return sb.ToString(); } } } diff --git a/libhac/Keyset.cs b/libhac/Keyset.cs index fa088f8f..c803612b 100644 --- a/libhac/Keyset.cs +++ b/libhac/Keyset.cs @@ -119,6 +119,8 @@ namespace libhac public static Keyset ReadKeyFile(string filename, IProgressReport progress = null) { var keyset = new Keyset(); + if (filename == null) return keyset; + using (var reader = new StreamReader(new FileStream(filename, FileMode.Open, FileAccess.Read))) { string line; diff --git a/libhac/Nca.cs b/libhac/Nca.cs index 0149b0df..4eb1bcbf 100644 --- a/libhac/Nca.cs +++ b/libhac/Nca.cs @@ -207,4 +207,46 @@ namespace libhac public Pfs0Superblock Pfs0 { get; set; } } + + public static class NcaExtensions + { + public static void ExportSection(this Nca nca, int index, string filename, bool raw = false, IProgressReport logger = null) + { + if(index < 0 || index > 3) throw new IndexOutOfRangeException(); + if (nca.Sections[index] == null) return; + + var section = nca.OpenSection(index, raw); + Directory.CreateDirectory(Path.GetDirectoryName(filename)); + + using (var outFile = new FileStream(filename, FileMode.Create, FileAccess.ReadWrite)) + { + section.CopyStream(outFile, section.Length, logger); + } + } + + public static void ExtractSection(this Nca nca, int index, string outputDir, IProgressReport logger = null) + { + if(index < 0 || index > 3) throw new IndexOutOfRangeException(); + if (nca.Sections[index] == null) return; + + var section = nca.Sections[index]; + var stream = nca.OpenSection(index, false); + + switch (section.Type) + { + case SectionType.Invalid: + break; + case SectionType.Pfs0: + var pfs0 = new Pfs0(stream); + pfs0.Extract(outputDir, logger); + break; + case SectionType.Romfs: + var romfs = new Romfs(stream); + romfs.Extract(outputDir, logger); + break; + case SectionType.Bktr: + break; + } + } + } } diff --git a/libhac/Pfs0.cs b/libhac/Pfs0.cs index ed06039c..7e1c3c22 100644 --- a/libhac/Pfs0.cs +++ b/libhac/Pfs0.cs @@ -1,4 +1,6 @@ -using System.IO; +using System.Collections.Generic; +using System.IO; +using System.Linq; using System.Text; namespace libhac @@ -7,7 +9,9 @@ namespace libhac { public Pfs0Header Header { get; set; } public int HeaderSize { get; set; } - public Pfs0FileEntry[] Entries { get; set; } + public Pfs0FileEntry[] Files { get; set; } + + private Dictionary<string, Pfs0FileEntry> FileDict { get; } private Stream Stream { get; set; } public Pfs0(Stream stream) @@ -25,26 +29,42 @@ namespace libhac { reader.BaseStream.Position = 16; - Entries = new Pfs0FileEntry[Header.NumFiles]; + Files = new Pfs0FileEntry[Header.NumFiles]; for (int i = 0; i < Header.NumFiles; i++) { - Entries[i] = new Pfs0FileEntry(reader) { Index = i }; + Files[i] = new Pfs0FileEntry(reader) { Index = i }; } int stringTableOffset = 16 + 24 * Header.NumFiles; for (int i = 0; i < Header.NumFiles; i++) { - reader.BaseStream.Position = stringTableOffset + Entries[i].StringTableOffset; - Entries[i].Name = reader.ReadAsciiZ(); + reader.BaseStream.Position = stringTableOffset + Files[i].StringTableOffset; + Files[i].Name = reader.ReadAsciiZ(); } } + FileDict = Files.ToDictionary(x => x.Name, x => x); Stream = stream; } + public Stream OpenFile(string filename) + { + if (!FileDict.TryGetValue(filename, out Pfs0FileEntry file)) + { + throw new FileNotFoundException(); + } + + return OpenFile(file); + } + + public Stream OpenFile(Pfs0FileEntry file) + { + return new SubStream(Stream, HeaderSize + file.Offset, file.Size); + } + public byte[] GetFile(int index) { - var entry = Entries[index]; + var entry = Files[index]; var file = new byte[entry.Size]; Stream.Position = HeaderSize + entry.Offset; Stream.Read(file, 0, file.Length); @@ -108,4 +128,23 @@ namespace libhac Reserved = reader.ReadUInt32(); } } + + public static class Pfs0Extensions + { + public static void Extract(this Pfs0 pfs0, string outDir, IProgressReport logger = null) + { + foreach (var file in pfs0.Files) + { + var stream = pfs0.OpenFile(file); + var outName = Path.Combine(outDir, file.Name); + Directory.CreateDirectory(Path.GetDirectoryName(outName)); + + using (var outFile = new FileStream(outName, FileMode.Create, FileAccess.ReadWrite)) + { + logger?.LogMessage(file.Name); + stream.CopyStream(outFile, stream.Length, logger); + } + } + } + } } diff --git a/libhac/Romfs.cs b/libhac/Romfs.cs index 39887eb8..6e9fb99c 100644 --- a/libhac/Romfs.cs +++ b/libhac/Romfs.cs @@ -8,7 +8,7 @@ namespace libhac { public class Romfs { - public static readonly int IvfcMaxLevel = 6; + internal const int IvfcMaxLevel = 6; public RomfsHeader Header { get; } public List<RomfsDir> Directories { get; } = new List<RomfsDir>(); public List<RomfsFile> Files { get; } = new List<RomfsFile>(); @@ -62,13 +62,17 @@ namespace libhac public Stream OpenFile(string filename) { - if (!FileDict.TryGetValue(filename, out var file)) + if (!FileDict.TryGetValue(filename, out RomfsFile file)) { throw new FileNotFoundException(); } - var stream = new SubStream(Stream, Header.DataOffset + file.DataOffset, file.DataLength); - return stream; + return OpenFile(file); + } + + public Stream OpenFile(RomfsFile file) + { + return new SubStream(Stream, Header.DataOffset + file.DataOffset, file.DataLength); } public byte[] GetFile(string filename) @@ -232,4 +236,23 @@ namespace libhac public ulong HashBlockSize { get; set; } public ulong HashBlockCount { get; set; } } + + public static class RomfsExtensions + { + public static void Extract(this Romfs romfs, string outDir, IProgressReport logger = null) + { + foreach (var file in romfs.Files) + { + var stream = romfs.OpenFile(file); + var outName = outDir + file.FullPath; + Directory.CreateDirectory(Path.GetDirectoryName(outName)); + + using (var outFile = new FileStream(outName, FileMode.Create, FileAccess.ReadWrite)) + { + logger?.LogMessage(file.FullPath); + stream.CopyStream(outFile, stream.Length, logger); + } + } + } + } } diff --git a/libhac/SdFs.cs b/libhac/SdFs.cs index 33aaf025..0bcd66c3 100644 --- a/libhac/SdFs.cs +++ b/libhac/SdFs.cs @@ -17,8 +17,6 @@ namespace libhac public Dictionary<ulong, Title> Titles { get; } = new Dictionary<ulong, Title>(); public Dictionary<ulong, Application> Applications { get; } = new Dictionary<ulong, Application>(); - private List<Nax0> Nax0s { get; } = new List<Nax0>(); - public SdFs(Keyset keyset, string rootDir) { RootDir = rootDir; @@ -63,7 +61,6 @@ namespace libhac { var sdPath = "/" + Util.GetRelativePath(file, ContentsDir).Replace('\\', '/'); var nax0 = new Nax0(Keyset, stream, sdPath, false); - Nax0s.Add(nax0); nca = new Nca(Keyset, nax0.Stream, false); } else @@ -212,9 +209,6 @@ namespace libhac nca.Dispose(); } Ncas.Clear(); - - // Disposing the Nca disposes the Nax0 as well - Nax0s.Clear(); Titles.Clear(); }