Add support for extracting XCI partitions

This commit is contained in:
Alex Barney 2018-08-14 17:21:07 -06:00
parent 60a8a7b2d3
commit 1e813818c0
12 changed files with 304 additions and 165 deletions

View File

@ -34,6 +34,11 @@ namespace hactoolnet
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("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("listapps", 0, (o, a) => o.ListApps = true),
new CliOption("listtitles", 0, (o, a) => o.ListTitles = true),
new CliOption("listromfs", 0, (o, a) => o.ListRomFs = true),
@ -153,6 +158,13 @@ namespace hactoolnet
sb.AppendLine(" --section3dir <dir> Specify Section 3 directory path.");
sb.AppendLine(" --listromfs List files in RomFS.");
sb.AppendLine(" --basenca Set Base NCA to use with update partitions.");
sb.AppendLine("XCI options:");
sb.AppendLine(" --rootdir <dir> Specify root XCI directory path.");
sb.AppendLine(" --updatedir <dir> Specify update XCI directory path.");
sb.AppendLine(" --normaldir <dir> Specify normal XCI directory path.");
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("Switch FS options:");
sb.AppendLine(" --sdseed <seed> Set console unique seed for SD card NAX0 encryption.");
sb.AppendLine(" --listapps List application info.");

View File

@ -24,6 +24,11 @@ namespace hactoolnet
public string NspOut;
public string SdPath;
public string BaseNca;
public string RootDir;
public string UpdateDir;
public string NormalDir;
public string SecureDir;
public string LogoDir;
public bool ListApps;
public bool ListTitles;
public bool ListRomFs;

View File

@ -192,6 +192,44 @@ namespace hactoolnet
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;
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);
}
}
}
}

View File

@ -168,7 +168,7 @@ namespace libhac
if (sect.Type == SectionType.Pfs0)
{
sect.Pfs0 = new Pfs0Section();
sect.Pfs0.Superblock = header.Pfs0;
sect.Pfs0.Superblock = header.Pfs;
}
else if (sect.Type == SectionType.Romfs)
{
@ -233,7 +233,7 @@ namespace libhac
case SectionType.Invalid:
break;
case SectionType.Pfs0:
var pfs0 = sect.Header.Pfs0;
var pfs0 = sect.Header.Pfs;
expected = pfs0.MasterHash;
offset = pfs0.HashTableOffset;
size = pfs0.HashTableSize;
@ -402,7 +402,7 @@ namespace libhac
case SectionType.Invalid:
break;
case SectionType.Pfs0:
var pfs0 = new Pfs0(stream);
var pfs0 = new Pfs(stream);
pfs0.Extract(outputDir, logger);
break;
case SectionType.Romfs:

View File

@ -94,7 +94,7 @@ namespace libhac
public SectionCryptType CryptType;
public SectionType Type;
public Pfs0Superblock Pfs0;
public PfsSuperblock Pfs;
public RomfsSuperblock Romfs;
public BktrSuperblock Bktr;
public byte[] Ctr;
@ -112,7 +112,7 @@ namespace libhac
if (PartitionType == SectionPartitionType.Pfs0 && FsType == SectionFsType.Pfs0)
{
Type = SectionType.Pfs0;
Pfs0 = new Pfs0Superblock(reader);
Pfs = new PfsSuperblock(reader);
}
else if (PartitionType == SectionPartitionType.Romfs && FsType == SectionFsType.Romfs)
{
@ -258,7 +258,7 @@ namespace libhac
public class Pfs0Section
{
public Pfs0Superblock Superblock { get; set; }
public PfsSuperblock Superblock { get; set; }
public Validity Validity { get; set; }
}

201
libhac/Pfs.cs Normal file
View File

@ -0,0 +1,201 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace libhac
{
public class Pfs
{
public PfsHeader Header { get; }
public int HeaderSize { get; }
public PfsFileEntry[] Files { get; }
private Dictionary<string, PfsFileEntry> FileDict { get; }
private Stream Stream { get; set; }
public Pfs(Stream stream)
{
using (var reader = new BinaryReader(stream, Encoding.Default, true))
{
Header = new PfsHeader(reader);
}
HeaderSize = Header.HeaderSize;
Files = Header.Files;
FileDict = Header.Files.ToDictionary(x => x.Name, x => x);
Stream = stream;
}
public Stream OpenFile(string filename)
{
if (!FileDict.TryGetValue(filename, out PfsFileEntry file))
{
throw new FileNotFoundException();
}
return OpenFile(file);
}
public bool TryOpenFile(string filename, out Stream stream)
{
if (!FileDict.TryGetValue(filename, out PfsFileEntry file))
{
stream = null;
return false;
}
stream = OpenFile(file);
return true;
}
public Stream OpenFile(PfsFileEntry file)
{
return new SubStream(Stream, HeaderSize + file.Offset, file.Size);
}
public bool FileExists(string filename)
{
return FileDict.ContainsKey(filename);
}
}
public enum PfsType
{
Pfs0,
Hfs0
}
public class PfsSuperblock
{
public byte[] MasterHash; /* SHA-256 hash of the hash table. */
public uint BlockSize; /* In bytes. */
public uint Always2;
public long HashTableOffset; /* Normally zero. */
public long HashTableSize;
public long Pfs0Offset;
public long Pfs0Size;
public PfsSuperblock(BinaryReader reader)
{
MasterHash = reader.ReadBytes(0x20);
BlockSize = reader.ReadUInt32();
Always2 = reader.ReadUInt32();
HashTableOffset = reader.ReadInt64();
HashTableSize = reader.ReadInt64();
Pfs0Offset = reader.ReadInt64();
Pfs0Size = reader.ReadInt64();
reader.BaseStream.Position += 0xF0;
}
}
public class PfsHeader
{
public string Magic;
public int NumFiles;
public int StringTableSize;
public long Reserved;
public PfsType Type;
public int HeaderSize;
public PfsFileEntry[] Files;
public PfsHeader(BinaryReader reader)
{
Magic = reader.ReadAscii(4);
NumFiles = reader.ReadInt32();
StringTableSize = reader.ReadInt32();
Reserved = reader.ReadInt32();
switch (Magic)
{
case "PFS0":
Type = PfsType.Pfs0;
break;
case "HFS0":
Type = PfsType.Hfs0;
break;
default:
throw new InvalidDataException($"Invalid Partition FS type \"{Magic}\"");
}
int entrysize = GetFileEntrySize(Type);
int stringTableOffset = 16 + entrysize * NumFiles;
HeaderSize = stringTableOffset + StringTableSize;
Files = new PfsFileEntry[NumFiles];
for (int i = 0; i < NumFiles; i++)
{
Files[i] = new PfsFileEntry(reader, Type) { Index = i };
}
for (int i = 0; i < NumFiles; i++)
{
reader.BaseStream.Position = stringTableOffset + Files[i].StringTableOffset;
Files[i].Name = reader.ReadAsciiZ();
}
}
private static int GetFileEntrySize(PfsType type)
{
switch (type)
{
case PfsType.Pfs0:
return 24;
case PfsType.Hfs0:
return 0x40;
default:
throw new ArgumentOutOfRangeException(nameof(type), type, null);
}
}
}
public class PfsFileEntry
{
public int Index;
public long Offset;
public long Size;
public uint StringTableOffset;
public long Reserved;
public int HashedRegionSize;
public byte[] Hash;
public string Name;
public PfsFileEntry(BinaryReader reader, PfsType type)
{
Offset = reader.ReadInt64();
Size = reader.ReadInt64();
StringTableOffset = reader.ReadUInt32();
if (type == PfsType.Hfs0)
{
HashedRegionSize = reader.ReadInt32();
Reserved = reader.ReadInt64();
Hash = reader.ReadBytes(Crypto.Sha256DigestSize);
}
else
{
Reserved = reader.ReadUInt32();
}
}
}
public static class PfsExtensions
{
public static void Extract(this Pfs pfs, string outDir, IProgressReport logger = null)
{
foreach (var file in pfs.Header.Files)
{
var stream = pfs.OpenFile(file);
var outName = Path.Combine(outDir, file.Name);
var dir = Path.GetDirectoryName(outName);
if (!string.IsNullOrWhiteSpace(dir)) Directory.CreateDirectory(dir);
using (var outFile = new FileStream(outName, FileMode.Create, FileAccess.ReadWrite))
{
logger?.LogMessage(file.Name);
stream.CopyStream(outFile, stream.Length, logger);
}
}
}
}
}

View File

@ -1,151 +0,0 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace libhac
{
public class Pfs0
{
public Pfs0Header Header { get; set; }
public int HeaderSize { get; set; }
public Pfs0FileEntry[] Files { get; set; }
private Dictionary<string, Pfs0FileEntry> FileDict { get; }
private Stream Stream { get; set; }
public Pfs0(Stream stream)
{
byte[] headerBytes;
using (var reader = new BinaryReader(stream, Encoding.Default, true))
{
Header = new Pfs0Header(reader);
HeaderSize = (int)(16 + 24 * Header.NumFiles + Header.StringTableSize);
stream.Position = 0;
headerBytes = reader.ReadBytes(HeaderSize);
}
using (var reader = new BinaryReader(new MemoryStream(headerBytes)))
{
reader.BaseStream.Position = 16;
Files = new Pfs0FileEntry[Header.NumFiles];
for (int i = 0; i < Header.NumFiles; i++)
{
Files[i] = new Pfs0FileEntry(reader) { Index = i };
}
int stringTableOffset = 16 + 24 * Header.NumFiles;
for (int i = 0; i < Header.NumFiles; i++)
{
reader.BaseStream.Position = stringTableOffset + Files[i].StringTableOffset;
Files[i].Name = reader.ReadAsciiZ();
}
}
FileDict = Files.ToDictionary(x => x.Name, x => x);
Stream = stream;
}
public Stream OpenFile(string filename)
{
if (!FileDict.TryGetValue(filename, out Pfs0FileEntry file))
{
throw new FileNotFoundException();
}
return OpenFile(file);
}
public Stream OpenFile(Pfs0FileEntry file)
{
return new SubStream(Stream, HeaderSize + file.Offset, file.Size);
}
public byte[] GetFile(int index)
{
var entry = Files[index];
var file = new byte[entry.Size];
Stream.Position = HeaderSize + entry.Offset;
Stream.Read(file, 0, file.Length);
return file;
}
}
public class Pfs0Superblock
{
public byte[] MasterHash; /* SHA-256 hash of the hash table. */
public uint BlockSize; /* In bytes. */
public uint Always2;
public long HashTableOffset; /* Normally zero. */
public long HashTableSize;
public long Pfs0Offset;
public long Pfs0Size;
public Pfs0Superblock(BinaryReader reader)
{
MasterHash = reader.ReadBytes(0x20);
BlockSize = reader.ReadUInt32();
Always2 = reader.ReadUInt32();
HashTableOffset = reader.ReadInt64();
HashTableSize = reader.ReadInt64();
Pfs0Offset = reader.ReadInt64();
Pfs0Size = reader.ReadInt64();
reader.BaseStream.Position += 0xF0;
}
}
public class Pfs0Header
{
public string Magic;
public int NumFiles;
public uint StringTableSize;
public long Reserved;
public Pfs0Header(BinaryReader reader)
{
Magic = reader.ReadAscii(4);
NumFiles = reader.ReadInt32();
StringTableSize = reader.ReadUInt32();
Reserved = reader.ReadInt32();
}
}
public class Pfs0FileEntry
{
public int Index;
public long Offset;
public long Size;
public uint StringTableOffset;
public uint Reserved;
public string Name;
public Pfs0FileEntry(BinaryReader reader)
{
Offset = reader.ReadInt64();
Size = reader.ReadInt64();
StringTableOffset = reader.ReadUInt32();
Reserved = reader.ReadUInt32();
}
}
public static class Pfs0Extensions
{
public static void Extract(this Pfs0 pfs0, string outDir, IProgressReport logger = null)
{
foreach (var file in pfs0.Files)
{
var stream = pfs0.OpenFile(file);
var outName = Path.Combine(outDir, file.Name);
var dir = Path.GetDirectoryName(outName);
if (!string.IsNullOrWhiteSpace(dir)) Directory.CreateDirectory(dir);
using (var outFile = new FileStream(outName, FileMode.Create, FileAccess.ReadWrite))
{
logger?.LogMessage(file.Name);
stream.CopyStream(outFile, stream.Length, logger);
}
}
}
}
}

View File

@ -1,7 +1,5 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace libhac
{

View File

@ -94,10 +94,10 @@ namespace libhac
// Meta contents always have 1 Partition FS section with 1 file in it
Stream sect = nca.OpenSection(0, false);
var pfs0 = new Pfs0(sect);
var file = pfs0.GetFile(0);
var pfs0 = new Pfs(sect);
var file = pfs0.OpenFile(pfs0.Files[0]);
var metadata = new Cnmt(new MemoryStream(file));
var metadata = new Cnmt(file);
title.Id = metadata.TitleId;
title.Version = metadata.TitleVersion;
title.Metadata = metadata;

View File

@ -21,6 +21,9 @@ namespace libhac
baseStream.Seek(offset, SeekOrigin.Begin);
}
public SubStream(Stream baseStream, long offset)
: this(baseStream, offset, baseStream.Length - offset) { }
public override int Read(byte[] buffer, int offset, int count)
{
long remaining = Length - Position;

View File

@ -126,7 +126,7 @@ namespace libhac
stream.Position = bodyStart + 0x40;
if (TitleKeyBlock?.Length <= 0x100) writer.Write(TitleKeyBlock);
stream.Position = bodyStart + 0x140;
writer.Write((byte)FormatVersion);
writer.Write(FormatVersion);
writer.Write((byte)TitleKeyType);
writer.Write(TicketVersion);
writer.Write((byte)LicenseType);

View File

@ -4,10 +4,43 @@ namespace libhac
{
public class Xci
{
public XciHeader Header { get; set; }
private const string UpdatePartitionName = "update";
private const string NormalPartitionName = "normal";
private const string SecurePartitionName = "secure";
private const string LogoPartitionName = "logo";
public XciHeader Header { get; }
public Pfs RootPartition { get; }
public Pfs UpdatePartition { get; }
public Pfs NormalPartition { get; }
public Pfs SecurePartition { get; }
public Pfs LogoPartition { get; }
public Xci(Keyset keyset, Stream stream)
{
Header = new XciHeader(keyset, stream);
var hfs0Stream = new SubStream(stream, Header.PartitionFsHeaderAddress);
RootPartition = new Pfs(hfs0Stream);
if (RootPartition.TryOpenFile(UpdatePartitionName, out var updateStream))
{
UpdatePartition = new Pfs(updateStream);
}
if (RootPartition.TryOpenFile(NormalPartitionName, out var normalStream))
{
NormalPartition = new Pfs(normalStream);
}
if (RootPartition.TryOpenFile(SecurePartitionName, out var secureStream))
{
SecurePartition = new Pfs(secureStream);
}
if (RootPartition.TryOpenFile(LogoPartitionName, out var logoStream))
{
LogoPartition = new Pfs(logoStream);
}
}
}
}