2018-06-21 18:16:51 +02:00
|
|
|
|
using System;
|
|
|
|
|
using System.IO;
|
2018-06-21 23:03:58 +02:00
|
|
|
|
using System.Linq;
|
2018-07-03 04:21:35 +02:00
|
|
|
|
using System.Text;
|
2018-06-21 16:25:20 +02:00
|
|
|
|
using libhac;
|
2018-06-16 19:11:38 +02:00
|
|
|
|
|
2018-06-21 16:25:20 +02:00
|
|
|
|
namespace hactoolnet
|
2018-06-16 19:11:38 +02:00
|
|
|
|
{
|
2018-06-21 16:25:20 +02:00
|
|
|
|
public static class Program
|
2018-06-16 19:11:38 +02:00
|
|
|
|
{
|
2018-07-05 23:37:30 +02:00
|
|
|
|
public static void Main(string[] args)
|
2018-06-28 03:25:25 +02:00
|
|
|
|
{
|
2018-07-03 04:21:35 +02:00
|
|
|
|
Console.OutputEncoding = Encoding.UTF8;
|
2018-07-02 22:12:41 +02:00
|
|
|
|
var ctx = new Context();
|
|
|
|
|
ctx.Options = CliParser.Parse(args);
|
|
|
|
|
if (ctx.Options == null) return;
|
|
|
|
|
|
|
|
|
|
using (var logger = new ProgressBar())
|
|
|
|
|
{
|
|
|
|
|
ctx.Logger = logger;
|
|
|
|
|
OpenKeyset(ctx);
|
|
|
|
|
|
2018-07-06 04:37:46 +02:00
|
|
|
|
if (ctx.Options.RunCustom)
|
|
|
|
|
{
|
|
|
|
|
CustomTask(ctx);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-02 22:12:41 +02:00
|
|
|
|
switch (ctx.Options.InFileType)
|
|
|
|
|
{
|
|
|
|
|
case FileType.Nca:
|
|
|
|
|
ProcessNca(ctx);
|
|
|
|
|
break;
|
|
|
|
|
case FileType.Pfs0:
|
|
|
|
|
break;
|
|
|
|
|
case FileType.Romfs:
|
|
|
|
|
break;
|
|
|
|
|
case FileType.Nax0:
|
|
|
|
|
break;
|
|
|
|
|
case FileType.SwitchFs:
|
2018-07-03 04:21:35 +02:00
|
|
|
|
ProcessSwitchFs(ctx);
|
2018-07-02 22:12:41 +02:00
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
throw new ArgumentOutOfRangeException();
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-06-28 03:25:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-07-02 22:12:41 +02:00
|
|
|
|
private static void ProcessNca(Context ctx)
|
|
|
|
|
{
|
|
|
|
|
using (var file = new FileStream(ctx.Options.InFile, FileMode.Open, FileAccess.Read))
|
|
|
|
|
{
|
|
|
|
|
var nca = new Nca(ctx.Keyset, file, false);
|
|
|
|
|
|
2018-07-07 22:45:06 +02:00
|
|
|
|
if (ctx.Options.BaseNca != null)
|
|
|
|
|
{
|
|
|
|
|
var baseFile = new FileStream(ctx.Options.BaseNca, FileMode.Open, FileAccess.Read);
|
|
|
|
|
var baseNca = new Nca(ctx.Keyset, baseFile, false);
|
|
|
|
|
nca.SetBaseNca(baseNca);
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-03 04:21:35 +02:00
|
|
|
|
for (int i = 0; i < 3; i++)
|
2018-07-02 22:12:41 +02:00
|
|
|
|
{
|
2018-07-03 04:21:35 +02:00
|
|
|
|
if (ctx.Options.SectionOut[i] != null)
|
|
|
|
|
{
|
|
|
|
|
nca.ExportSection(i, ctx.Options.SectionOut[i], ctx.Options.Raw, ctx.Logger);
|
|
|
|
|
}
|
2018-07-02 22:12:41 +02:00
|
|
|
|
|
2018-07-03 04:21:35 +02:00
|
|
|
|
if (ctx.Options.SectionOutDir[i] != null)
|
2018-07-02 22:12:41 +02:00
|
|
|
|
{
|
2018-07-03 04:21:35 +02:00
|
|
|
|
nca.ExtractSection(i, ctx.Options.SectionOutDir[i], ctx.Logger);
|
2018-07-02 22:12:41 +02:00
|
|
|
|
}
|
2018-07-05 23:37:30 +02:00
|
|
|
|
|
|
|
|
|
if (ctx.Options.Validate && nca.Sections[i] != null)
|
|
|
|
|
{
|
|
|
|
|
nca.VerifySection(i, ctx.Logger);
|
|
|
|
|
}
|
2018-07-02 22:12:41 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-07-03 04:21:35 +02:00
|
|
|
|
if (ctx.Options.ListRomFs && nca.Sections[1] != null)
|
2018-07-02 22:12:41 +02:00
|
|
|
|
{
|
2018-07-03 04:21:35 +02:00
|
|
|
|
var romfs = new Romfs(nca.OpenSection(1, false));
|
2018-07-02 22:12:41 +02:00
|
|
|
|
|
2018-07-03 04:21:35 +02:00
|
|
|
|
foreach (var romfsFile in romfs.Files)
|
2018-07-02 22:12:41 +02:00
|
|
|
|
{
|
2018-07-03 04:21:35 +02:00
|
|
|
|
ctx.Logger.LogMessage(romfsFile.FullPath);
|
2018-07-02 22:12:41 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
2018-07-05 23:37:30 +02:00
|
|
|
|
|
2018-07-07 22:45:06 +02:00
|
|
|
|
if (ctx.Options.RomfsOutDir != null)
|
|
|
|
|
{
|
|
|
|
|
NcaSection section = nca.Sections.FirstOrDefault(x => x.Type == SectionType.Romfs || x.Type == SectionType.Bktr);
|
|
|
|
|
|
|
|
|
|
if (section == null)
|
|
|
|
|
{
|
|
|
|
|
ctx.Logger.LogMessage("NCA has no RomFS section");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (section.Type == SectionType.Bktr)
|
|
|
|
|
{
|
|
|
|
|
if (ctx.Options.BaseNca == null)
|
|
|
|
|
{
|
|
|
|
|
ctx.Logger.LogMessage("Cannot save BKTR section without base RomFS");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var bktr = nca.OpenSection(1, false);
|
|
|
|
|
var romfs = new Romfs(bktr);
|
|
|
|
|
romfs.Extract(ctx.Options.RomfsOutDir, ctx.Logger);
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
var romfs = new Romfs(nca.OpenSection(section.SectionNum, false));
|
|
|
|
|
romfs.Extract(ctx.Options.RomfsOutDir, ctx.Logger);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-05 23:37:30 +02:00
|
|
|
|
ctx.Logger.LogMessage(nca.Dump());
|
2018-07-02 22:12:41 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-03 04:21:35 +02:00
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-06 04:37:46 +02:00
|
|
|
|
if (title.MainNca == null)
|
|
|
|
|
{
|
|
|
|
|
ctx.Logger.LogMessage($"Could not find main data for title {id:X16}");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var section = title.MainNca.Sections.FirstOrDefault(x => x.Type == SectionType.Romfs);
|
|
|
|
|
|
|
|
|
|
if (section == null)
|
2018-07-03 04:21:35 +02:00
|
|
|
|
{
|
2018-07-06 04:37:46 +02:00
|
|
|
|
ctx.Logger.LogMessage($"Main NCA for title {id:X16} has no Rom FS section");
|
2018-07-03 04:21:35 +02:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-06 04:37:46 +02:00
|
|
|
|
var romfs = new Romfs(title.MainNca.OpenSection(section.SectionNum, false));
|
2018-07-03 04:21:35 +02:00
|
|
|
|
romfs.Extract(ctx.Options.RomfsOutDir, ctx.Logger);
|
|
|
|
|
}
|
2018-07-05 23:37:30 +02:00
|
|
|
|
|
|
|
|
|
if (ctx.Options.OutDir != null)
|
|
|
|
|
{
|
|
|
|
|
SaveTitle(ctx, switchFs);
|
|
|
|
|
}
|
2018-07-03 04:21:35 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-07-02 22:12:41 +02:00
|
|
|
|
private static void OpenKeyset(Context ctx)
|
|
|
|
|
{
|
2018-07-03 04:21:35 +02:00
|
|
|
|
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());
|
|
|
|
|
}
|
2018-07-02 22:12:41 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-07-05 23:37:30 +02:00
|
|
|
|
// For running random stuff
|
|
|
|
|
// ReSharper disable once UnusedParameter.Local
|
|
|
|
|
private static void CustomTask(Context ctx)
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void SaveTitle(Context ctx, SdFs switchFs)
|
2018-06-27 02:42:01 +02:00
|
|
|
|
{
|
2018-07-05 23:37:30 +02:00
|
|
|
|
var id = ctx.Options.TitleId;
|
|
|
|
|
if (id == 0)
|
|
|
|
|
{
|
|
|
|
|
ctx.Logger.LogMessage("Title ID must be specified to save title");
|
|
|
|
|
return;
|
|
|
|
|
}
|
2018-06-27 02:42:01 +02:00
|
|
|
|
|
2018-07-05 23:37:30 +02:00
|
|
|
|
if (!switchFs.Titles.TryGetValue(id, out var title))
|
|
|
|
|
{
|
|
|
|
|
ctx.Logger.LogMessage($"Could not find title {id:X16}");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var saveDir = Path.Combine(ctx.Options.OutDir, $"{title.Id:X16}v{title.Version.Version}");
|
|
|
|
|
Directory.CreateDirectory(saveDir);
|
2018-06-27 02:42:01 +02:00
|
|
|
|
|
|
|
|
|
foreach (var nca in title.Ncas)
|
|
|
|
|
{
|
2018-07-05 23:37:30 +02:00
|
|
|
|
nca.Stream.Position = 0;
|
|
|
|
|
var outFile = Path.Combine(saveDir, nca.Filename);
|
|
|
|
|
ctx.Logger.LogMessage(nca.Filename);
|
|
|
|
|
using (var outStream = new FileStream(outFile, FileMode.Create, FileAccess.ReadWrite))
|
2018-06-27 02:42:01 +02:00
|
|
|
|
{
|
2018-07-05 23:37:30 +02:00
|
|
|
|
nca.Stream.CopyStream(outStream, nca.Stream.Length, ctx.Logger);
|
2018-06-27 02:42:01 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-06-27 02:10:21 +02:00
|
|
|
|
static void ListTitles(SdFs sdfs)
|
2018-06-21 18:16:51 +02:00
|
|
|
|
{
|
2018-06-27 02:10:21 +02:00
|
|
|
|
foreach (var title in sdfs.Titles.Values.OrderBy(x => x.Id))
|
2018-06-26 00:26:47 +02:00
|
|
|
|
{
|
2018-06-30 21:15:55 +02:00
|
|
|
|
Console.WriteLine($"{title.Name} {title.Control?.Version}");
|
2018-06-27 02:10:21 +02:00
|
|
|
|
Console.WriteLine($"{title.Id:X16} v{title.Version.Version} ({title.Version}) {title.Metadata.Type}");
|
2018-06-22 23:17:20 +02:00
|
|
|
|
|
2018-06-27 02:10:21 +02:00
|
|
|
|
foreach (var content in title.Metadata.ContentEntries)
|
2018-06-22 23:17:20 +02:00
|
|
|
|
{
|
2018-06-27 02:10:21 +02:00
|
|
|
|
Console.WriteLine(
|
2018-06-27 02:42:01 +02:00
|
|
|
|
$" {content.NcaId.ToHexString()}.nca {content.Type} {Util.GetBytesReadable(content.Size)}");
|
2018-06-23 04:02:19 +02:00
|
|
|
|
}
|
2018-06-27 02:10:21 +02:00
|
|
|
|
|
2018-06-28 22:02:23 +02:00
|
|
|
|
foreach (var nca in title.Ncas)
|
|
|
|
|
{
|
|
|
|
|
Console.WriteLine($" {nca.HasRightsId} {nca.NcaId} {nca.Header.ContentType}");
|
|
|
|
|
|
2018-06-28 23:55:36 +02:00
|
|
|
|
foreach (var sect in nca.Sections.Where(x => x != null))
|
2018-06-28 22:02:23 +02:00
|
|
|
|
{
|
|
|
|
|
Console.WriteLine($" {sect.SectionNum} {sect.Type} {sect.Header.CryptType} {sect.SuperblockHashValidity}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-06-27 02:10:21 +02:00
|
|
|
|
Console.WriteLine("");
|
2018-06-21 18:16:51 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
2018-07-02 20:16:38 +02:00
|
|
|
|
|
2018-07-03 04:21:35 +02:00
|
|
|
|
static string ListApplications(SdFs sdfs)
|
2018-07-02 20:16:38 +02:00
|
|
|
|
{
|
2018-07-03 04:21:35 +02:00
|
|
|
|
var sb = new StringBuilder();
|
|
|
|
|
|
2018-07-02 20:16:38 +02:00
|
|
|
|
foreach (var app in sdfs.Applications.Values.OrderBy(x => x.Name))
|
|
|
|
|
{
|
2018-07-03 04:21:35 +02:00
|
|
|
|
sb.AppendLine($"{app.Name} v{app.DisplayVersion}");
|
2018-07-02 20:16:38 +02:00
|
|
|
|
|
|
|
|
|
if (app.Main != null)
|
|
|
|
|
{
|
2018-07-03 04:21:35 +02:00
|
|
|
|
sb.AppendLine($"Software: {Util.GetBytesReadable(app.Main.GetSize())}");
|
2018-07-02 20:16:38 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (app.Patch != null)
|
|
|
|
|
{
|
2018-07-03 04:21:35 +02:00
|
|
|
|
sb.AppendLine($"Update Data: {Util.GetBytesReadable(app.Patch.GetSize())}");
|
2018-07-02 20:16:38 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (app.AddOnContent.Count > 0)
|
|
|
|
|
{
|
2018-07-03 04:21:35 +02:00
|
|
|
|
sb.AppendLine($"DLC: {Util.GetBytesReadable(app.AddOnContent.Sum(x => x.GetSize()))}");
|
2018-07-02 20:16:38 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (app.Nacp?.UserTotalSaveDataSize > 0)
|
2018-07-03 04:21:35 +02:00
|
|
|
|
sb.AppendLine($"User save: {Util.GetBytesReadable(app.Nacp.UserTotalSaveDataSize)}");
|
2018-07-02 20:16:38 +02:00
|
|
|
|
if (app.Nacp?.DeviceTotalSaveDataSize > 0)
|
2018-07-03 04:21:35 +02:00
|
|
|
|
sb.AppendLine($"System save: {Util.GetBytesReadable(app.Nacp.DeviceTotalSaveDataSize)}");
|
2018-07-02 20:16:38 +02:00
|
|
|
|
if (app.Nacp?.BcatSaveDataSize > 0)
|
2018-07-03 04:21:35 +02:00
|
|
|
|
sb.AppendLine($"BCAT save: {Util.GetBytesReadable(app.Nacp.BcatSaveDataSize)}");
|
2018-07-02 20:16:38 +02:00
|
|
|
|
|
2018-07-03 04:21:35 +02:00
|
|
|
|
sb.AppendLine();
|
2018-07-02 20:16:38 +02:00
|
|
|
|
}
|
2018-07-03 04:21:35 +02:00
|
|
|
|
|
|
|
|
|
return sb.ToString();
|
2018-07-02 20:16:38 +02:00
|
|
|
|
}
|
2018-06-16 19:11:38 +02:00
|
|
|
|
}
|
|
|
|
|
}
|