LibHac/hactoolnet/Program.cs

655 lines
24 KiB
C#
Raw Normal View History

2018-06-21 18:16:51 +02:00
using System;
using System.IO;
2018-06-21 23:03:58 +02:00
using System.Linq;
using System.Reflection;
2018-07-03 04:21:35 +02:00
using System.Text;
2018-08-31 17:47:11 +02:00
using LibHac;
using LibHac.Savefile;
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:
2018-08-28 19:33:24 +02:00
ProcessRomFs(ctx);
2018-07-02 22:12:41 +02:00
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;
case FileType.Save:
ProcessSave(ctx);
break;
2018-08-12 22:45:10 +02:00
case FileType.Xci:
ProcessXci(ctx);
break;
2018-08-28 00:40:06 +02:00
case FileType.Keygen:
ProcessKeygen(ctx);
break;
2018-07-02 22:12:41 +02:00
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
if (ctx.Options.RomfsOutDir != null || ctx.Options.RomfsOut != null)
2018-07-07 22:45:06 +02:00
{
2018-08-30 23:18:27 +02:00
NcaSection section = nca.Sections.FirstOrDefault(x => x?.Type == SectionType.Romfs || x?.Type == SectionType.Bktr);
2018-07-07 22:45:06 +02:00
if (section == null)
{
ctx.Logger.LogMessage("NCA has no RomFS section");
return;
}
if (section.Type == SectionType.Bktr && ctx.Options.BaseNca == null)
2018-07-07 22:45:06 +02:00
{
ctx.Logger.LogMessage("Cannot save BKTR section without base RomFS");
return;
}
2018-07-07 22:45:06 +02:00
if (ctx.Options.RomfsOut != null)
{
nca.ExportSection(section.SectionNum, ctx.Options.RomfsOut, ctx.Options.Raw, ctx.Logger);
2018-07-07 22:45:06 +02:00
}
if (ctx.Options.RomfsOutDir != null)
2018-07-07 22:45:06 +02:00
{
var romfs = new Romfs(nca.OpenSection(section.SectionNum, false));
romfs.Extract(ctx.Options.RomfsOutDir, ctx.Logger);
}
}
if (ctx.Options.ExefsOutDir != null || ctx.Options.ExefsOut != null)
2018-08-16 00:33:45 +02:00
{
NcaSection section = nca.Sections.FirstOrDefault(x => x.IsExefs);
if (section == null)
{
ctx.Logger.LogMessage("Could not find an ExeFS section");
return;
}
if (ctx.Options.ExefsOut != null)
{
nca.ExportSection(section.SectionNum, ctx.Options.ExefsOut, ctx.Options.Raw, ctx.Logger);
}
if (ctx.Options.ExefsOutDir != null)
{
nca.ExtractSection(section.SectionNum, ctx.Options.ExefsOutDir, ctx.Logger);
}
2018-08-16 00:33:45 +02:00
}
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)
{
2018-08-31 18:07:41 +02:00
var switchFs = new SwitchFs(ctx.Keyset, new FileSystem(ctx.Options.InFile));
2018-07-03 04:21:35 +02:00
if (ctx.Options.ListTitles)
{
ListTitles(switchFs);
}
if (ctx.Options.ListApps)
{
ctx.Logger.LogMessage(ListApplications(switchFs));
}
if (ctx.Options.ExefsOutDir != null || ctx.Options.ExefsOut != null)
{
var id = ctx.Options.TitleId;
if (id == 0)
{
ctx.Logger.LogMessage("Title ID must be specified to dump ExeFS");
return;
}
if (!switchFs.Titles.TryGetValue(id, out var title))
{
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-08-23 18:28:45 +02:00
var section = title.MainNca.Sections.FirstOrDefault(x => x.IsExefs);
if (section == null)
{
ctx.Logger.LogMessage($"Main NCA for title {id:X16} has no ExeFS section");
return;
}
if (ctx.Options.ExefsOutDir != null)
{
title.MainNca.ExtractSection(section.SectionNum, ctx.Options.ExefsOutDir, ctx.Logger);
}
if (ctx.Options.ExefsOut != null)
{
title.MainNca.ExportSection(section.SectionNum, ctx.Options.ExefsOut, ctx.Options.Raw, ctx.Logger);
}
}
if (ctx.Options.RomfsOutDir != null || ctx.Options.RomfsOut != null)
2018-07-03 04:21:35 +02:00
{
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 || x?.Type == SectionType.Bktr);
2018-07-06 04:37:46 +02:00
if (section == null)
2018-07-03 04:21:35 +02:00
{
ctx.Logger.LogMessage($"Main NCA for title {id:X16} has no RomFS section");
2018-07-03 04:21:35 +02:00
return;
}
if (ctx.Options.RomfsOutDir != null)
{
var romfs = new Romfs(title.MainNca.OpenSection(section.SectionNum, false));
romfs.Extract(ctx.Options.RomfsOutDir, ctx.Logger);
}
if (ctx.Options.RomfsOut != null)
{
title.MainNca.ExportSection(section.SectionNum, ctx.Options.RomfsOut, ctx.Options.Raw, ctx.Logger);
}
2018-07-03 04:21:35 +02:00
}
2018-07-05 23:37:30 +02:00
if (ctx.Options.OutDir != null)
{
SaveTitle(ctx, switchFs);
}
if (ctx.Options.NspOut != null)
{
CreateNsp(ctx, switchFs);
}
2018-07-03 04:21:35 +02:00
}
2018-08-12 22:45:10 +02:00
private static void ProcessXci(Context ctx)
{
using (var file = new FileStream(ctx.Options.InFile, FileMode.Open, FileAccess.Read))
{
var xci = new Xci(ctx.Keyset, file);
if (ctx.Options.RootDir != null)
{
xci.RootPartition?.Extract(ctx.Options.RootDir, ctx.Logger);
}
if (ctx.Options.UpdateDir != null)
{
xci.UpdatePartition?.Extract(ctx.Options.UpdateDir, ctx.Logger);
}
if (ctx.Options.NormalDir != null)
{
xci.NormalPartition?.Extract(ctx.Options.NormalDir, ctx.Logger);
}
if (ctx.Options.SecureDir != null)
{
xci.SecurePartition?.Extract(ctx.Options.SecureDir, ctx.Logger);
}
if (ctx.Options.LogoDir != null)
{
xci.LogoPartition?.Extract(ctx.Options.LogoDir, ctx.Logger);
}
if (ctx.Options.OutDir != null && xci.RootPartition != null)
{
var root = xci.RootPartition;
2018-08-15 01:38:32 +02:00
if (root == null)
{
ctx.Logger.LogMessage("Could not find root partition");
return;
}
foreach (var sub in root.Files)
{
var subPfs = new Pfs(root.OpenFile(sub));
var subDir = Path.Combine(ctx.Options.OutDir, sub.Name);
subPfs.Extract(subDir, ctx.Logger);
}
}
2018-08-15 01:38:32 +02:00
if (ctx.Options.ExefsOutDir != null || ctx.Options.ExefsOut != null)
2018-08-15 01:38:32 +02:00
{
var mainNca = GetXciMainNca(xci, ctx);
if (mainNca == null)
2018-08-15 01:38:32 +02:00
{
ctx.Logger.LogMessage("Could not find Program NCA");
2018-08-15 01:38:32 +02:00
return;
}
2018-08-23 18:28:45 +02:00
var exefsSection = mainNca.Sections.FirstOrDefault(x => x.IsExefs);
if (exefsSection == null)
{
ctx.Logger.LogMessage("NCA has no ExeFS section");
return;
}
2018-08-15 01:38:32 +02:00
if (ctx.Options.ExefsOutDir != null)
2018-08-15 01:38:32 +02:00
{
mainNca.ExtractSection(exefsSection.SectionNum, ctx.Options.ExefsOutDir, ctx.Logger);
}
2018-08-15 01:38:32 +02:00
if (ctx.Options.ExefsOut != null)
{
mainNca.ExportSection(exefsSection.SectionNum, ctx.Options.ExefsOut, ctx.Options.Raw, ctx.Logger);
2018-08-15 01:38:32 +02:00
}
}
if (ctx.Options.RomfsOutDir != null || ctx.Options.RomfsOut != null)
{
var mainNca = GetXciMainNca(xci, ctx);
2018-08-15 01:38:32 +02:00
if (mainNca == null)
{
ctx.Logger.LogMessage("Could not find Program NCA");
return;
}
var romfsSection = mainNca.Sections.FirstOrDefault(x => x.Type == SectionType.Romfs);
if (romfsSection == null)
{
ctx.Logger.LogMessage("NCA has no RomFS section");
return;
}
2018-08-22 16:58:36 +02:00
if (ctx.Options.RomfsOutDir != null)
{
var romfs = new Romfs(mainNca.OpenSection(romfsSection.SectionNum, false));
romfs.Extract(ctx.Options.RomfsOutDir, ctx.Logger);
}
if (ctx.Options.RomfsOut != null)
{
mainNca.ExportSection(romfsSection.SectionNum, ctx.Options.RomfsOut, ctx.Options.Raw, ctx.Logger);
}
2018-08-15 01:38:32 +02:00
}
2018-08-28 19:33:24 +02:00
}
}
private static void ProcessRomFs(Context ctx)
{
using (var file = new FileStream(ctx.Options.InFile, FileMode.Open, FileAccess.Read))
{
var romfs = new Romfs(file);
ProcessRomFs(ctx, romfs);
}
}
private static void ProcessRomFs(Context ctx, Romfs romfs)
{
if (ctx.Options.ListRomFs)
2018-08-28 19:33:24 +02:00
{
foreach (var romfsFile in romfs.Files)
2018-08-28 19:33:24 +02:00
{
ctx.Logger.LogMessage(romfsFile.FullPath);
2018-08-28 19:33:24 +02:00
}
}
2018-08-28 19:33:24 +02:00
if (ctx.Options.RomfsOut != null)
{
using (var outFile = new FileStream(ctx.Options.RomfsOut, FileMode.Create, FileAccess.ReadWrite))
2018-08-28 19:33:24 +02:00
{
var romfsStream = romfs.OpenRawStream();
romfsStream.CopyStream(outFile, romfsStream.Length, ctx.Logger);
2018-08-28 19:33:24 +02:00
}
}
2018-08-28 19:33:24 +02:00
if (ctx.Options.RomfsOutDir != null)
{
romfs.Extract(ctx.Options.RomfsOutDir, ctx.Logger);
2018-08-12 22:45:10 +02:00
}
}
private static Nca GetXciMainNca(Xci xci, Context ctx)
{
if (xci.SecurePartition == null)
{
ctx.Logger.LogMessage("Could not find secure partition");
return null;
}
Nca mainNca = null;
foreach (var fileEntry in xci.SecurePartition.Files.Where(x => x.Name.EndsWith(".nca")))
{
var ncaStream = xci.SecurePartition.OpenFile(fileEntry);
var nca = new Nca(ctx.Keyset, ncaStream, true);
if (nca.Header.ContentType == ContentType.Program)
{
mainNca = nca;
}
}
return mainNca;
}
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");
2018-07-09 18:47:32 +02:00
var homeTitleKeyFile = Path.Combine(home, ".switch", "title.keys");
var homeConsoleKeyFile = Path.Combine(home, ".switch", "console.keys");
2018-07-03 04:21:35 +02:00
var keyFile = ctx.Options.Keyfile;
var titleKeyFile = ctx.Options.TitleKeyFile;
2018-07-09 18:47:32 +02:00
var consoleKeyFile = ctx.Options.ConsoleKeyFile;
2018-07-03 04:21:35 +02:00
if (keyFile == null && File.Exists(homeKeyFile))
{
keyFile = homeKeyFile;
}
if (titleKeyFile == null && File.Exists(homeTitleKeyFile))
{
titleKeyFile = homeTitleKeyFile;
}
2018-07-09 18:47:32 +02:00
if (consoleKeyFile == null && File.Exists(homeConsoleKeyFile))
{
consoleKeyFile = homeConsoleKeyFile;
}
ctx.Keyset = ExternalKeys.ReadKeyFile(keyFile, titleKeyFile, consoleKeyFile, ctx.Logger);
2018-07-03 04:21:35 +02:00
if (ctx.Options.SdSeed != null)
{
ctx.Keyset.SetSdSeed(ctx.Options.SdSeed.ToBytes());
}
2018-07-02 22:12:41 +02:00
}
private static void ProcessSave(Context ctx)
{
using (var file = new FileStream(ctx.Options.InFile, FileMode.Open, FileAccess.Read))
{
var save = new Savefile(file, ctx.Logger);
2018-07-24 21:51:52 +02:00
if (ctx.Options.OutDir != null)
{
2018-07-24 21:51:52 +02:00
save.Extract(ctx.Options.OutDir, ctx.Logger);
}
2018-07-24 21:51:52 +02:00
if (ctx.Options.DebugOutDir != null)
{
2018-07-24 21:51:52 +02:00
var dir = ctx.Options.DebugOutDir;
Directory.CreateDirectory(dir);
File.WriteAllBytes(Path.Combine(dir, "L1_0_MasterHashA"), save.Header.MasterHashA);
File.WriteAllBytes(Path.Combine(dir, "L1_1_MasterHashB"), save.Header.MasterHashB);
File.WriteAllBytes(Path.Combine(dir, "L1_2_DuplexMasterA"), save.Header.DuplexMasterA);
File.WriteAllBytes(Path.Combine(dir, "L1_3_DuplexMasterB"), save.Header.DuplexMasterB);
save.DuplexL1A.WriteAllBytes(Path.Combine(dir, "L0_0_DuplexL1A"), ctx.Logger);
save.DuplexL1B.WriteAllBytes(Path.Combine(dir, "L0_1_DuplexL1B"), ctx.Logger);
save.DuplexDataA.WriteAllBytes(Path.Combine(dir, "L0_2_DuplexDataA"), ctx.Logger);
save.DuplexDataB.WriteAllBytes(Path.Combine(dir, "L0_3_DuplexDataB"), ctx.Logger);
save.JournalData.WriteAllBytes(Path.Combine(dir, "L0_4_JournalData"), ctx.Logger);
save.JournalTable.WriteAllBytes(Path.Combine(dir, "L1_0_JournalTable"), ctx.Logger);
save.JournalBitmapUpdatedPhysical.WriteAllBytes(Path.Combine(dir, "L1_1_JournalBitmapUpdatedPhysical"), ctx.Logger);
save.JournalBitmapUpdatedVirtual.WriteAllBytes(Path.Combine(dir, "L1_2_JournalBitmapUpdatedVirtual"), ctx.Logger);
save.JournalBitmapUnassigned.WriteAllBytes(Path.Combine(dir, "L1_3_JournalBitmapUnassigned"), ctx.Logger);
save.JournalLayer1Hash.WriteAllBytes(Path.Combine(dir, "L1_4_Layer1Hash"), ctx.Logger);
save.JournalLayer2Hash.WriteAllBytes(Path.Combine(dir, "L1_5_Layer2Hash"), ctx.Logger);
save.JournalLayer3Hash.WriteAllBytes(Path.Combine(dir, "L1_6_Layer3Hash"), ctx.Logger);
save.JournalFat.WriteAllBytes(Path.Combine(dir, "L1_7_FAT"), ctx.Logger);
save.JournalStreamSource.CreateStream().WriteAllBytes(Path.Combine(dir, "L2_0_SaveData"), ctx.Logger);
}
}
}
2018-08-28 00:40:06 +02:00
private static void ProcessKeygen(Context ctx)
{
Console.WriteLine(ExternalKeys.PrintKeys(ctx.Keyset));
}
2018-07-05 23:37:30 +02:00
// For running random stuff
// ReSharper disable once UnusedParameter.Local
private static void CustomTask(Context ctx)
{
}
2018-08-31 18:07:41 +02:00
private static void SaveTitle(Context ctx, SwitchFs switchFs)
{
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-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);
foreach (var nca in title.Ncas)
{
var stream = nca.GetStream();
2018-07-05 23:37:30 +02:00
var outFile = Path.Combine(saveDir, nca.Filename);
ctx.Logger.LogMessage(nca.Filename);
using (var outStream = new FileStream(outFile, FileMode.Create, FileAccess.ReadWrite))
{
stream.CopyStream(outStream, stream.Length, ctx.Logger);
}
}
}
2018-08-31 18:07:41 +02:00
private static void CreateNsp(Context ctx, SwitchFs switchFs)
{
var id = ctx.Options.TitleId;
if (id == 0)
{
ctx.Logger.LogMessage("Title ID must be specified to save title");
return;
}
if (!switchFs.Titles.TryGetValue(id, out var title))
{
ctx.Logger.LogMessage($"Could not find title {id:X16}");
return;
}
var builder = new Pfs0Builder();
foreach (var nca in title.Ncas)
{
builder.AddFile(nca.Filename, nca.GetStream());
}
var ticket = new Ticket
{
SignatureType = TicketSigType.Rsa2048Sha256,
Signature = new byte[0x200],
Issuer = "Root-CA00000003-XS00000020",
FormatVersion = 2,
RightsId = title.MainNca.Header.RightsId,
TitleKeyBlock = title.MainNca.TitleKey,
CryptoType = title.MainNca.Header.CryptoType2,
SectHeaderOffset = 0x2C0
};
var ticketBytes = ticket.GetBytes();
builder.AddFile($"{ticket.RightsId.ToHexString()}.tik", new MemoryStream(ticketBytes));
var thisAssembly = Assembly.GetExecutingAssembly();
var cert = thisAssembly.GetManifestResourceStream("hactoolnet.CA00000003_XS00000020");
builder.AddFile($"{ticket.RightsId.ToHexString()}.cert", cert);
using (var outStream = new FileStream(ctx.Options.NspOut, FileMode.Create, FileAccess.ReadWrite))
{
builder.Build(outStream, ctx.Logger);
}
}
2018-08-31 18:07:41 +02:00
static void ListTitles(SwitchFs 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-08-02 23:40:08 +02:00
Console.WriteLine($"{title.Name} {title.Control?.DisplayVersion}");
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(
$" {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
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))
{
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-08-31 18:07:41 +02:00
static string ListApplications(SwitchFs 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-08-02 23:40:08 +02:00
if (app.Nacp?.BcatDeliveryCacheStorageSize > 0)
sb.AppendLine($"BCAT save: {Util.GetBytesReadable(app.Nacp.BcatDeliveryCacheStorageSize)}");
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
}
}