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 <dir> Specify directory path to save title to. (--title must be specified)"); sb.AppendLine(" --romfsdir <dir> Specify RomFS directory path. (--title must be specified)"); + sb.AppendLine("Savefile options:"); + sb.AppendLine(" --outdir <dir> Specify directory path to save contents to."); + sb.AppendLine(" --debugoutdir <dir> 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<string>(); + 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<string, FileEntry> 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;