mirror of
https://github.com/Thealexbarney/LibHac.git
synced 2024-11-14 10:49:41 +01:00
Add basic CLI functionality
This commit is contained in:
parent
79b09267e6
commit
20be7206a0
8 changed files with 267 additions and 40 deletions
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue