Add basic CLI functionality

This commit is contained in:
Alex Barney 2018-07-02 21:21:35 -05:00
parent 79b09267e6
commit 20be7206a0
8 changed files with 267 additions and 40 deletions

View file

@ -1,5 +1,7 @@
using System; using System;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Text;
namespace hactoolnet namespace hactoolnet
{ {
@ -26,6 +28,10 @@ namespace hactoolnet
new CliOption("outdir", 1, (o, a) => o.OutDir = a[0]), new CliOption("outdir", 1, (o, a) => o.OutDir = a[0]),
new CliOption("sdseed", 1, (o, a) => o.SdSeed = a[0]), new CliOption("sdseed", 1, (o, a) => o.SdSeed = a[0]),
new CliOption("sdpath", 1, (o, a) => o.SdPath = 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) public static Options Parse(string[] args)
@ -49,7 +55,7 @@ namespace hactoolnet
{ {
if (inputSpecified) if (inputSpecified)
{ {
Console.WriteLine($"Unable to parse option {args[i]}"); PrintWithUsage($"Unable to parse option {args[i]}");
return null; return null;
} }
@ -61,13 +67,13 @@ namespace hactoolnet
var option = CliOptions.FirstOrDefault(x => x.Long == arg || x.Short == arg); var option = CliOptions.FirstOrDefault(x => x.Long == arg || x.Short == arg);
if (option == null) if (option == null)
{ {
Console.WriteLine($"Unknown option {args[i]}"); PrintWithUsage($"Unknown option {args[i]}");
return null; return null;
} }
if (i + option.ArgsNeeded >= args.Length) 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; return null;
} }
@ -80,7 +86,7 @@ namespace hactoolnet
if (!inputSpecified) if (!inputSpecified)
{ {
Console.WriteLine("Input file must be specified"); PrintWithUsage("Input file must be specified");
return null; return null;
} }
@ -97,12 +103,58 @@ namespace hactoolnet
return type; 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) private static void PrintWithUsage(string toPrint)
{ {
Console.WriteLine(toPrint); Console.WriteLine(toPrint);
Console.WriteLine(GetUsage());
// PrintUsage(); // PrintUsage();
} }
private static string GetUsage()
{
var sb = new StringBuilder();
sb.AppendLine("Usage: hactoolnet.exe [options...] <path>");
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 <file> Load title keys from an external file.");
sb.AppendLine("NCA options:");
sb.AppendLine(" --section0 <file> Specify Section 0 file path.");
sb.AppendLine(" --section1 <file> Specify Section 1 file path.");
sb.AppendLine(" --section2 <file> Specify Section 2 file path.");
sb.AppendLine(" --section3 <file> Specify Section 3 file path.");
sb.AppendLine(" --section0dir <dir> Specify Section 0 directory path.");
sb.AppendLine(" --section1dir <dir> Specify Section 1 directory path.");
sb.AppendLine(" --section2dir <dir> Specify Section 2 directory path.");
sb.AppendLine(" --section3dir <dir> Specify Section 3 directory path.");
sb.AppendLine(" --listromfs List files in RomFS.");
sb.AppendLine("Switch FS options:");
sb.AppendLine(" --sdseed <seed> 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 <title id> Specify title ID to use.");
sb.AppendLine(" --romfsdir <dir> Specify RomFS directory path.");
return sb.ToString();
}
private class CliOption private class CliOption
{ {
public CliOption(string longName, char shortName, int argsNeeded, Action<Options, string[]> assigner) public CliOption(string longName, char shortName, int argsNeeded, Action<Options, string[]> assigner)

View file

@ -18,6 +18,11 @@ namespace hactoolnet
public string OutDir; public string OutDir;
public string SdSeed; public string SdSeed;
public string SdPath; public string SdPath;
public bool ListApps;
public bool ListTitles;
public bool ListRomFs;
public ulong TitleId;
} }
internal enum FileType internal enum FileType

View file

@ -2,6 +2,7 @@
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text;
using libhac; using libhac;
namespace hactoolnet namespace hactoolnet
@ -10,6 +11,7 @@ namespace hactoolnet
{ {
static void Main(string[] args) static void Main(string[] args)
{ {
Console.OutputEncoding = Encoding.UTF8;
var ctx = new Context(); var ctx = new Context();
ctx.Options = CliParser.Parse(args); ctx.Options = CliParser.Parse(args);
if (ctx.Options == null) return; if (ctx.Options == null) return;
@ -31,6 +33,7 @@ namespace hactoolnet
case FileType.Nax0: case FileType.Nax0:
break; break;
case FileType.SwitchFs: case FileType.SwitchFs:
ProcessSwitchFs(ctx);
break; break;
default: default:
throw new ArgumentOutOfRangeException(); throw new ArgumentOutOfRangeException();
@ -50,31 +53,94 @@ namespace hactoolnet
{ {
var nca = new Nca(ctx.Keyset, file, false); 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); if (ctx.Options.SectionOut[i] != null)
{
nca.ExportSection(i, ctx.Options.SectionOut[i], ctx.Options.Raw, ctx.Logger);
}
using (var outFile = new FileStream(ctx.Options.RomfsOut, FileMode.Create, FileAccess.ReadWrite)) if (ctx.Options.SectionOutDir[i] != null)
{ {
romfs.CopyStream(outFile, romfs.Length, ctx.Logger); 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) 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) 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)) 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) if (app.Main != null)
{ {
Console.WriteLine($"Software: {Util.GetBytesReadable(app.Main.GetSize())}"); sb.AppendLine($"Software: {Util.GetBytesReadable(app.Main.GetSize())}");
} }
if (app.Patch != null) 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) 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) 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) 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) 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();
} }
} }
} }

View file

@ -119,6 +119,8 @@ namespace libhac
public static Keyset ReadKeyFile(string filename, IProgressReport progress = null) public static Keyset ReadKeyFile(string filename, IProgressReport progress = null)
{ {
var keyset = new Keyset(); var keyset = new Keyset();
if (filename == null) return keyset;
using (var reader = new StreamReader(new FileStream(filename, FileMode.Open, FileAccess.Read))) using (var reader = new StreamReader(new FileStream(filename, FileMode.Open, FileAccess.Read)))
{ {
string line; string line;

View file

@ -207,4 +207,46 @@ namespace libhac
public Pfs0Superblock Pfs0 { get; set; } 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;
}
}
}
} }

View file

@ -1,4 +1,6 @@
using System.IO; using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text; using System.Text;
namespace libhac namespace libhac
@ -7,7 +9,9 @@ namespace libhac
{ {
public Pfs0Header Header { get; set; } public Pfs0Header Header { get; set; }
public int HeaderSize { 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; } private Stream Stream { get; set; }
public Pfs0(Stream stream) public Pfs0(Stream stream)
@ -25,26 +29,42 @@ namespace libhac
{ {
reader.BaseStream.Position = 16; reader.BaseStream.Position = 16;
Entries = new Pfs0FileEntry[Header.NumFiles]; Files = new Pfs0FileEntry[Header.NumFiles];
for (int i = 0; i < Header.NumFiles; i++) 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; int stringTableOffset = 16 + 24 * Header.NumFiles;
for (int i = 0; i < Header.NumFiles; i++) for (int i = 0; i < Header.NumFiles; i++)
{ {
reader.BaseStream.Position = stringTableOffset + Entries[i].StringTableOffset; reader.BaseStream.Position = stringTableOffset + Files[i].StringTableOffset;
Entries[i].Name = reader.ReadAsciiZ(); Files[i].Name = reader.ReadAsciiZ();
} }
} }
FileDict = Files.ToDictionary(x => x.Name, x => x);
Stream = stream; 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) public byte[] GetFile(int index)
{ {
var entry = Entries[index]; var entry = Files[index];
var file = new byte[entry.Size]; var file = new byte[entry.Size];
Stream.Position = HeaderSize + entry.Offset; Stream.Position = HeaderSize + entry.Offset;
Stream.Read(file, 0, file.Length); Stream.Read(file, 0, file.Length);
@ -108,4 +128,23 @@ namespace libhac
Reserved = reader.ReadUInt32(); 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);
}
}
}
}
} }

View file

@ -8,7 +8,7 @@ namespace libhac
{ {
public class Romfs public class Romfs
{ {
public static readonly int IvfcMaxLevel = 6; internal const int IvfcMaxLevel = 6;
public RomfsHeader Header { get; } public RomfsHeader Header { get; }
public List<RomfsDir> Directories { get; } = new List<RomfsDir>(); public List<RomfsDir> Directories { get; } = new List<RomfsDir>();
public List<RomfsFile> Files { get; } = new List<RomfsFile>(); public List<RomfsFile> Files { get; } = new List<RomfsFile>();
@ -62,13 +62,17 @@ namespace libhac
public Stream OpenFile(string filename) public Stream OpenFile(string filename)
{ {
if (!FileDict.TryGetValue(filename, out var file)) if (!FileDict.TryGetValue(filename, out RomfsFile file))
{ {
throw new FileNotFoundException(); throw new FileNotFoundException();
} }
var stream = new SubStream(Stream, Header.DataOffset + file.DataOffset, file.DataLength); return OpenFile(file);
return stream; }
public Stream OpenFile(RomfsFile file)
{
return new SubStream(Stream, Header.DataOffset + file.DataOffset, file.DataLength);
} }
public byte[] GetFile(string filename) public byte[] GetFile(string filename)
@ -232,4 +236,23 @@ namespace libhac
public ulong HashBlockSize { get; set; } public ulong HashBlockSize { get; set; }
public ulong HashBlockCount { 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);
}
}
}
}
} }

View file

@ -17,8 +17,6 @@ namespace libhac
public Dictionary<ulong, Title> Titles { get; } = new Dictionary<ulong, Title>(); public Dictionary<ulong, Title> Titles { get; } = new Dictionary<ulong, Title>();
public Dictionary<ulong, Application> Applications { get; } = new Dictionary<ulong, Application>(); public Dictionary<ulong, Application> Applications { get; } = new Dictionary<ulong, Application>();
private List<Nax0> Nax0s { get; } = new List<Nax0>();
public SdFs(Keyset keyset, string rootDir) public SdFs(Keyset keyset, string rootDir)
{ {
RootDir = rootDir; RootDir = rootDir;
@ -63,7 +61,6 @@ namespace libhac
{ {
var sdPath = "/" + Util.GetRelativePath(file, ContentsDir).Replace('\\', '/'); var sdPath = "/" + Util.GetRelativePath(file, ContentsDir).Replace('\\', '/');
var nax0 = new Nax0(Keyset, stream, sdPath, false); var nax0 = new Nax0(Keyset, stream, sdPath, false);
Nax0s.Add(nax0);
nca = new Nca(Keyset, nax0.Stream, false); nca = new Nca(Keyset, nax0.Stream, false);
} }
else else
@ -212,9 +209,6 @@ namespace libhac
nca.Dispose(); nca.Dispose();
} }
Ncas.Clear(); Ncas.Clear();
// Disposing the Nca disposes the Nax0 as well
Nax0s.Clear();
Titles.Clear(); Titles.Clear();
} }