diff --git a/README.md b/README.md index 5fa24b8c..de93739d 100644 --- a/README.md +++ b/README.md @@ -74,11 +74,16 @@ XCI options: --securedir Specify secure XCI directory path. --logodir Specify logo XCI directory path. --outdir Specify XCI directory path. + --nspout Specify file for the created NSP. +Partition FS and XCI options: --exefs Specify main ExeFS file path. --exefsdir Specify main ExeFS directory path. --romfs Specify main RomFS file path. --romfsdir Specify main RomFS directory path. - --nspout Specify file for the created NSP. + --listapps List application info. + --listtitles List title info for all titles. + --listncas List info for all NCAs. + --title Specify title ID to use. Package1 options: --outdir <dir> Specify Package1 directory path. Package2 options: diff --git a/src/LibHac/Ncm/ContentEnums.cs b/src/LibHac/Ncm/ContentEnums.cs index 792ef977..2923d8fd 100644 --- a/src/LibHac/Ncm/ContentEnums.cs +++ b/src/LibHac/Ncm/ContentEnums.cs @@ -1,4 +1,6 @@ -namespace LibHac.Ncm; +using System; + +namespace LibHac.Ncm; public enum ContentType : byte { @@ -24,11 +26,13 @@ public enum ContentMetaType : byte Delta = 0x83 } +[Flags] public enum ContentMetaAttribute : byte { None = 0, - IncludesExFatDriver = 1, - Rebootless = 2 + IncludesExFatDriver = 1 << 0, + Rebootless = 1 << 1, + Compacted = 1 << 2, } public enum UpdateType : byte @@ -36,4 +40,4 @@ public enum UpdateType : byte ApplyAsDelta = 0, Overwrite = 1, Create = 2 -} +} \ No newline at end of file diff --git a/src/LibHac/Tools/Ncm/Cnmt.cs b/src/LibHac/Tools/Ncm/Cnmt.cs index 28922c4e..56d234d8 100644 --- a/src/LibHac/Tools/Ncm/Cnmt.cs +++ b/src/LibHac/Tools/Ncm/Cnmt.cs @@ -15,6 +15,7 @@ public class Cnmt public int TableOffset { get; } public int ContentEntryCount { get; } public int MetaEntryCount { get; } + public ContentMetaAttribute ContentMetaAttributes { get; } public CnmtContentEntry[] ContentEntries { get; } public CnmtContentMetaEntry[] MetaEntries { get; } @@ -42,11 +43,12 @@ public class Cnmt TableOffset = reader.ReadUInt16(); ContentEntryCount = reader.ReadUInt16(); MetaEntryCount = reader.ReadUInt16(); + ContentMetaAttributes = (ContentMetaAttribute)reader.ReadByte(); // Old, pre-release cnmt files don't have the "required system version" field. // Try to detect this by reading the padding after that field. // The old format usually contains hashes there. - file.Position += 8; + file.Position += 7; int padding = reader.ReadInt32(); bool isOldCnmtFormat = padding != 0; diff --git a/src/hactoolnet/CliParser.cs b/src/hactoolnet/CliParser.cs index 1c19ac0b..74eedb42 100644 --- a/src/hactoolnet/CliParser.cs +++ b/src/hactoolnet/CliParser.cs @@ -9,70 +9,70 @@ internal static class CliParser { private static CliOption[] GetCliOptions() => new[] { - new CliOption("custom", 0, (o, _) => o.RunCustom = true), - new CliOption("intype", 't', 1, (o, a) => o.InFileType = ParseFileType(a[0])), - new CliOption("raw", 'r', 0, (o, _) => o.Raw = true), - new CliOption("verify", 'y', 0, (o, _) => o.Validate = true), - new CliOption("dev", 'd', 0, (o, _) => o.UseDevKeys = true), - new CliOption("enablehash", 'h', 0, (o, _) => o.EnableHash = true), - new CliOption("disablekeywarns", 0, (o, _) => o.DisableKeyWarns = true), - new CliOption("keyset", 'k', 1, (o, a) => o.Keyfile = a[0]), - new CliOption("titlekeys", 1, (o, a) => o.TitleKeyFile = a[0]), - new CliOption("consolekeys", 1, (o, a) => o.ConsoleKeyFile = a[0]), - new CliOption("accesslog", 1, (o, a) => o.AccessLog = a[0]), - new CliOption("resultlog", 1, (o, a) => o.ResultLog = a[0]), - new CliOption("section0", 1, (o, a) => o.SectionOut[0] = a[0]), - new CliOption("section1", 1, (o, a) => o.SectionOut[1] = a[0]), - new CliOption("section2", 1, (o, a) => o.SectionOut[2] = a[0]), - new CliOption("section3", 1, (o, a) => o.SectionOut[3] = a[0]), - new CliOption("section0dir", 1, (o, a) => o.SectionOutDir[0] = a[0]), - new CliOption("section1dir", 1, (o, a) => o.SectionOutDir[1] = a[0]), - new CliOption("section2dir", 1, (o, a) => o.SectionOutDir[2] = a[0]), - new CliOption("section3dir", 1, (o, a) => o.SectionOutDir[3] = a[0]), - new CliOption("header", 1, (o, a) => o.HeaderOut = a[0]), - new CliOption("exefs", 1, (o, a) => o.ExefsOut = a[0]), - new CliOption("exefsdir", 1, (o, a) => o.ExefsOutDir = a[0]), - new CliOption("romfs", 1, (o, a) => o.RomfsOut = a[0]), - new CliOption("romfsdir", 1, (o, a) => o.RomfsOutDir = a[0]), - new CliOption("debugoutdir", 1, (o, a) => o.DebugOutDir = a[0]), - new CliOption("savedir", 1, (o, a) => o.SaveOutDir = a[0]), - new CliOption("outdir", 1, (o, a) => o.OutDir = a[0]), - new CliOption("ini1dir", 1, (o, a) => o.Ini1OutDir = a[0]), - new CliOption("outfile", 1, (o, a) => o.OutFile = a[0]), - new CliOption("plaintext", 1, (o, a) => o.PlaintextOut = a[0]), - new CliOption("ciphertext", 1, (o, a) => o.CiphertextOut = a[0]), - new CliOption("uncompressed", 1, (o, a) => o.UncompressedOut = a[0]), - new CliOption("nspout", 1, (o, a) => o.NspOut = a[0]), - new CliOption("sdseed", 1, (o, a) => o.SdSeed = a[0]), - new CliOption("sdpath", 1, (o, a) => o.SdPath = a[0]), - new CliOption("basenca", 1, (o, a) => o.BaseNca = a[0]), - new CliOption("basefile", 1, (o, a) => o.BaseFile = a[0]), - new CliOption("rootdir", 1, (o, a) => o.RootDir = a[0]), - new CliOption("updatedir", 1, (o, a) => o.UpdateDir = a[0]), - new CliOption("normaldir", 1, (o, a) => o.NormalDir = a[0]), - new CliOption("securedir", 1, (o, a) => o.SecureDir = a[0]), - new CliOption("logodir", 1, (o, a) => o.LogoDir = a[0]), - new CliOption("repack", 1, (o, a) => o.RepackSource = a[0]), - new CliOption("listapps", 0, (o, _) => o.ListApps = true), - new CliOption("listtitles", 0, (o, _) => o.ListTitles = true), - new CliOption("listncas", 0, (o, _) => o.ListNcas = true), - new CliOption("listromfs", 0, (o, _) => o.ListRomFs = true), - new CliOption("listfiles", 0, (o, _) => o.ListFiles = true), - new CliOption("sign", 0, (o, _) => o.SignSave = true), - new CliOption("trim", 0, (o, _) => o.TrimSave = true), - new CliOption("readbench", 0, (o, _) => o.ReadBench = true), - new CliOption("hashedfs", 0, (o, _) => o.BuildHfs = true), - new CliOption("extractini1", 0, (o, _) => o.ExtractIni1 = true), - new CliOption("title", 1, (o, a) => o.TitleId = ParseTitleId(a[0])), - new CliOption("bench", 1, (o, a) => o.BenchType = a[0]), - new CliOption("cpufreq", 1, (o, a) => o.CpuFrequencyGhz = ParseDouble(a[0])), + new CliOption("custom", 0, (o, _) => o.RunCustom = true), + new CliOption("intype", 't', 1, (o, a) => o.InFileType = ParseFileType(a[0])), + new CliOption("raw", 'r', 0, (o, _) => o.Raw = true), + new CliOption("verify", 'y', 0, (o, _) => o.Validate = true), + new CliOption("dev", 'd', 0, (o, _) => o.UseDevKeys = true), + new CliOption("enablehash", 'h', 0, (o, _) => o.EnableHash = true), + new CliOption("disablekeywarns", 0, (o, _) => o.DisableKeyWarns = true), + new CliOption("keyset", 'k', 1, (o, a) => o.Keyfile = a[0]), + new CliOption("titlekeys", 1, (o, a) => o.TitleKeyFile = a[0]), + new CliOption("consolekeys", 1, (o, a) => o.ConsoleKeyFile = a[0]), + new CliOption("accesslog", 1, (o, a) => o.AccessLog = a[0]), + new CliOption("resultlog", 1, (o, a) => o.ResultLog = a[0]), + new CliOption("section0", 1, (o, a) => o.SectionOut[0] = a[0]), + new CliOption("section1", 1, (o, a) => o.SectionOut[1] = a[0]), + new CliOption("section2", 1, (o, a) => o.SectionOut[2] = a[0]), + new CliOption("section3", 1, (o, a) => o.SectionOut[3] = a[0]), + new CliOption("section0dir", 1, (o, a) => o.SectionOutDir[0] = a[0]), + new CliOption("section1dir", 1, (o, a) => o.SectionOutDir[1] = a[0]), + new CliOption("section2dir", 1, (o, a) => o.SectionOutDir[2] = a[0]), + new CliOption("section3dir", 1, (o, a) => o.SectionOutDir[3] = a[0]), + new CliOption("header", 1, (o, a) => o.HeaderOut = a[0]), + new CliOption("exefs", 1, (o, a) => o.ExefsOut = a[0]), + new CliOption("exefsdir", 1, (o, a) => o.ExefsOutDir = a[0]), + new CliOption("romfs", 1, (o, a) => o.RomfsOut = a[0]), + new CliOption("romfsdir", 1, (o, a) => o.RomfsOutDir = a[0]), + new CliOption("debugoutdir", 1, (o, a) => o.DebugOutDir = a[0]), + new CliOption("savedir", 1, (o, a) => o.SaveOutDir = a[0]), + new CliOption("outdir", 1, (o, a) => o.OutDir = a[0]), + new CliOption("ini1dir", 1, (o, a) => o.Ini1OutDir = a[0]), + new CliOption("outfile", 1, (o, a) => o.OutFile = a[0]), + new CliOption("plaintext", 1, (o, a) => o.PlaintextOut = a[0]), + new CliOption("ciphertext", 1, (o, a) => o.CiphertextOut = a[0]), + new CliOption("uncompressed", 1, (o, a) => o.UncompressedOut = a[0]), + new CliOption("nspout", 1, (o, a) => o.NspOut = a[0]), + new CliOption("sdseed", 1, (o, a) => o.SdSeed = a[0]), + new CliOption("sdpath", 1, (o, a) => o.SdPath = a[0]), + new CliOption("basenca", 1, (o, a) => o.BaseNca = a[0]), + new CliOption("basefile", 1, (o, a) => o.BaseFile = a[0]), + new CliOption("rootdir", 1, (o, a) => o.RootDir = a[0]), + new CliOption("updatedir", 1, (o, a) => o.UpdateDir = a[0]), + new CliOption("normaldir", 1, (o, a) => o.NormalDir = a[0]), + new CliOption("securedir", 1, (o, a) => o.SecureDir = a[0]), + new CliOption("logodir", 1, (o, a) => o.LogoDir = a[0]), + new CliOption("repack", 1, (o, a) => o.RepackSource = a[0]), + new CliOption("listapps", 0, (o, _) => o.ListApps = true), + new CliOption("listtitles", 0, (o, _) => o.ListTitles = true), + new CliOption("listncas", 0, (o, _) => o.ListNcas = true), + new CliOption("listromfs", 0, (o, _) => o.ListRomFs = true), + new CliOption("listfiles", 0, (o, _) => o.ListFiles = true), + new CliOption("sign", 0, (o, _) => o.SignSave = true), + new CliOption("trim", 0, (o, _) => o.TrimSave = true), + new CliOption("readbench", 0, (o, _) => o.ReadBench = true), + new CliOption("hashedfs", 0, (o, _) => o.BuildHfs = true), + new CliOption("extractini1", 0, (o, _) => o.ExtractIni1 = true), + new CliOption("title", 1, (o, a) => o.TitleId = ParseTitleId(a[0])), + new CliOption("bench", 1, (o, a) => o.BenchType = a[0]), + new CliOption("cpufreq", 1, (o, a) => o.CpuFrequencyGhz = ParseDouble(a[0])), - new CliOption("replacefile", 2, (o, a) => - { - o.ReplaceFileDest = a[0]; - o.ReplaceFileSource = a[1]; - }) - }; + new CliOption("replacefile", 2, (o, a) => + { + o.ReplaceFileDest = a[0]; + o.ReplaceFileSource = a[1]; + }) + }; public static Options Parse(string[] args) { @@ -249,11 +249,16 @@ internal static class CliParser sb.AppendLine(" --securedir <dir> Specify secure XCI directory path."); sb.AppendLine(" --logodir <dir> Specify logo 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(" --exefsdir <dir> Specify main ExeFS directory path."); sb.AppendLine(" --romfs <file> Specify main RomFS file 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(" --outdir <dir> Specify Package1 directory path."); sb.AppendLine("Package2 options:"); @@ -319,4 +324,4 @@ internal static class CliParser public int ArgsNeeded { get; } public Action<Options, string[]> Assigner { get; } } -} +} \ No newline at end of file diff --git a/src/hactoolnet/ProcessAppFs.cs b/src/hactoolnet/ProcessAppFs.cs new file mode 100644 index 00000000..43853bd9 --- /dev/null +++ b/src/hactoolnet/ProcessAppFs.cs @@ -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; + } +} \ No newline at end of file diff --git a/src/hactoolnet/ProcessPfs.cs b/src/hactoolnet/ProcessPfs.cs index ec6bef80..42fdf987 100644 --- a/src/hactoolnet/ProcessPfs.cs +++ b/src/hactoolnet/ProcessPfs.cs @@ -1,4 +1,5 @@ using System.IO; +using System.Linq; using System.Reflection; using System.Text; using LibHac.Fs; @@ -24,6 +25,11 @@ internal static class ProcessPfs { pfs.Extract(ctx.Options.OutDir, ctx.Logger); } + + if (pfs.EnumerateEntries("*.nca", SearchOptions.Default).Any()) + { + ProcessAppFs.Process(ctx, pfs); + } } } diff --git a/src/hactoolnet/ProcessSwitchFs.cs b/src/hactoolnet/ProcessSwitchFs.cs index 2998c695..c554eef0 100644 --- a/src/hactoolnet/ProcessSwitchFs.cs +++ b/src/hactoolnet/ProcessSwitchFs.cs @@ -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"); @@ -252,7 +252,7 @@ internal static class ProcessSwitchFs return table.Print(); } - static string ListNcas(SwitchFs sdfs) + public static string ListNcas(SwitchFs sdfs) { var table = new TableBuilder("NCA ID", "Type", "Title ID"); @@ -264,7 +264,7 @@ internal static class ProcessSwitchFs return table.Print(); } - static string ListApplications(SwitchFs sdfs) + public static string ListApplications(SwitchFs sdfs) { var sb = new StringBuilder(); diff --git a/src/hactoolnet/ProcessXci.cs b/src/hactoolnet/ProcessXci.cs index 70688f1e..eb13e85f 100644 --- a/src/hactoolnet/ProcessXci.cs +++ b/src/hactoolnet/ProcessXci.cs @@ -8,7 +8,6 @@ using LibHac.FsSystem; using LibHac.Gc.Impl; using LibHac.Tools.Fs; using LibHac.Tools.FsSystem; -using LibHac.Tools.FsSystem.NcaUtils; namespace hactoolnet; @@ -65,80 +64,13 @@ internal static class ProcessXci } } - if (ctx.Options.ExefsOutDir != null || ctx.Options.ExefsOut != null) + if (xci.HasPartition(XciPartitionType.Secure)) { - Nca mainNca = GetXciMainNca(xci, ctx); - - 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)); + ProcessAppFs.Process(ctx, xci.OpenPartition(XciPartitionType.Secure)); } } } - 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) { const int colLen = 52;