mirror of
https://github.com/Thealexbarney/LibHac.git
synced 2025-02-09 13:14:46 +01:00
Add support for extracting XCI partitions
This commit is contained in:
parent
60a8a7b2d3
commit
1e813818c0
@ -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.");
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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
201
libhac/Pfs.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
151
libhac/Pfs0.cs
151
libhac/Pfs0.cs
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace libhac
|
||||
{
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user