Process the remapping layer of save data

This commit is contained in:
Alex Barney 2018-07-19 18:31:35 -05:00
parent 5c3e4af4be
commit 8eeed95639
5 changed files with 394 additions and 1 deletions

View file

@ -34,7 +34,8 @@ namespace hactoolnet
Pfs0, Pfs0,
Romfs, Romfs,
Nax0, Nax0,
SwitchFs SwitchFs,
Save
} }
internal class Context internal class Context

View file

@ -3,6 +3,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using libhac; using libhac;
using libhac.Savefile;
namespace hactoolnet namespace hactoolnet
{ {
@ -40,6 +41,9 @@ namespace hactoolnet
case FileType.SwitchFs: case FileType.SwitchFs:
ProcessSwitchFs(ctx); ProcessSwitchFs(ctx);
break; break;
case FileType.Save:
ProcessSave(ctx);
break;
default: default:
throw new ArgumentOutOfRangeException(); 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 // For running random stuff
// ReSharper disable once UnusedParameter.Local // ReSharper disable once UnusedParameter.Local
private static void CustomTask(Context ctx) private static void CustomTask(Context ctx)

175
libhac/Savefile/Header.cs Normal file
View file

@ -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();
}
}
}

View file

@ -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<MapEntry> Entries { get; } = new List<MapEntry>();
public long Offset { get; internal set; }
public long Length { get; internal set; }
}
}

View file

@ -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);
}
}
}
}