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,
|
||||
Romfs,
|
||||
Nax0,
|
||||
SwitchFs
|
||||
SwitchFs,
|
||||
Save
|
||||
}
|
||||
|
||||
internal class Context
|
||||
|
|
|
@ -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)
|
||||
|
|
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