diff --git a/hactoolnet/Options.cs b/hactoolnet/Options.cs index f2a85c10..a477264f 100644 --- a/hactoolnet/Options.cs +++ b/hactoolnet/Options.cs @@ -34,7 +34,8 @@ namespace hactoolnet Pfs0, Romfs, Nax0, - SwitchFs + SwitchFs, + Save } internal class Context diff --git a/hactoolnet/Program.cs b/hactoolnet/Program.cs index 1383fe8f..67a87da8 100644 --- a/hactoolnet/Program.cs +++ b/hactoolnet/Program.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using System.Text; using libhac; +using libhac.Savefile; namespace hactoolnet { @@ -40,6 +41,9 @@ namespace hactoolnet case FileType.SwitchFs: ProcessSwitchFs(ctx); break; + case FileType.Save: + ProcessSave(ctx); + break; default: throw new ArgumentOutOfRangeException(); } @@ -206,6 +210,45 @@ namespace hactoolnet } } + private static void ProcessSave(Context ctx) + { + using (var file = new FileStream(ctx.Options.InFile, FileMode.Open, FileAccess.Read)) + { + var save = new Savefile(file); + var layout = save.Header.Layout; + + save.FileRemap.Position = layout.DuplexL1OffsetB; + using (var outFile = new FileStream("0_DuplexL1A", FileMode.Create, FileAccess.Write)) + { + save.FileRemap.CopyStream(outFile, layout.DuplexDataSize); + } + + save.FileRemap.Position = layout.DuplexL1OffsetB; + using (var outFile = new FileStream("1_DuplexL1B", FileMode.Create, FileAccess.Write)) + { + save.FileRemap.CopyStream(outFile, layout.DuplexDataSize); + } + + save.FileRemap.Position = layout.DuplexDataOffsetA; + using (var outFile = new FileStream("2_DuplexDataA", FileMode.Create, FileAccess.Write)) + { + save.FileRemap.CopyStream(outFile, layout.DuplexDataSize); + } + + save.FileRemap.Position = layout.DuplexDataOffsetB; + using (var outFile = new FileStream("3_DuplexDataB", FileMode.Create, FileAccess.Write)) + { + save.FileRemap.CopyStream(outFile, layout.DuplexDataSize); + } + + save.FileRemap.Position = layout.JournalDataOffset; + using (var outFile = new FileStream("4_JournalData", FileMode.Create, FileAccess.Write)) + { + save.FileRemap.CopyStream(outFile, layout.JournalDataSizeB + layout.SizeReservedArea); + } + } + } + // For running random stuff // ReSharper disable once UnusedParameter.Local private static void CustomTask(Context ctx) diff --git a/libhac/Savefile/Header.cs b/libhac/Savefile/Header.cs new file mode 100644 index 00000000..d88791bb --- /dev/null +++ b/libhac/Savefile/Header.cs @@ -0,0 +1,175 @@ +using System.IO; + +namespace libhac.Savefile +{ + public class Header + { + public byte[] Cmac { get; set; } + public FsLayout Layout { get; set; } + + public RemapHeader FileRemap { get; set; } + public RemapHeader MetaRemap { get; set; } + + public MapEntry[] FileMapEntries { get; set; } + public MapEntry[] MetaMapEntries { get; set; } + + public Header(BinaryReader reader) + { + Cmac = reader.ReadBytes(0x10); + + reader.BaseStream.Position = 0x100; + Layout = new FsLayout(reader); + + reader.BaseStream.Position = 0x650; + FileRemap = new RemapHeader(reader); + reader.BaseStream.Position = 0x690; + MetaRemap = new RemapHeader(reader); + + reader.BaseStream.Position = Layout.FileMapEntryOffset; + FileMapEntries = new MapEntry[FileRemap.MapEntryCount]; + for (int i = 0; i < FileRemap.MapEntryCount; i++) + { + FileMapEntries[i] = new MapEntry(reader); + } + + reader.BaseStream.Position = Layout.MetaMapEntryOffset; + MetaMapEntries = new MapEntry[MetaRemap.MapEntryCount]; + for (int i = 0; i < MetaRemap.MapEntryCount; i++) + { + MetaMapEntries[i] = new MapEntry(reader); + } + } + } + + public class FsLayout + { + public string Magic { get; set; } + public uint MagicNum { get; set; } + public byte[] Hash { get; set; } + public long FileMapEntryOffset { get; set; } + public long FileMapEntrySize { get; set; } + public long MetaMapEntryOffset { get; set; } + public long MetaMapEntrySize { get; set; } + public long FileMapDataOffset { get; set; } + public long FileMapDataSize { get; set; } + public long DuplexL1OffsetA { get; set; } + public long DuplexL1OffsetB { get; set; } + public long DuplexL1Size { get; set; } + public long DuplexDataOffsetA { get; set; } + public long DuplexDataOffsetB { get; set; } + public long DuplexDataSize { get; set; } + public long JournalDataOffset { get; set; } + public long JournalDataSizeA { get; set; } + public long JournalDataSizeB { get; set; } + public long SizeReservedArea { get; set; } + public long OffsetL1Bitmap0 { get; set; } + public long OffsetL1Bitmap1 { get; set; } + public long SizeL1Bitmap { get; set; } + public long MasterHashOffset { get; set; } + public long FieldC8 { get; set; } + public long MasterHashSize { get; set; } + public long OffsetJournalTable { get; set; } + public long SizeJournalTable { get; set; } + public long JournalBitmapUpdatedPhysicalOffset { get; set; } + public long JournalBitmapUpdatedPhysicalSize { get; set; } + public long JournalBitmapUpdatedVirtualOffset { get; set; } + public long JournalBitmapUpdatedVirtualSize { get; set; } + public long JournalBitmapUnassignedOffset { get; set; } + public long JournalBitmapUnassignedSize { get; set; } + public long Layer1HashOffset { get; set; } + public long Layer1HashSize { get; set; } + public long Layer2HashOffset { get; set; } + public long Layer2HashSize { get; set; } + public long Layer3HashOffset { get; set; } + public long Layer3HashSize { get; set; } + public long Field148 { get; set; } + public long Field150 { get; set; } + public long Field158 { get; set; } + + public FsLayout(BinaryReader reader) + { + Magic = reader.ReadAscii(4); + MagicNum = reader.ReadUInt32(); + Hash = reader.ReadBytes(0x20); + FileMapEntryOffset = reader.ReadInt64(); + FileMapEntrySize = reader.ReadInt64(); + MetaMapEntryOffset = reader.ReadInt64(); + MetaMapEntrySize = reader.ReadInt64(); + FileMapDataOffset = reader.ReadInt64(); + FileMapDataSize = reader.ReadInt64(); + DuplexL1OffsetA = reader.ReadInt64(); + DuplexL1OffsetB = reader.ReadInt64(); + DuplexL1Size = reader.ReadInt64(); + DuplexDataOffsetA = reader.ReadInt64(); + DuplexDataOffsetB = reader.ReadInt64(); + DuplexDataSize = reader.ReadInt64(); + JournalDataOffset = reader.ReadInt64(); + JournalDataSizeA = reader.ReadInt64(); + JournalDataSizeB = reader.ReadInt64(); + SizeReservedArea = reader.ReadInt64(); + OffsetL1Bitmap0 = reader.ReadInt64(); + OffsetL1Bitmap1 = reader.ReadInt64(); + SizeL1Bitmap = reader.ReadInt64(); + MasterHashOffset = reader.ReadInt64(); + FieldC8 = reader.ReadInt64(); + MasterHashSize = reader.ReadInt64(); + OffsetJournalTable = reader.ReadInt64(); + SizeJournalTable = reader.ReadInt64(); + JournalBitmapUpdatedPhysicalOffset = reader.ReadInt64(); + JournalBitmapUpdatedPhysicalSize = reader.ReadInt64(); + JournalBitmapUpdatedVirtualOffset = reader.ReadInt64(); + JournalBitmapUpdatedVirtualSize = reader.ReadInt64(); + JournalBitmapUnassignedOffset = reader.ReadInt64(); + JournalBitmapUnassignedSize = reader.ReadInt64(); + Layer1HashOffset = reader.ReadInt64(); + Layer1HashSize = reader.ReadInt64(); + Layer2HashOffset = reader.ReadInt64(); + Layer2HashSize = reader.ReadInt64(); + Layer3HashOffset = reader.ReadInt64(); + Layer3HashSize = reader.ReadInt64(); + Field148 = reader.ReadInt64(); + Field150 = reader.ReadInt64(); + Field158 = reader.ReadInt64(); + } + } + + public class RemapHeader + { + public string Magic { get; set; } + public uint MagicNum { get; set; } + public int MapEntryCount { get; set; } + public int MapSegmentCount { get; set; } + public int Field10 { get; set; } + + public RemapHeader(BinaryReader reader) + { + Magic = reader.ReadAscii(4); + MagicNum = reader.ReadUInt32(); + MapEntryCount = reader.ReadInt32(); + MapSegmentCount = reader.ReadInt32(); + Field10 = reader.ReadInt32(); + } + } + + public class MapEntry + { + public long VirtualOffset { get; } + public long PhysicalOffset { get; } + public long Size { get; } + public int Alignment { get; } + public int StorageType { get; } + public long VirtualOffsetEnd => VirtualOffset + Size; + public long PhysicalOffsetEnd => PhysicalOffset + Size; + internal RemapSegment Segment { get; set; } + internal MapEntry Next { get; set; } + + public MapEntry(BinaryReader reader) + { + VirtualOffset = reader.ReadInt64(); + PhysicalOffset = reader.ReadInt64(); + Size = reader.ReadInt64(); + Alignment = reader.ReadInt32(); + StorageType = reader.ReadInt32(); + } + } +} diff --git a/libhac/Savefile/RemapStream.cs b/libhac/Savefile/RemapStream.cs new file mode 100644 index 00000000..490094f4 --- /dev/null +++ b/libhac/Savefile/RemapStream.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace libhac.Savefile +{ + public class RemapStream : Stream + { + private long _position; + private Stream BaseStream { get; } + public MapEntry[] MapEntries { get; set; } + public MapEntry CurrentEntry { get; set; } + public RemapSegment[] Segments { get; set; } + + public RemapStream(Stream baseStream, MapEntry[] entries, int segmentCount) + { + BaseStream = baseStream; + MapEntries = entries; + Segments = new RemapSegment[segmentCount]; + + int entryIdx = 0; + for (int i = 0; i < segmentCount; i++) + { + var seg = new RemapSegment(); + seg.Entries.Add(MapEntries[entryIdx]); + seg.Offset = MapEntries[entryIdx].VirtualOffset; + MapEntries[entryIdx].Segment = seg; + entryIdx++; + + while (entryIdx < MapEntries.Length && + MapEntries[entryIdx - 1].VirtualOffsetEnd == MapEntries[entryIdx].VirtualOffset) + { + MapEntries[entryIdx].Segment = seg; + MapEntries[entryIdx - 1].Next = MapEntries[entryIdx]; + seg.Entries.Add(MapEntries[entryIdx]); + entryIdx++; + } + + seg.Length = seg.Entries[seg.Entries.Count - 1].VirtualOffsetEnd - seg.Entries[0].VirtualOffset; + Segments[i] = seg; + } + + CurrentEntry = GetMapEntry(0); + UpdateBaseStreamPosition(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + if (CurrentEntry == null) return 0; + long remaining = CurrentEntry.Segment.Offset + CurrentEntry.Segment.Length - Position; + if (remaining <= 0) return 0; + if (remaining < count) count = (int)remaining; + + var toOutput = count; + int pos = 0; + + while (toOutput > 0) + { + var remainInEntry = CurrentEntry.VirtualOffsetEnd - Position; + int toRead = (int)Math.Min(toOutput, remainInEntry); + BaseStream.Read(buffer, pos, toRead); + pos += toRead; + toOutput -= toRead; + Position += toRead; + } + + return count; + } + + public override long Seek(long offset, SeekOrigin origin) + { + switch (origin) + { + case SeekOrigin.Begin: + Position = offset; + break; + case SeekOrigin.Current: + Position += offset; + break; + case SeekOrigin.End: + Position = Length - offset; + break; + } + + return Position; + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + public override void Flush() + { + throw new NotImplementedException(); + } + + private MapEntry GetMapEntry(long offset) + { + // todo: is O(n) search a possible performance issue? + var entry = MapEntries.FirstOrDefault(x => offset >= x.VirtualOffset && offset < x.VirtualOffsetEnd); + if (entry == null) throw new ArgumentOutOfRangeException(nameof(offset)); + return entry; + } + + private void UpdateBaseStreamPosition() + { + // At end of virtual stream + if (CurrentEntry == null) return; + var entryOffset = Position - CurrentEntry.VirtualOffset; + BaseStream.Position = CurrentEntry.PhysicalOffset + entryOffset; + } + + public override bool CanRead => true; + public override bool CanSeek => true; + public override bool CanWrite => false; + public override long Length { get; } = -1; + + public override long Position + { + get => _position; + set + { + // Avoid doing a search when reading sequentially + if (CurrentEntry != null && value == CurrentEntry.VirtualOffsetEnd) + { + CurrentEntry = CurrentEntry.Next; + } + else if (CurrentEntry == null || value < CurrentEntry.VirtualOffset || value > CurrentEntry.VirtualOffsetEnd) + { + CurrentEntry = GetMapEntry(value); + } + + _position = value; + UpdateBaseStreamPosition(); + } + } + } + + public class RemapSegment + { + public List Entries { get; } = new List(); + public long Offset { get; internal set; } + public long Length { get; internal set; } + } +} diff --git a/libhac/Savefile/Savefile.cs b/libhac/Savefile/Savefile.cs new file mode 100644 index 00000000..5fb656a6 --- /dev/null +++ b/libhac/Savefile/Savefile.cs @@ -0,0 +1,22 @@ +using System.IO; +using System.Text; + +namespace libhac.Savefile +{ + public class Savefile + { + public Header Header { get; } + public RemapStream FileRemap { get; } + + public Savefile(Stream file) + { + using (var reader = new BinaryReader(file, Encoding.Default, true)) + { + Header = new Header(reader); + FileRemap = new RemapStream( + new SubStream(file, Header.Layout.FileMapDataOffset, Header.Layout.FileMapDataSize), + Header.FileMapEntries, Header.FileRemap.MapSegmentCount); + } + } + } +}