mirror of
https://github.com/Thealexbarney/LibHac.git
synced 2024-11-14 10:49:41 +01:00
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:
parent
a002d92b13
commit
4362b7be53
8 changed files with 243 additions and 144 deletions
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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:");
|
||||||
|
|
145
src/hactoolnet/ProcessAppFs.cs
Normal file
145
src/hactoolnet/ProcessAppFs.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue