diff --git a/hactoolnet/CliParser.cs b/hactoolnet/CliParser.cs
index 2bbf8cba..00926911 100644
--- a/hactoolnet/CliParser.cs
+++ b/hactoolnet/CliParser.cs
@@ -28,6 +28,7 @@ namespace hactoolnet
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("outdir", 1, (o, a) => o.OutDir = a[0]),
new CliOption("sdseed", 1, (o, a) => o.SdSeed = a[0]),
new CliOption("sdpath", 1, (o, a) => o.SdPath = a[0]),
@@ -158,6 +159,10 @@ namespace hactoolnet
sb.AppendLine(" --title
Specify title ID to use.");
sb.AppendLine(" --outdir Specify directory path to save title to. (--title must be specified)");
sb.AppendLine(" --romfsdir Specify RomFS directory path. (--title must be specified)");
+ sb.AppendLine("Savefile options:");
+ sb.AppendLine(" --outdir Specify directory path to save contents to.");
+ sb.AppendLine(" --debugoutdir Specify directory path to save intermediate data to for debugging.");
+
return sb.ToString();
}
diff --git a/hactoolnet/Options.cs b/hactoolnet/Options.cs
index a477264f..bb56ca4a 100644
--- a/hactoolnet/Options.cs
+++ b/hactoolnet/Options.cs
@@ -18,6 +18,7 @@ namespace hactoolnet
public string ExefsOutDir;
public string RomfsOut;
public string RomfsOutDir;
+ public string DebugOutDir;
public string OutDir;
public string SdSeed;
public string SdPath;
diff --git a/hactoolnet/Program.cs b/hactoolnet/Program.cs
index 23e8384d..1cfb41db 100644
--- a/hactoolnet/Program.cs
+++ b/hactoolnet/Program.cs
@@ -217,30 +217,41 @@ namespace hactoolnet
var save = new Savefile(file, ctx.Logger);
var layout = save.Header.Layout;
- File.WriteAllBytes("d0_JournalTable", save.JournalTable);
- File.WriteAllBytes("d1_JournalBitmapUpdatedPhysical", save.JournalBitmapUpdatedPhysical);
- File.WriteAllBytes("d2_JournalBitmapUpdatedVirtual", save.JournalBitmapUpdatedVirtual);
- File.WriteAllBytes("d3_JournalBitmapUnassigned", save.JournalBitmapUnassigned);
- File.WriteAllBytes("d4_Layer1Hash", save.JournalLayer1Hash);
- File.WriteAllBytes("d5_Layer2Hash", save.JournalLayer2Hash);
- File.WriteAllBytes("d6_Layer3Hash", save.JournalLayer3Hash);
- File.WriteAllBytes("d7_Stuff", save.JournalStuff);
-
- File.WriteAllBytes("0_DuplexL1A", save.DuplexL1A);
- File.WriteAllBytes("1_DuplexL1B", save.DuplexL1B);
- File.WriteAllBytes("2_DuplexDataA", save.DuplexDataA);
- File.WriteAllBytes("3_DuplexDataB", save.DuplexDataB);
-
- save.FileRemap.Position = layout.JournalDataOffset;
- using (var outFile = new FileStream("4_JournalData", FileMode.Create, FileAccess.Write))
+ if (ctx.Options.OutDir != null)
{
- save.FileRemap.CopyStream(outFile, layout.JournalDataSizeB + layout.SizeReservedArea);
+ save.Extract(ctx.Options.OutDir, ctx.Logger);
}
- save.JournalStream.Position = 0;
- using (var outFile = new FileStream("j0_Data", FileMode.Create, FileAccess.Write))
+ if (ctx.Options.DebugOutDir != null)
{
- save.JournalStream.CopyStream(outFile, save.JournalStream.Length);
+ var dir = ctx.Options.DebugOutDir;
+ Directory.CreateDirectory(dir);
+
+ File.WriteAllBytes(Path.Combine(dir, "L0_0_DuplexL1A"), save.DuplexL1A);
+ File.WriteAllBytes(Path.Combine(dir, "L0_1_DuplexL1B"), save.DuplexL1B);
+ File.WriteAllBytes(Path.Combine(dir, "L0_2_DuplexDataA"), save.DuplexDataA);
+ File.WriteAllBytes(Path.Combine(dir, "L0_3_DuplexDataB"), save.DuplexDataB);
+
+ save.FileRemap.Position = layout.JournalDataOffset;
+ using (var outFile = new FileStream(Path.Combine(dir, "L0_4_JournalData"), FileMode.Create, FileAccess.Write))
+ {
+ save.FileRemap.CopyStream(outFile, layout.JournalDataSizeB + layout.SizeReservedArea);
+ }
+
+ File.WriteAllBytes(Path.Combine(dir, "L1_0_JournalTable"), save.JournalTable);
+ File.WriteAllBytes(Path.Combine(dir, "L1_1_JournalBitmapUpdatedPhysical"), save.JournalBitmapUpdatedPhysical);
+ File.WriteAllBytes(Path.Combine(dir, "L1_2_JournalBitmapUpdatedVirtual"), save.JournalBitmapUpdatedVirtual);
+ File.WriteAllBytes(Path.Combine(dir, "L1_3_JournalBitmapUnassigned"), save.JournalBitmapUnassigned);
+ File.WriteAllBytes(Path.Combine(dir, "L1_4_Layer1Hash"), save.JournalLayer1Hash);
+ File.WriteAllBytes(Path.Combine(dir, "L1_5_Layer2Hash"), save.JournalLayer2Hash);
+ File.WriteAllBytes(Path.Combine(dir, "L1_6_Layer3Hash"), save.JournalLayer3Hash);
+ File.WriteAllBytes(Path.Combine(dir, "L1_7_FAT"), save.JournalStuff);
+
+ save.JournalStream.Position = 0;
+ using (var outFile = new FileStream(Path.Combine(dir, "L2_0_SaveData"), FileMode.Create, FileAccess.Write))
+ {
+ save.JournalStream.CopyStream(outFile, save.JournalStream.Length);
+ }
}
}
}
diff --git a/libhac/Savefile/FileEntry.cs b/libhac/Savefile/FileEntry.cs
new file mode 100644
index 00000000..b34e250b
--- /dev/null
+++ b/libhac/Savefile/FileEntry.cs
@@ -0,0 +1,62 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+namespace libhac.Savefile
+{
+ public class FileEntry
+ {
+ public int ParentDirIndex { get; }
+ public string Name { get; }
+ public int Field44 { get; }
+ public int Offset { get; }
+ public long Size { get; }
+ public long Field54 { get; }
+ public int NextIndex { get; }
+
+ public string FullPath { get; private set; }
+ public FileEntry ParentDir { get; internal set; }
+ public FileEntry Next { get; internal set; }
+
+ public FileEntry(BinaryReader reader)
+ {
+ var start = reader.BaseStream.Position;
+ ParentDirIndex = reader.ReadInt32();
+ Name = reader.ReadUtf8Z(0x40);
+ reader.BaseStream.Position = start + 0x44;
+
+ Field44 = reader.ReadInt32();
+ Offset = reader.ReadInt32();
+ Size = reader.ReadInt64();
+ Field54 = reader.ReadInt64();
+ NextIndex = reader.ReadInt32();
+ }
+
+ public static void ResolveFilenames(FileEntry[] entries)
+ {
+ var list = new List();
+ var sb = new StringBuilder();
+ var delimiter = "/";
+ foreach (var file in entries)
+ {
+ list.Add(file.Name);
+ var dir = file.ParentDir;
+ while (dir != null)
+ {
+ list.Add(delimiter);
+ list.Add(dir.Name);
+ dir = dir.ParentDir;
+ }
+
+ for (int i = list.Count - 1; i >= 0; i--)
+ {
+ sb.Append(list[i]);
+ }
+
+ file.FullPath = sb.ToString();
+ list.Clear();
+ sb.Clear();
+ }
+ }
+ }
+}
diff --git a/libhac/Savefile/Header.cs b/libhac/Savefile/Header.cs
index 703c4637..fc1a1ee8 100644
--- a/libhac/Savefile/Header.cs
+++ b/libhac/Savefile/Header.cs
@@ -7,7 +7,8 @@ namespace libhac.Savefile
{
public byte[] Cmac { get; set; }
public FsLayout Layout { get; set; }
- public JournalHeader Journal{ get; set; }
+ public JournalHeader Journal { get; set; }
+ public SaveHeader Save { get; set; }
public RemapHeader FileRemap { get; set; }
public RemapHeader MetaRemap { get; set; }
@@ -31,6 +32,9 @@ namespace libhac.Savefile
reader.BaseStream.Position = 0x408;
Journal = new JournalHeader(reader);
+ reader.BaseStream.Position = 0x608;
+ Save = new SaveHeader(reader);
+
reader.BaseStream.Position = 0x650;
FileRemap = new RemapHeader(reader);
reader.BaseStream.Position = 0x690;
@@ -200,6 +204,50 @@ namespace libhac.Savefile
}
}
+ public class SaveHeader
+ {
+ public string Magic { get; }
+ public uint MagicNum { get; }
+ public int Field8 { get; }
+ public int FieldC { get; }
+ public int Field10 { get; }
+ public int Field14 { get; }
+ public long BlockSize { get; }
+ public StorageInfo AllocationTableInfo { get; }
+ public StorageInfo DataInfo { get; }
+ public int DirectoryTableBlock { get; }
+ public int FileTableBlock { get; }
+
+ public SaveHeader(BinaryReader reader)
+ {
+ Magic = reader.ReadAscii(4);
+ MagicNum = reader.ReadUInt32();
+ Field8 = reader.ReadInt32();
+ FieldC = reader.ReadInt32();
+ Field10 = reader.ReadInt32();
+ Field14 = reader.ReadInt32();
+ BlockSize = reader.ReadInt64();
+ AllocationTableInfo = new StorageInfo(reader);
+ DataInfo = new StorageInfo(reader);
+ DirectoryTableBlock = reader.ReadInt32();
+ FileTableBlock = reader.ReadInt32();
+ }
+ }
+
+ public class StorageInfo
+ {
+ public long Offset { get; }
+ public int Size { get; }
+ public int FieldC { get; }
+
+ public StorageInfo(BinaryReader reader)
+ {
+ Offset = reader.ReadInt64();
+ Size = reader.ReadInt32();
+ FieldC = reader.ReadInt32();
+ }
+ }
+
public class MapEntry
{
public long VirtualOffset { get; }
diff --git a/libhac/Savefile/Savefile.cs b/libhac/Savefile/Savefile.cs
index 59da839a..2e5fa9ca 100644
--- a/libhac/Savefile/Savefile.cs
+++ b/libhac/Savefile/Savefile.cs
@@ -1,4 +1,7 @@
-using System.IO;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
using System.Text;
namespace libhac.Savefile
@@ -25,6 +28,9 @@ namespace libhac.Savefile
public byte[] JournalLayer3Hash { get; }
public byte[] JournalStuff { get; }
+ public FileEntry[] Files { get; private set; }
+ private Dictionary FileDict { get; }
+
public Savefile(Stream file, IProgressReport logger = null)
{
FileStream = file;
@@ -84,8 +90,97 @@ namespace libhac.Savefile
var journalData = new SubStream(FileRemap, layout.JournalDataOffset,
layout.JournalDataSizeB + layout.SizeReservedArea);
- JournalStream = new JournalStream(journalData, journalMap, (int) Header.Journal.BlockSize)
- ;
+ JournalStream = new JournalStream(journalData, journalMap, (int)Header.Journal.BlockSize);
+ ReadFileInfo();
+ FileDict = Files.ToDictionary(x => x.FullPath, x => x);
+ }
+ }
+
+ public Stream OpenFile(string filename)
+ {
+ if (!FileDict.TryGetValue(filename, out FileEntry file))
+ {
+ throw new FileNotFoundException();
+ }
+
+ return OpenFile(file);
+ }
+
+ public Stream OpenFile(FileEntry file)
+ {
+ return new SubStream(JournalStream, file.Offset * Header.Save.BlockSize, file.Size);
+ }
+
+ public bool FileExists(string filename) => FileDict.ContainsKey(filename);
+
+ private void ReadFileInfo()
+ {
+ var blockSize = Header.Save.BlockSize;
+ var dirOffset = Header.Save.DirectoryTableBlock * blockSize;
+ var fileOffset = Header.Save.FileTableBlock * blockSize;
+
+ FileEntry[] dirEntries;
+ FileEntry[] fileEntries;
+ using (var reader = new BinaryReader(JournalStream, Encoding.Default, true))
+ {
+ JournalStream.Position = dirOffset;
+ dirEntries = ReadFileEntries(reader);
+
+ JournalStream.Position = fileOffset;
+ fileEntries = ReadFileEntries(reader);
+ }
+
+ foreach (var dir in dirEntries)
+ {
+ if (dir.NextIndex != 0) dir.Next = dirEntries[dir.NextIndex];
+ if (dir.ParentDirIndex != 0 && dir.ParentDirIndex < dirEntries.Length)
+ dir.ParentDir = dirEntries[dir.ParentDirIndex];
+ }
+
+ foreach (var file in fileEntries)
+ {
+ if (file.NextIndex != 0) file.Next = fileEntries[file.NextIndex];
+ if (file.ParentDirIndex != 0 && file.ParentDirIndex < dirEntries.Length)
+ file.ParentDir = dirEntries[file.ParentDirIndex];
+ }
+
+ Files = new FileEntry[fileEntries.Length - 2];
+ Array.Copy(fileEntries, 2, Files, 0, Files.Length);
+
+ FileEntry.ResolveFilenames(Files);
+ }
+
+ private FileEntry[] ReadFileEntries(BinaryReader reader)
+ {
+ var count = reader.ReadInt32();
+ JournalStream.Position -= 4;
+
+ var entries = new FileEntry[count];
+ for (int i = 0; i < count; i++)
+ {
+ entries[i] = new FileEntry(reader);
+ }
+
+ return entries;
+ }
+ }
+
+ public static class SavefileExtensions
+ {
+ public static void Extract(this Savefile save, string outDir, IProgressReport logger = null)
+ {
+ foreach (var file in save.Files)
+ {
+ var stream = save.OpenFile(file);
+ var outName = outDir + file.FullPath;
+ 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.FullPath);
+ stream.CopyStream(outFile, stream.Length, logger);
+ }
}
}
}
diff --git a/libhac/Util.cs b/libhac/Util.cs
index a44d7288..fabef9da 100644
--- a/libhac/Util.cs
+++ b/libhac/Util.cs
@@ -78,31 +78,35 @@ namespace libhac
}
}
- public static string ReadAsciiZ(this BinaryReader reader)
+ public static string ReadAsciiZ(this BinaryReader reader, int maxLength = int.MaxValue)
{
var start = reader.BaseStream.Position;
+ int size = 0;
// Read until we hit the end of the stream (-1) or a zero
- while (reader.BaseStream.ReadByte() - 1 > 0) { }
+ while (reader.BaseStream.ReadByte() - 1 > 0 && size < maxLength)
+ {
+ size++;
+ }
- int size = (int)(reader.BaseStream.Position - start - 1);
reader.BaseStream.Position = start;
-
string text = reader.ReadAscii(size);
reader.BaseStream.Position++; // Skip the null byte
return text;
}
- public static string ReadUtf8Z(this BinaryReader reader)
+ public static string ReadUtf8Z(this BinaryReader reader, int maxLength = int.MaxValue)
{
var start = reader.BaseStream.Position;
+ int size = 0;
// Read until we hit the end of the stream (-1) or a zero
- while (reader.BaseStream.ReadByte() - 1 > 0) { }
+ while (reader.BaseStream.ReadByte() - 1 > 0 && size < maxLength)
+ {
+ size++;
+ }
- int size = (int)(reader.BaseStream.Position - start - 1);
reader.BaseStream.Position = start;
-
string text = reader.ReadUtf8(size);
reader.BaseStream.Position++; // Skip the null byte
return text;