From 44fb0d39d4ef5c6fe047d92480985f4b9bd876a8 Mon Sep 17 00:00:00 2001 From: Alex Barney Date: Sun, 1 Jul 2018 15:12:59 -0500 Subject: [PATCH] Support reading Switch user and system partition contents --- libhac/Cnmt.cs | 58 +++++++++++++++++-- libhac/Keyset.cs | 2 +- libhac/Nacp.cs | 5 +- libhac/Nax0.cs | 35 ------------ libhac/NcaStructs.cs | 31 +++++++---- libhac/SdFs.cs | 130 ++++++++++++++++++++++++++++++++++++++----- 6 files changed, 192 insertions(+), 69 deletions(-) diff --git a/libhac/Cnmt.cs b/libhac/Cnmt.cs index ed7d8787..bec3c181 100644 --- a/libhac/Cnmt.cs +++ b/libhac/Cnmt.cs @@ -5,7 +5,7 @@ namespace libhac public class Cnmt { public ulong TitleId { get; set; } - public uint TitleVersion { get; set; } + public TitleVersion TitleVersion { get; set; } public TitleType Type { get; set; } public byte FieldD { get; set; } public int TableOffset { get; set; } @@ -13,27 +13,57 @@ namespace libhac public int MetaEntryCount { get; set; } public CnmtContentEntry[] ContentEntries { get; set; } + public CnmtMetaEntry[] MetaEntries { get; set; } + + public ulong ApplicationTitleId { get; set; } + public ulong PatchTitleId { get; set; } + public TitleVersion MinimumSystemVersion { get; } + public TitleVersion MinimumApplicationVersion { get; } public Cnmt(Stream file) { using (var reader = new BinaryReader(file)) { TitleId = reader.ReadUInt64(); - TitleVersion = reader.ReadUInt32(); + var version = reader.ReadUInt32(); Type = (TitleType)reader.ReadByte(); + TitleVersion = new TitleVersion(version, Type < TitleType.Application); FieldD = reader.ReadByte(); TableOffset = reader.ReadUInt16(); ContentEntryCount = reader.ReadUInt16(); MetaEntryCount = reader.ReadUInt16(); file.Position += 12; - file.Position += TableOffset; + + switch (Type) + { + case TitleType.Application: + PatchTitleId = reader.ReadUInt64(); + MinimumSystemVersion = new TitleVersion(reader.ReadUInt32(), true); + break; + case TitleType.Patch: + ApplicationTitleId = reader.ReadUInt64(); + MinimumSystemVersion = new TitleVersion(reader.ReadUInt32(), true); + break; + case TitleType.AddOnContent: + ApplicationTitleId = reader.ReadUInt64(); + MinimumApplicationVersion = new TitleVersion(reader.ReadUInt32()); + break; + } + + file.Position = 0x20 + TableOffset; ContentEntries = new CnmtContentEntry[ContentEntryCount]; + MetaEntries = new CnmtMetaEntry[MetaEntryCount]; for (int i = 0; i < ContentEntryCount; i++) { ContentEntries[i] = new CnmtContentEntry(reader); } + + for (int i = 0; i < MetaEntryCount; i++) + { + MetaEntries[i] = new CnmtMetaEntry(reader); + } } } } @@ -56,6 +86,22 @@ namespace libhac } } + public class CnmtMetaEntry + { + public ulong TitleId { get; } + public TitleVersion Version { get; } + public CnmtContentType Type { get; } + + + public CnmtMetaEntry(BinaryReader reader) + { + TitleId = reader.ReadUInt64(); + Version = new TitleVersion(reader.ReadUInt32(), true); + Type = (CnmtContentType)reader.ReadByte(); + reader.BaseStream.Position += 3; + } + } + public enum CnmtContentType { Meta, @@ -70,10 +116,10 @@ namespace libhac public enum TitleType { SystemProgram = 1, - SystemDataArchive, + SystemData, SystemUpdate, - FirmwarePackageA, - FirmwarePackageB, + BootImagePackage, + BootImagePackageSafe, Application = 0x80, Patch, AddOnContent, diff --git a/libhac/Keyset.cs b/libhac/Keyset.cs index c4a7c54e..5b86532b 100644 --- a/libhac/Keyset.cs +++ b/libhac/Keyset.cs @@ -107,7 +107,7 @@ namespace libhac continue; } - keyset.TitleKeys.Add(rightsId, titleKey); + keyset.TitleKeys[rightsId] = titleKey; } } diff --git a/libhac/Nacp.cs b/libhac/Nacp.cs index 71f392b5..3af5843a 100644 --- a/libhac/Nacp.cs +++ b/libhac/Nacp.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; +using System.IO; namespace libhac { diff --git a/libhac/Nax0.cs b/libhac/Nax0.cs index 63c49780..438d1a71 100644 --- a/libhac/Nax0.cs +++ b/libhac/Nax0.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.IO; using System.Security.Cryptography; using System.Text; @@ -78,40 +77,6 @@ namespace libhac if (!isValid) throw new ArgumentException("NAX0 key derivation failed."); } - public static Nax0 CreateFromPath(Keyset keyset, string path, string sdPath) - { - List files = new List(); - List streams = new List(); - - if (Directory.Exists(path)) - { - while (true) - { - var partName = Path.Combine(path, $"{files.Count:D2}"); - if (!File.Exists(partName)) break; - - files.Add(partName); - } - - } - else if (File.Exists(path)) - { - files.Add(path); - } - else - { - throw new FileNotFoundException("Could not find the input file or directory"); - } - - foreach (var file in files) - { - streams.Add(new FileStream(file, FileMode.Open)); - } - - var stream = new CombinationStream(streams); - return new Nax0(keyset, stream, sdPath, false); - } - public void Dispose() { if (!KeepOpen) diff --git a/libhac/NcaStructs.cs b/libhac/NcaStructs.cs index 0b26eae6..e47f1e75 100644 --- a/libhac/NcaStructs.cs +++ b/libhac/NcaStructs.cs @@ -225,18 +225,29 @@ namespace libhac public class TitleVersion { public uint Version { get; } - public byte Major { get; } - public byte Minor { get; } - public byte Patch { get; } - public byte Revision { get; } + public int Major { get; } + public int Minor { get; } + public int Patch { get; } + public int Revision { get; } - public TitleVersion(uint version) + public TitleVersion(uint version, bool isSystemTitle = false) { Version = version; - Revision = (byte)version; - Patch = (byte)(version >> 8); - Minor = (byte)(version >> 16); - Major = (byte)(version >> 24); + + if (isSystemTitle) + { + Revision = (int)(version & ((1 << 16) - 1)); + Patch = (int)((version >> 16) & ((1 << 4) - 1)); + Minor = (int)((version >> 20) & ((1 << 6) - 1)); + Major = (int)((version >> 26) & ((1 << 6) - 1)); + } + else + { + Revision = (byte)version; + Patch = (byte)(version >> 8); + Minor = (byte)(version >> 16); + Major = (byte)(version >> 24); + } } public override string ToString() @@ -244,7 +255,7 @@ namespace libhac return $"{Major}.{Minor}.{Patch}.{Revision}"; } } - + public enum ContentType { Program, diff --git a/libhac/SdFs.cs b/libhac/SdFs.cs index 8196bf92..b4f959bd 100644 --- a/libhac/SdFs.cs +++ b/libhac/SdFs.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; +using System.Text; namespace libhac { @@ -11,23 +12,26 @@ namespace libhac public Keyset Keyset { get; } public string RootDir { get; } public string ContentsDir { get; } - public string[] Files { get; } public Dictionary Ncas { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); public Dictionary Titles { get; } = new Dictionary(); private List Nax0s { get; } = new List(); - public SdFs(Keyset keyset, string sdPath) + public SdFs(Keyset keyset, string rootDir) { - if (Directory.Exists(Path.Combine(sdPath, "Nintendo"))) + RootDir = rootDir; + Keyset = keyset; + + if (Directory.Exists(Path.Combine(rootDir, "Nintendo"))) { - RootDir = sdPath; - Keyset = keyset; - ContentsDir = Path.Combine(sdPath, "Nintendo", "Contents"); + ContentsDir = Path.Combine(rootDir, "Nintendo", "Contents"); + } + else if (Directory.Exists(Path.Combine(rootDir, "Contents"))) + { + ContentsDir = Path.Combine(rootDir, "Contents"); } - Files = Directory.GetFiles(ContentsDir, "00", SearchOption.AllDirectories).Select(Path.GetDirectoryName).ToArray(); OpenAllNcas(); ReadTitles(); ReadControls(); @@ -35,15 +39,36 @@ namespace libhac private void OpenAllNcas() { - foreach (var file in Files) + string[] files = Directory.GetFileSystemEntries(ContentsDir, "*.nca", SearchOption.AllDirectories).ToArray(); + + foreach (var file in files) { Nca nca = null; try { - var sdPath = "/" + Util.GetRelativePath(file, ContentsDir).Replace('\\', '/'); - var nax0 = Nax0.CreateFromPath(Keyset, file, sdPath); - Nax0s.Add(nax0); - nca = new Nca(Keyset, nax0.Stream, false); + bool isNax0; + Stream stream = OpenSplitNcaStream(file); + if (stream == null) continue; + + using (var reader = new BinaryReader(stream, Encoding.Default, true)) + { + stream.Position = 0x20; + isNax0 = reader.ReadUInt32() == 0x3058414E; // NAX0 + stream.Position = 0; + } + + if (isNax0) + { + var sdPath = "/" + Util.GetRelativePath(file, ContentsDir).Replace('\\', '/'); + var nax0 = new Nax0(Keyset, stream, sdPath, false); + Nax0s.Add(nax0); + nca = new Nca(Keyset, nax0.Stream, false); + } + else + { + nca = new Nca(Keyset, stream, false); + } + nca.NcaId = Path.GetFileNameWithoutExtension(file); var extention = nca.Header.ContentType == ContentType.Meta ? ".cnmt.nca" : ".nca"; nca.Filename = nca.NcaId + extention; @@ -70,7 +95,7 @@ namespace libhac var metadata = new Cnmt(new MemoryStream(file)); title.Id = metadata.TitleId; - title.Version = new TitleVersion(metadata.TitleVersion); + title.Version = metadata.TitleVersion; title.Metadata = metadata; title.MetaNca = nca; title.Ncas.Add(nca); @@ -122,6 +147,45 @@ namespace libhac } } + internal static Stream OpenSplitNcaStream(string path) + { + List files = new List(); + List streams = new List(); + + if (Directory.Exists(path)) + { + while (true) + { + var partName = Path.Combine(path, $"{files.Count:D2}"); + if (!File.Exists(partName)) break; + + files.Add(partName); + } + } + else if (File.Exists(path)) + { + if (Path.GetFileName(path) != "00") + { + return new FileStream(path, FileMode.Open, FileAccess.Read); + } + files.Add(path); + } + else + { + throw new FileNotFoundException("Could not find the input file or directory"); + } + + foreach (var file in files) + { + streams.Add(new FileStream(file, FileMode.Open, FileAccess.Read)); + } + + if (streams.Count == 0) return null; + + var stream = new CombinationStream(streams); + return stream; + } + private void DisposeNcas() { foreach (Nca nca in Ncas.Values) @@ -155,4 +219,44 @@ namespace libhac public Nca ProgramNca { get; internal set; } public Nca ControlNca { get; internal set; } } + + public class Application + { + public Title Main { get; private set; } + public Title Patch { get; private set; } + public List AddOnContent { get; private set; } + + public string Name { get; private set; } + public string Version { get; private set; } + + public void SetMainTitle(Title title) + { + Main = title; + } + + public void SetPatchTitle(Title title) + { + if (title.Metadata.Type != TitleType.Patch) throw new InvalidDataException("Title is not a patch"); + Patch = title; + } + + private void UpdateName() + { + if (Patch != null) + { + Name = Patch.Name; + Version = Patch.Control?.Version ?? ""; + } + else if (Main != null) + { + Name = Main.Name; + Version = Main.Control?.Version ?? ""; + } + else + { + Name = ""; + Version = ""; + } + } + } }