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.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...] <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
{
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 SdSeed;
public string SdPath;
public bool ListApps;
public bool ListTitles;
public bool ListRomFs;
public ulong TitleId;
}
internal enum FileType

View file

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

View file

@ -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;

View file

@ -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;
}
}
}
}

View file

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

View file

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

View file

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