hactoolnet: Improve XCI and PFS0 handling

- Automatically import title keys from tickets.
- Automatically apply updates when reading an XCI or NSP.
- Allow listing all contained NCAs, programs or applications.
- Allow specifying the title when an XCI or NSP contains more than one.
- Allow extracting ExeFS or RomFS directly from an NSP.
This commit is contained in:
Alex Barney 2022-11-06 14:44:44 -07:00
parent a002d92b13
commit 4362b7be53
8 changed files with 243 additions and 144 deletions

View file

@ -74,11 +74,16 @@ XCI options:
--securedir <dir> Specify secure XCI directory path. --securedir <dir> Specify secure XCI directory path.
--logodir <dir> Specify logo XCI directory path. --logodir <dir> Specify logo XCI directory path.
--outdir <dir> Specify XCI directory path. --outdir <dir> Specify XCI directory path.
--nspout <file> Specify file for the created NSP.
Partition FS and XCI options:
--exefs <file> Specify main ExeFS file path. --exefs <file> Specify main ExeFS file path.
--exefsdir <dir> Specify main ExeFS directory path. --exefsdir <dir> Specify main ExeFS directory path.
--romfs <file> Specify main RomFS file path. --romfs <file> Specify main RomFS file path.
--romfsdir <dir> Specify main RomFS directory path. --romfsdir <dir> Specify main RomFS directory path.
--nspout <file> Specify file for the created NSP. --listapps List application info.
--listtitles List title info for all titles.
--listncas List info for all NCAs.
--title <title id> Specify title ID to use.
Package1 options: Package1 options:
--outdir <dir> Specify Package1 directory path. --outdir <dir> Specify Package1 directory path.
Package2 options: Package2 options:

View file

@ -1,4 +1,6 @@
namespace LibHac.Ncm; using System;
namespace LibHac.Ncm;
public enum ContentType : byte public enum ContentType : byte
{ {
@ -24,11 +26,13 @@ public enum ContentMetaType : byte
Delta = 0x83 Delta = 0x83
} }
[Flags]
public enum ContentMetaAttribute : byte public enum ContentMetaAttribute : byte
{ {
None = 0, None = 0,
IncludesExFatDriver = 1, IncludesExFatDriver = 1 << 0,
Rebootless = 2 Rebootless = 1 << 1,
Compacted = 1 << 2,
} }
public enum UpdateType : byte public enum UpdateType : byte

View file

@ -15,6 +15,7 @@ public class Cnmt
public int TableOffset { get; } public int TableOffset { get; }
public int ContentEntryCount { get; } public int ContentEntryCount { get; }
public int MetaEntryCount { get; } public int MetaEntryCount { get; }
public ContentMetaAttribute ContentMetaAttributes { get; }
public CnmtContentEntry[] ContentEntries { get; } public CnmtContentEntry[] ContentEntries { get; }
public CnmtContentMetaEntry[] MetaEntries { get; } public CnmtContentMetaEntry[] MetaEntries { get; }
@ -42,11 +43,12 @@ public class Cnmt
TableOffset = reader.ReadUInt16(); TableOffset = reader.ReadUInt16();
ContentEntryCount = reader.ReadUInt16(); ContentEntryCount = reader.ReadUInt16();
MetaEntryCount = reader.ReadUInt16(); MetaEntryCount = reader.ReadUInt16();
ContentMetaAttributes = (ContentMetaAttribute)reader.ReadByte();
// Old, pre-release cnmt files don't have the "required system version" field. // Old, pre-release cnmt files don't have the "required system version" field.
// Try to detect this by reading the padding after that field. // Try to detect this by reading the padding after that field.
// The old format usually contains hashes there. // The old format usually contains hashes there.
file.Position += 8; file.Position += 7;
int padding = reader.ReadInt32(); int padding = reader.ReadInt32();
bool isOldCnmtFormat = padding != 0; bool isOldCnmtFormat = padding != 0;

View file

@ -249,11 +249,16 @@ internal static class CliParser
sb.AppendLine(" --securedir <dir> Specify secure XCI directory path."); sb.AppendLine(" --securedir <dir> Specify secure XCI directory path.");
sb.AppendLine(" --logodir <dir> Specify logo XCI directory path."); sb.AppendLine(" --logodir <dir> Specify logo XCI directory path.");
sb.AppendLine(" --outdir <dir> Specify XCI directory path."); sb.AppendLine(" --outdir <dir> Specify XCI directory path.");
sb.AppendLine(" --nspout <file> Specify file for the created NSP.");
sb.AppendLine("Partition FS and XCI options:");
sb.AppendLine(" --exefs <file> Specify main ExeFS file path."); sb.AppendLine(" --exefs <file> Specify main ExeFS file path.");
sb.AppendLine(" --exefsdir <dir> Specify main ExeFS directory path."); sb.AppendLine(" --exefsdir <dir> Specify main ExeFS directory path.");
sb.AppendLine(" --romfs <file> Specify main RomFS file path."); sb.AppendLine(" --romfs <file> Specify main RomFS file path.");
sb.AppendLine(" --romfsdir <dir> Specify main RomFS directory path."); sb.AppendLine(" --romfsdir <dir> Specify main RomFS directory path.");
sb.AppendLine(" --nspout <file> Specify file for the created NSP."); sb.AppendLine(" --listapps List application info.");
sb.AppendLine(" --listtitles List title info for all titles.");
sb.AppendLine(" --listncas List info for all NCAs.");
sb.AppendLine(" --title <title id> Specify title ID to use.");
sb.AppendLine("Package1 options:"); sb.AppendLine("Package1 options:");
sb.AppendLine(" --outdir <dir> Specify Package1 directory path."); sb.AppendLine(" --outdir <dir> Specify Package1 directory path.");
sb.AppendLine("Package2 options:"); sb.AppendLine("Package2 options:");

View file

@ -0,0 +1,145 @@
using System.Linq;
using LibHac.Common;
using LibHac.Fs;
using LibHac.Fs.Fsa;
using LibHac.FsSystem;
using LibHac.Ncm;
using LibHac.Spl;
using LibHac.Tools.Es;
using LibHac.Tools.Fs;
using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
namespace hactoolnet;
internal static class ProcessAppFs
{
private static void ImportTickets(Context ctx, IFileSystem fileSystem)
{
foreach (DirectoryEntryEx entry in fileSystem.EnumerateEntries("*.tik", SearchOptions.Default))
{
using var tikFile = new UniqueRef<IFile>();
fileSystem.OpenFile(ref tikFile.Ref(), entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
var ticket = new Ticket(tikFile.Get.AsStream());
if (ticket.TitleKeyType != TitleKeyType.Common)
continue;
if (ticket.RightsId.IsZeros())
continue;
var rightsId = SpanHelpers.AsStruct<RightsId>(ticket.RightsId);
var accessKey = SpanHelpers.AsStruct<AccessKey>(ticket.TitleKeyBlock);
ctx.KeySet.ExternalKeySet.Add(rightsId, accessKey).ThrowIfFailure();
}
}
public static void Process(Context ctx, IFileSystem fileSystem)
{
ImportTickets(ctx, fileSystem);
SwitchFs switchFs = SwitchFs.OpenNcaDirectory(ctx.KeySet, fileSystem);
if (ctx.Options.ListNcas)
{
ctx.Logger.LogMessage(ProcessSwitchFs.ListNcas(switchFs));
}
if (ctx.Options.ListTitles)
{
ctx.Logger.LogMessage(ProcessSwitchFs.ListTitles(switchFs));
}
if (ctx.Options.ListApps)
{
ctx.Logger.LogMessage(ProcessSwitchFs.ListApplications(switchFs));
}
ulong id = GetTargetProgramId(ctx, switchFs);
if (id == ulong.MaxValue)
{
ctx.Logger.LogMessage("Title ID must be specified to dump ExeFS or RomFS");
return;
}
if (!switchFs.Titles.TryGetValue(id, out Title 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;
}
if (title.Metadata?.ContentMetaAttributes.HasFlag(ContentMetaAttribute.Compacted) == true)
{
ctx.Logger.LogMessage($"Cannot extract compacted NCAs");
return;
}
if (ctx.Options.ExefsOutDir != null || ctx.Options.ExefsOut != null)
{
if (!title.MainNca.Nca.SectionExists(NcaSectionType.Code))
{
ctx.Logger.LogMessage($"Main NCA for title {id:X16} has no ExeFS section");
return;
}
if (ctx.Options.ExefsOutDir != null)
{
IFileSystem fs = title.MainNca.OpenFileSystem(NcaSectionType.Code, ctx.Options.IntegrityLevel);
fs.Extract(ctx.Options.ExefsOutDir, ctx.Logger);
}
if (ctx.Options.ExefsOut != null)
{
title.MainNca.Nca.ExportSection(NcaSectionType.Code, ctx.Options.ExefsOut, ctx.Options.Raw, ctx.Options.IntegrityLevel, ctx.Logger);
}
}
if (ctx.Options.RomfsOutDir != null || ctx.Options.RomfsOut != null || ctx.Options.ListRomFs)
{
if (!title.MainNca.Nca.SectionExists(NcaSectionType.Data))
{
ctx.Logger.LogMessage($"Main NCA for title {id:X16} has no RomFS section");
return;
}
ProcessRomfs.Process(ctx, title.MainNca.OpenStorage(NcaSectionType.Data, ctx.Options.IntegrityLevel));
}
if (ctx.Options.NspOut != null)
{
ProcessPfs.CreateNsp(ctx, switchFs);
}
}
private static ulong GetTargetProgramId(Context ctx, SwitchFs switchFs)
{
ulong id = ctx.Options.TitleId;
if (id != 0)
{
return id;
}
if (switchFs.Applications.Count != 1)
{
return ulong.MaxValue;
}
ulong applicationId = switchFs.Applications.Values.First().TitleId;
ulong updateId = applicationId | 0x800ul;
if (switchFs.Titles.ContainsKey(updateId))
{
return updateId;
}
return applicationId;
}
}

View file

@ -1,4 +1,5 @@
using System.IO; using System.IO;
using System.Linq;
using System.Reflection; using System.Reflection;
using System.Text; using System.Text;
using LibHac.Fs; using LibHac.Fs;
@ -24,6 +25,11 @@ internal static class ProcessPfs
{ {
pfs.Extract(ctx.Options.OutDir, ctx.Logger); pfs.Extract(ctx.Options.OutDir, ctx.Logger);
} }
if (pfs.EnumerateEntries("*.nca", SearchOptions.Default).Any())
{
ProcessAppFs.Process(ctx, pfs);
}
} }
} }

View file

@ -234,7 +234,7 @@ internal static class ProcessSwitchFs
} }
} }
static string ListTitles(SwitchFs sdfs) public static string ListTitles(SwitchFs sdfs)
{ {
var table = new TableBuilder("Title ID", "Version", "", "Type", "Size", "Display Version", "Name"); var table = new TableBuilder("Title ID", "Version", "", "Type", "Size", "Display Version", "Name");
@ -252,7 +252,7 @@ internal static class ProcessSwitchFs
return table.Print(); return table.Print();
} }
static string ListNcas(SwitchFs sdfs) public static string ListNcas(SwitchFs sdfs)
{ {
var table = new TableBuilder("NCA ID", "Type", "Title ID"); var table = new TableBuilder("NCA ID", "Type", "Title ID");
@ -264,7 +264,7 @@ internal static class ProcessSwitchFs
return table.Print(); return table.Print();
} }
static string ListApplications(SwitchFs sdfs) public static string ListApplications(SwitchFs sdfs)
{ {
var sb = new StringBuilder(); var sb = new StringBuilder();

View file

@ -8,7 +8,6 @@ using LibHac.FsSystem;
using LibHac.Gc.Impl; using LibHac.Gc.Impl;
using LibHac.Tools.Fs; using LibHac.Tools.Fs;
using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
namespace hactoolnet; namespace hactoolnet;
@ -65,78 +64,11 @@ internal static class ProcessXci
} }
} }
if (ctx.Options.ExefsOutDir != null || ctx.Options.ExefsOut != null) if (xci.HasPartition(XciPartitionType.Secure))
{ {
Nca mainNca = GetXciMainNca(xci, ctx); ProcessAppFs.Process(ctx, xci.OpenPartition(XciPartitionType.Secure));
if (mainNca == null)
{
ctx.Logger.LogMessage("Could not find Program NCA");
return;
}
if (!mainNca.SectionExists(NcaSectionType.Code))
{
ctx.Logger.LogMessage("NCA has no ExeFS section");
return;
}
if (ctx.Options.ExefsOutDir != null)
{
mainNca.ExtractSection(NcaSectionType.Code, ctx.Options.ExefsOutDir, ctx.Options.IntegrityLevel, ctx.Logger);
}
if (ctx.Options.ExefsOut != null)
{
mainNca.ExportSection(NcaSectionType.Code, ctx.Options.ExefsOut, ctx.Options.Raw, ctx.Options.IntegrityLevel, ctx.Logger);
} }
} }
if (ctx.Options.RomfsOutDir != null || ctx.Options.RomfsOut != null || ctx.Options.ListRomFs)
{
Nca mainNca = GetXciMainNca(xci, ctx);
if (mainNca == null)
{
ctx.Logger.LogMessage("Could not find Program NCA");
return;
}
if (!mainNca.SectionExists(NcaSectionType.Data))
{
ctx.Logger.LogMessage("NCA has no RomFS section");
return;
}
ProcessRomfs.Process(ctx, mainNca.OpenStorage(NcaSectionType.Data, ctx.Options.IntegrityLevel, false));
}
}
}
private static Nca GetXciMainNca(Xci xci, Context ctx)
{
XciPartition partition = xci.OpenPartition(XciPartitionType.Secure);
if (partition == null)
{
ctx.Logger.LogMessage("Could not find secure partition");
return null;
}
Nca mainNca = null;
foreach (PartitionFileEntry fileEntry in partition.Files.Where(x => x.Name.EndsWith(".nca")))
{
IStorage ncaStorage = partition.OpenFile(fileEntry, OpenMode.Read).AsStorage();
var nca = new Nca(ctx.KeySet, ncaStorage);
if (nca.Header.ContentType == NcaContentType.Program)
{
mainNca = nca;
}
}
return mainNca;
} }
private static string Print(this Xci xci) private static string Print(this Xci xci)