2018-09-13 03:28:50 +02:00
|
|
|
|
using System;
|
2018-10-03 00:25:58 +02:00
|
|
|
|
using System.Collections.Generic;
|
2018-09-13 03:28:50 +02:00
|
|
|
|
using System.IO;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using System.Text;
|
|
|
|
|
using LibHac;
|
2018-10-13 22:12:10 +02:00
|
|
|
|
using LibHac.Save;
|
2018-09-13 03:28:50 +02:00
|
|
|
|
|
|
|
|
|
namespace hactoolnet
|
|
|
|
|
{
|
|
|
|
|
internal static class ProcessSwitchFs
|
|
|
|
|
{
|
|
|
|
|
public static void Process(Context ctx)
|
|
|
|
|
{
|
|
|
|
|
var switchFs = new SwitchFs(ctx.Keyset, new FileSystem(ctx.Options.InFile));
|
|
|
|
|
|
|
|
|
|
if (ctx.Options.ListTitles)
|
|
|
|
|
{
|
|
|
|
|
ListTitles(switchFs);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ctx.Options.ListApps)
|
|
|
|
|
{
|
|
|
|
|
ctx.Logger.LogMessage(ListApplications(switchFs));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ctx.Options.ExefsOutDir != null || ctx.Options.ExefsOut != null)
|
|
|
|
|
{
|
2018-10-03 00:25:58 +02:00
|
|
|
|
ulong id = ctx.Options.TitleId;
|
2018-09-13 03:28:50 +02:00
|
|
|
|
if (id == 0)
|
|
|
|
|
{
|
|
|
|
|
ctx.Logger.LogMessage("Title ID must be specified to dump ExeFS");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-03 00:25:58 +02:00
|
|
|
|
if (!switchFs.Titles.TryGetValue(id, out Title title))
|
2018-09-13 03:28:50 +02:00
|
|
|
|
{
|
|
|
|
|
ctx.Logger.LogMessage($"Could not find title {id:X16}");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (title.MainNca == null)
|
|
|
|
|
{
|
|
|
|
|
ctx.Logger.LogMessage($"Could not find main data for title {id:X16}");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-13 04:28:05 +02:00
|
|
|
|
NcaSection section = title.MainNca.Sections[(int)ProgramPartitionType.Code];
|
2018-09-13 03:28:50 +02:00
|
|
|
|
|
|
|
|
|
if (section == null)
|
|
|
|
|
{
|
|
|
|
|
ctx.Logger.LogMessage($"Main NCA for title {id:X16} has no ExeFS section");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ctx.Options.ExefsOutDir != null)
|
|
|
|
|
{
|
2018-10-09 22:33:56 +02:00
|
|
|
|
title.MainNca.ExtractSection(section.SectionNum, ctx.Options.ExefsOutDir, ctx.Options.IntegrityLevel, ctx.Logger);
|
2018-09-13 03:28:50 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ctx.Options.ExefsOut != null)
|
|
|
|
|
{
|
2018-10-09 22:33:56 +02:00
|
|
|
|
title.MainNca.ExportSection(section.SectionNum, ctx.Options.ExefsOut, ctx.Options.Raw, ctx.Options.IntegrityLevel, ctx.Logger);
|
2018-09-13 03:28:50 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ctx.Options.RomfsOutDir != null || ctx.Options.RomfsOut != null)
|
|
|
|
|
{
|
2018-10-03 00:25:58 +02:00
|
|
|
|
ulong id = ctx.Options.TitleId;
|
2018-09-13 03:28:50 +02:00
|
|
|
|
if (id == 0)
|
|
|
|
|
{
|
|
|
|
|
ctx.Logger.LogMessage("Title ID must be specified to dump RomFS");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-03 00:25:58 +02:00
|
|
|
|
if (!switchFs.Titles.TryGetValue(id, out Title title))
|
2018-09-13 03:28:50 +02:00
|
|
|
|
{
|
|
|
|
|
ctx.Logger.LogMessage($"Could not find title {id:X16}");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (title.MainNca == null)
|
|
|
|
|
{
|
|
|
|
|
ctx.Logger.LogMessage($"Could not find main data for title {id:X16}");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-03 00:25:58 +02:00
|
|
|
|
NcaSection section = title.MainNca.Sections.FirstOrDefault(x => x?.Type == SectionType.Romfs || x?.Type == SectionType.Bktr);
|
2018-09-13 03:28:50 +02:00
|
|
|
|
|
|
|
|
|
if (section == null)
|
|
|
|
|
{
|
|
|
|
|
ctx.Logger.LogMessage($"Main NCA for title {id:X16} has no RomFS section");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ctx.Options.RomfsOutDir != null)
|
|
|
|
|
{
|
2018-10-09 22:33:56 +02:00
|
|
|
|
var romfs = new Romfs(title.MainNca.OpenSection(section.SectionNum, false, ctx.Options.IntegrityLevel));
|
2018-09-13 03:28:50 +02:00
|
|
|
|
romfs.Extract(ctx.Options.RomfsOutDir, ctx.Logger);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ctx.Options.RomfsOut != null)
|
|
|
|
|
{
|
2018-10-09 22:33:56 +02:00
|
|
|
|
title.MainNca.ExportSection(section.SectionNum, ctx.Options.RomfsOut, ctx.Options.Raw, ctx.Options.IntegrityLevel, ctx.Logger);
|
2018-09-13 03:28:50 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ctx.Options.OutDir != null)
|
|
|
|
|
{
|
|
|
|
|
SaveTitle(ctx, switchFs);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ctx.Options.NspOut != null)
|
|
|
|
|
{
|
|
|
|
|
ProcessNsp.CreateNsp(ctx, switchFs);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ctx.Options.SaveOutDir != null)
|
|
|
|
|
{
|
|
|
|
|
ExportSdSaves(ctx, switchFs);
|
|
|
|
|
}
|
2018-10-13 00:52:15 +02:00
|
|
|
|
|
|
|
|
|
if (ctx.Options.Validate)
|
|
|
|
|
{
|
|
|
|
|
ValidateSwitchFs(ctx, switchFs);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void ValidateSwitchFs(Context ctx, SwitchFs switchFs)
|
|
|
|
|
{
|
|
|
|
|
if (ctx.Options.TitleId != 0)
|
|
|
|
|
{
|
|
|
|
|
ulong id = ctx.Options.TitleId;
|
|
|
|
|
|
|
|
|
|
if (!switchFs.Titles.TryGetValue(id, out Title title))
|
|
|
|
|
{
|
|
|
|
|
ctx.Logger.LogMessage($"Could not find title {id:X16}");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ValidateTitle(ctx, title, "");
|
|
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach (Application app in switchFs.Applications.Values)
|
|
|
|
|
{
|
|
|
|
|
ctx.Logger.LogMessage($"Checking {app.Name}...");
|
|
|
|
|
|
|
|
|
|
Title mainTitle = app.Patch ?? app.Main;
|
|
|
|
|
|
|
|
|
|
if (mainTitle != null)
|
|
|
|
|
{
|
|
|
|
|
ValidateTitle(ctx, mainTitle, "Main title");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach (Title title in app.AddOnContent)
|
|
|
|
|
{
|
|
|
|
|
ValidateTitle(ctx, title, "Add-on content");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void ValidateTitle(Context ctx, Title title, string caption)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
ctx.Logger.LogMessage($" {caption} {title.Id:x16}");
|
|
|
|
|
|
|
|
|
|
foreach (Nca nca in title.Ncas)
|
|
|
|
|
{
|
|
|
|
|
ctx.Logger.LogMessage($" {nca.Header.ContentType.ToString()}");
|
|
|
|
|
|
|
|
|
|
Validity validity = nca.VerifyNca(ctx.Logger, true);
|
|
|
|
|
|
|
|
|
|
ctx.Logger.LogMessage($" {validity.ToString()}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
ctx.Logger.LogMessage($"Error processing title {title.Id:x16}:\n{ex.Message}");
|
|
|
|
|
}
|
2018-09-13 03:28:50 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void SaveTitle(Context ctx, SwitchFs switchFs)
|
|
|
|
|
{
|
2018-10-03 00:25:58 +02:00
|
|
|
|
ulong id = ctx.Options.TitleId;
|
2018-09-13 03:28:50 +02:00
|
|
|
|
if (id == 0)
|
|
|
|
|
{
|
|
|
|
|
ctx.Logger.LogMessage("Title ID must be specified to save title");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-03 00:25:58 +02:00
|
|
|
|
if (!switchFs.Titles.TryGetValue(id, out Title title))
|
2018-09-13 03:28:50 +02:00
|
|
|
|
{
|
|
|
|
|
ctx.Logger.LogMessage($"Could not find title {id:X16}");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-03 00:25:58 +02:00
|
|
|
|
string saveDir = Path.Combine(ctx.Options.OutDir, $"{title.Id:X16}v{title.Version.Version}");
|
2018-09-13 03:28:50 +02:00
|
|
|
|
Directory.CreateDirectory(saveDir);
|
|
|
|
|
|
2018-10-03 00:25:58 +02:00
|
|
|
|
foreach (Nca nca in title.Ncas)
|
2018-09-13 03:28:50 +02:00
|
|
|
|
{
|
2018-10-03 00:25:58 +02:00
|
|
|
|
Stream stream = nca.GetStream();
|
|
|
|
|
string outFile = Path.Combine(saveDir, nca.Filename);
|
2018-09-13 03:28:50 +02:00
|
|
|
|
ctx.Logger.LogMessage(nca.Filename);
|
|
|
|
|
using (var outStream = new FileStream(outFile, FileMode.Create, FileAccess.ReadWrite))
|
|
|
|
|
{
|
|
|
|
|
stream.CopyStream(outStream, stream.Length, ctx.Logger);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void ListTitles(SwitchFs sdfs)
|
|
|
|
|
{
|
2018-10-03 00:25:58 +02:00
|
|
|
|
foreach (Title title in sdfs.Titles.Values.OrderBy(x => x.Id))
|
2018-09-13 03:28:50 +02:00
|
|
|
|
{
|
|
|
|
|
Console.WriteLine($"{title.Name} {title.Control?.DisplayVersion}");
|
|
|
|
|
Console.WriteLine($"{title.Id:X16} v{title.Version.Version} ({title.Version}) {title.Metadata.Type}");
|
|
|
|
|
|
2018-10-03 00:25:58 +02:00
|
|
|
|
foreach (CnmtContentEntry content in title.Metadata.ContentEntries)
|
2018-09-13 03:28:50 +02:00
|
|
|
|
{
|
|
|
|
|
Console.WriteLine(
|
|
|
|
|
$" {content.NcaId.ToHexString()}.nca {content.Type} {Util.GetBytesReadable(content.Size)}");
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-03 00:25:58 +02:00
|
|
|
|
foreach (Nca nca in title.Ncas)
|
2018-09-13 03:28:50 +02:00
|
|
|
|
{
|
|
|
|
|
Console.WriteLine($" {nca.HasRightsId} {nca.NcaId} {nca.Header.ContentType}");
|
|
|
|
|
|
2018-10-03 00:25:58 +02:00
|
|
|
|
foreach (NcaSection sect in nca.Sections.Where(x => x != null))
|
2018-09-13 03:28:50 +02:00
|
|
|
|
{
|
2018-10-09 04:04:39 +02:00
|
|
|
|
Console.WriteLine($" {sect.SectionNum} {sect.Type} {sect.Header.EncryptionType} {sect.MasterHashValidity}");
|
2018-09-13 03:28:50 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Console.WriteLine("");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static string ListApplications(SwitchFs sdfs)
|
|
|
|
|
{
|
|
|
|
|
var sb = new StringBuilder();
|
|
|
|
|
|
2018-10-03 00:25:58 +02:00
|
|
|
|
foreach (Application app in sdfs.Applications.Values.OrderBy(x => x.Name))
|
2018-09-13 03:28:50 +02:00
|
|
|
|
{
|
|
|
|
|
sb.AppendLine($"{app.Name} v{app.DisplayVersion}");
|
|
|
|
|
|
|
|
|
|
if (app.Main != null)
|
|
|
|
|
{
|
|
|
|
|
sb.AppendLine($"Software: {Util.GetBytesReadable(app.Main.GetSize())}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (app.Patch != null)
|
|
|
|
|
{
|
|
|
|
|
sb.AppendLine($"Update Data: {Util.GetBytesReadable(app.Patch.GetSize())}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (app.AddOnContent.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
sb.AppendLine($"DLC: {Util.GetBytesReadable(app.AddOnContent.Sum(x => x.GetSize()))}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (app.Nacp?.UserTotalSaveDataSize > 0)
|
|
|
|
|
sb.AppendLine($"User save: {Util.GetBytesReadable(app.Nacp.UserTotalSaveDataSize)}");
|
|
|
|
|
if (app.Nacp?.DeviceTotalSaveDataSize > 0)
|
|
|
|
|
sb.AppendLine($"System save: {Util.GetBytesReadable(app.Nacp.DeviceTotalSaveDataSize)}");
|
|
|
|
|
if (app.Nacp?.BcatDeliveryCacheStorageSize > 0)
|
|
|
|
|
sb.AppendLine($"BCAT save: {Util.GetBytesReadable(app.Nacp.BcatDeliveryCacheStorageSize)}");
|
|
|
|
|
|
|
|
|
|
sb.AppendLine();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return sb.ToString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void ExportSdSaves(Context ctx, SwitchFs switchFs)
|
|
|
|
|
{
|
2018-10-03 00:25:58 +02:00
|
|
|
|
foreach (KeyValuePair<string, Savefile> save in switchFs.Saves)
|
2018-09-13 03:28:50 +02:00
|
|
|
|
{
|
2018-10-03 00:25:58 +02:00
|
|
|
|
string outDir = Path.Combine(ctx.Options.SaveOutDir, save.Key);
|
2018-09-13 03:28:50 +02:00
|
|
|
|
save.Value.Extract(outDir, ctx.Logger);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|