mirror of
https://github.com/Thealexbarney/LibHac.git
synced 2024-11-14 10:49:41 +01:00
Process the remapping layer of save data
This commit is contained in:
parent
5c3e4af4be
commit
8eeed95639
5 changed files with 394 additions and 1 deletions
|
@ -34,7 +34,8 @@ namespace hactoolnet
|
||||||
Pfs0,
|
Pfs0,
|
||||||
Romfs,
|
Romfs,
|
||||||
Nax0,
|
Nax0,
|
||||||
SwitchFs
|
SwitchFs,
|
||||||
|
Save
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class Context
|
internal class Context
|
||||||
|
|
|
@ -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
175
libhac/Savefile/Header.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
152
libhac/Savefile/RemapStream.cs
Normal file
152
libhac/Savefile/RemapStream.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
22
libhac/Savefile/Savefile.cs
Normal file
22
libhac/Savefile/Savefile.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue