mirror of
https://github.com/Thealexbarney/LibHac.git
synced 2024-11-14 10:49:41 +01:00
Read and extract core save file system
This commit is contained in:
parent
b0cac269f3
commit
f074d89d46
7 changed files with 258 additions and 32 deletions
|
@ -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 <title id> 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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
62
libhac/Savefile/FileEntry.cs
Normal file
62
libhac/Savefile/FileEntry.cs
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue