mirror of
https://github.com/Thealexbarney/LibHac.git
synced 2025-02-09 13:14:46 +01:00
Merge pull request #52 from Thealexbarney/save-edit
Add more support for savedata FS editing
This commit is contained in:
commit
6764dc7800
@ -1,47 +1,442 @@
|
||||
using System.IO;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace LibHac.IO.Save
|
||||
{
|
||||
public class AllocationTable
|
||||
{
|
||||
private const int FreeListEntryIndex = 0;
|
||||
private const int EntrySize = 8;
|
||||
|
||||
private IStorage BaseStorage { get; }
|
||||
private IStorage HeaderStorage { get; }
|
||||
|
||||
public AllocationTableEntry[] Entries { get; }
|
||||
public AllocationTableHeader Header { get; }
|
||||
|
||||
public IStorage GetBaseStorage() => BaseStorage.AsReadOnly();
|
||||
public IStorage GetHeaderStorage() => HeaderStorage.AsReadOnly();
|
||||
|
||||
public AllocationTable(IStorage storage, IStorage header)
|
||||
{
|
||||
BaseStorage = storage;
|
||||
HeaderStorage = header;
|
||||
Header = new AllocationTableHeader(HeaderStorage);
|
||||
}
|
||||
|
||||
Stream tableStream = storage.AsStream();
|
||||
public void ReadEntry(int blockIndex, out int next, out int previous, out int length)
|
||||
{
|
||||
int entryIndex = BlockToEntryIndex(blockIndex);
|
||||
|
||||
// The first entry in the table is reserved. Block 0 is at table index 1
|
||||
int blockCount = (int)(Header.AllocationTableBlockCount) + 1;
|
||||
Span<AllocationTableEntry> entries = stackalloc AllocationTableEntry[2];
|
||||
ReadEntries(entryIndex, entries);
|
||||
|
||||
Entries = new AllocationTableEntry[blockCount];
|
||||
tableStream.Position = 0;
|
||||
var reader = new BinaryReader(tableStream);
|
||||
|
||||
for (int i = 0; i < blockCount; i++)
|
||||
if (entries[0].IsSingleBlockSegment())
|
||||
{
|
||||
int parent = reader.ReadInt32();
|
||||
int child = reader.ReadInt32();
|
||||
length = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
length = entries[1].Next - entryIndex + 1;
|
||||
}
|
||||
|
||||
Entries[i] = new AllocationTableEntry { Next = child, Prev = parent };
|
||||
if (entries[0].IsListEnd())
|
||||
{
|
||||
next = -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
next = EntryIndexToBlock(entries[0].GetNext());
|
||||
}
|
||||
|
||||
if (entries[0].IsListStart())
|
||||
{
|
||||
previous = -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
previous = EntryIndexToBlock(entries[0].GetPrev());
|
||||
}
|
||||
}
|
||||
|
||||
public IStorage GetBaseStorage() => BaseStorage.AsReadOnly();
|
||||
public IStorage GetHeaderStorage() => HeaderStorage.AsReadOnly();
|
||||
public int GetFreeListBlockIndex()
|
||||
{
|
||||
return EntryIndexToBlock(GetFreeListEntryIndex());
|
||||
}
|
||||
|
||||
public void SetFreeListBlockIndex(int headBlockIndex)
|
||||
{
|
||||
SetFreeListEntryIndex(BlockToEntryIndex(headBlockIndex));
|
||||
}
|
||||
|
||||
public int GetFreeListEntryIndex()
|
||||
{
|
||||
AllocationTableEntry freeList = ReadEntry(FreeListEntryIndex);
|
||||
return freeList.GetNext();
|
||||
}
|
||||
|
||||
public void SetFreeListEntryIndex(int headBlockIndex)
|
||||
{
|
||||
var freeList = new AllocationTableEntry { Next = headBlockIndex };
|
||||
WriteEntry(FreeListEntryIndex, freeList);
|
||||
}
|
||||
|
||||
public int Allocate(int blockCount)
|
||||
{
|
||||
if (blockCount <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(blockCount));
|
||||
}
|
||||
|
||||
int freeList = GetFreeListBlockIndex();
|
||||
|
||||
int newFreeList = Trim(freeList, blockCount);
|
||||
if (newFreeList == -1) return -1;
|
||||
|
||||
SetFreeListBlockIndex(newFreeList);
|
||||
|
||||
return freeList;
|
||||
}
|
||||
|
||||
public void Free(int listBlockIndex)
|
||||
{
|
||||
int listEntryIndex = BlockToEntryIndex(listBlockIndex);
|
||||
AllocationTableEntry listEntry = ReadEntry(listEntryIndex);
|
||||
|
||||
if (!listEntry.IsListStart())
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(listBlockIndex), "The block to free must be the start of a list.");
|
||||
}
|
||||
|
||||
int freeListIndex = GetFreeListEntryIndex();
|
||||
|
||||
// Free list is empty
|
||||
if (freeListIndex == 0)
|
||||
{
|
||||
SetFreeListEntryIndex(listEntryIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
Join(listBlockIndex, EntryIndexToBlock(freeListIndex));
|
||||
|
||||
SetFreeListBlockIndex(listBlockIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Combines 2 lists into one list. The second list will be attached to the end of the first list.
|
||||
/// </summary>
|
||||
/// <param name="frontListBlockIndex">The index of the start block of the first list.</param>
|
||||
/// <param name="backListBlockIndex">The index of the start block of the second list.</param>
|
||||
public void Join(int frontListBlockIndex, int backListBlockIndex)
|
||||
{
|
||||
int frontEntryIndex = BlockToEntryIndex(frontListBlockIndex);
|
||||
int backEntryIndex = BlockToEntryIndex(backListBlockIndex);
|
||||
|
||||
int frontTailIndex = GetListTail(frontEntryIndex);
|
||||
|
||||
AllocationTableEntry frontTail = ReadEntry(frontTailIndex);
|
||||
AllocationTableEntry backHead = ReadEntry(backEntryIndex);
|
||||
|
||||
frontTail.SetNext(backEntryIndex);
|
||||
backHead.SetPrev(frontTailIndex);
|
||||
|
||||
WriteEntry(frontTailIndex, frontTail);
|
||||
WriteEntry(backEntryIndex, backHead);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trims an existing list to the specified length and returns the excess blocks as a new list.
|
||||
/// </summary>
|
||||
/// <param name="listHeadBlockIndex">The starting block of the list to trim.</param>
|
||||
/// <param name="newListLength">The length in blocks that the list will be shortened to.</param>
|
||||
/// <returns>The index of the head node of the removed blocks.</returns>
|
||||
public int Trim(int listHeadBlockIndex, int newListLength)
|
||||
{
|
||||
int blocksRemaining = newListLength;
|
||||
int next = BlockToEntryIndex(listHeadBlockIndex);
|
||||
int listAIndex = -1;
|
||||
int listBIndex = -1;
|
||||
|
||||
while (blocksRemaining > 0)
|
||||
{
|
||||
if (next < 0)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
int currentEntryIndex = next;
|
||||
|
||||
ReadEntry(EntryIndexToBlock(currentEntryIndex), out next, out int _, out int segmentLength);
|
||||
|
||||
next = BlockToEntryIndex(next);
|
||||
|
||||
if (segmentLength == blocksRemaining)
|
||||
{
|
||||
listAIndex = currentEntryIndex;
|
||||
listBIndex = next;
|
||||
}
|
||||
else if (segmentLength > blocksRemaining)
|
||||
{
|
||||
Split(EntryIndexToBlock(currentEntryIndex), blocksRemaining);
|
||||
|
||||
listAIndex = currentEntryIndex;
|
||||
listBIndex = currentEntryIndex + blocksRemaining;
|
||||
}
|
||||
|
||||
blocksRemaining -= segmentLength;
|
||||
}
|
||||
|
||||
if (listAIndex == -1 || listBIndex == -1) return -1;
|
||||
|
||||
AllocationTableEntry listANode = ReadEntry(listAIndex);
|
||||
AllocationTableEntry listBNode = ReadEntry(listBIndex);
|
||||
|
||||
listANode.SetNext(0);
|
||||
listBNode.MakeListStart();
|
||||
|
||||
WriteEntry(listAIndex, listANode);
|
||||
WriteEntry(listBIndex, listBNode);
|
||||
|
||||
return EntryIndexToBlock(listBIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Splits a single list segment into 2 segments. The sequence of blocks in the full list will remain the same.
|
||||
/// </summary>
|
||||
/// <param name="segmentBlockIndex">The block index of the segment to split.</param>
|
||||
/// <param name="firstSubSegmentLength">The length of the first subsegment.</param>
|
||||
public void Split(int segmentBlockIndex, int firstSubSegmentLength)
|
||||
{
|
||||
Debug.Assert(firstSubSegmentLength > 0);
|
||||
|
||||
int segAIndex = BlockToEntryIndex(segmentBlockIndex);
|
||||
|
||||
AllocationTableEntry segA = ReadEntry(segAIndex);
|
||||
if (!segA.IsMultiBlockSegment()) throw new ArgumentException("Cannot split a single-entry segment.");
|
||||
|
||||
AllocationTableEntry segARange = ReadEntry(segAIndex + 1);
|
||||
int originalLength = segARange.GetNext() - segARange.GetPrev() + 1;
|
||||
|
||||
if (firstSubSegmentLength >= originalLength)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(firstSubSegmentLength),
|
||||
$"Requested sub-segment length ({firstSubSegmentLength}) must be less than the full segment length ({originalLength})");
|
||||
}
|
||||
|
||||
int segBIndex = segAIndex + firstSubSegmentLength;
|
||||
|
||||
int segALength = firstSubSegmentLength;
|
||||
int segBLength = originalLength - segALength;
|
||||
|
||||
var segB = new AllocationTableEntry();
|
||||
|
||||
// Insert segment B between segments A and C
|
||||
segB.SetPrev(segAIndex);
|
||||
segB.SetNext(segA.GetNext());
|
||||
segA.SetNext(segBIndex);
|
||||
|
||||
if (!segB.IsListEnd())
|
||||
{
|
||||
AllocationTableEntry segC = ReadEntry(segB.GetNext());
|
||||
segC.SetPrev(segBIndex);
|
||||
WriteEntry(segB.GetNext(), segC);
|
||||
}
|
||||
|
||||
// Write the new range entries if needed
|
||||
if (segBLength > 1)
|
||||
{
|
||||
segB.MakeMultiBlockSegment();
|
||||
|
||||
var segBRange = new AllocationTableEntry();
|
||||
segBRange.SetRange(segBIndex, segBIndex + segBLength - 1);
|
||||
|
||||
WriteEntry(segBIndex + 1, segBRange);
|
||||
WriteEntry(segBIndex + segBLength - 1, segBRange);
|
||||
}
|
||||
|
||||
WriteEntry(segBIndex, segB);
|
||||
|
||||
if (segALength == 1)
|
||||
{
|
||||
segA.MakeSingleBlockSegment();
|
||||
}
|
||||
else
|
||||
{
|
||||
segARange.SetRange(segAIndex, segAIndex + segALength - 1);
|
||||
|
||||
WriteEntry(segAIndex + 1, segARange);
|
||||
WriteEntry(segAIndex + segALength - 1, segARange);
|
||||
}
|
||||
|
||||
WriteEntry(segAIndex, segA);
|
||||
}
|
||||
|
||||
public int GetListLength(int blockIndex)
|
||||
{
|
||||
int index = blockIndex;
|
||||
int totalLength = 0;
|
||||
|
||||
int tableSize = Header.AllocationTableBlockCount;
|
||||
int nodesIterated = 0;
|
||||
|
||||
while (index != -1)
|
||||
{
|
||||
ReadEntry(index, out index, out int _, out int length);
|
||||
|
||||
totalLength += length;
|
||||
nodesIterated++;
|
||||
|
||||
if (nodesIterated > tableSize)
|
||||
{
|
||||
throw new InvalidDataException("Cycle detected in allocation table.");
|
||||
}
|
||||
}
|
||||
|
||||
return totalLength;
|
||||
}
|
||||
|
||||
public void FsTrimList(int blockIndex)
|
||||
{
|
||||
int index = blockIndex;
|
||||
|
||||
int tableSize = Header.AllocationTableBlockCount;
|
||||
int nodesIterated = 0;
|
||||
|
||||
while (index != -1)
|
||||
{
|
||||
ReadEntry(index, out int next, out int _, out int length);
|
||||
|
||||
if (length > 3)
|
||||
{
|
||||
int fillOffset = BlockToEntryIndex(index + 2) * EntrySize;
|
||||
int fillLength = (length - 3) * EntrySize;
|
||||
|
||||
BaseStorage.Slice(fillOffset, fillLength).Fill(0x00);
|
||||
}
|
||||
|
||||
nodesIterated++;
|
||||
|
||||
if (nodesIterated > tableSize)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
index = next;
|
||||
}
|
||||
}
|
||||
|
||||
public void FsTrim()
|
||||
{
|
||||
int tableSize = BlockToEntryIndex(Header.AllocationTableBlockCount) * EntrySize;
|
||||
BaseStorage.Slice(tableSize).Fill(0x00);
|
||||
}
|
||||
|
||||
private void ReadEntries(int entryIndex, Span<AllocationTableEntry> entries)
|
||||
{
|
||||
Debug.Assert(entries.Length >= 2);
|
||||
|
||||
bool isLastBlock = entryIndex == BlockToEntryIndex(Header.AllocationTableBlockCount) - 1;
|
||||
int entriesToRead = isLastBlock ? 1 : 2;
|
||||
int offset = entryIndex * EntrySize;
|
||||
|
||||
Span<byte> buffer = MemoryMarshal.Cast<AllocationTableEntry, byte>(entries.Slice(0, entriesToRead));
|
||||
|
||||
BaseStorage.Read(buffer, offset);
|
||||
}
|
||||
|
||||
private AllocationTableEntry ReadEntry(int entryIndex)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[EntrySize];
|
||||
int offset = entryIndex * EntrySize;
|
||||
|
||||
BaseStorage.Read(bytes, offset);
|
||||
|
||||
return GetEntryFromBytes(bytes);
|
||||
}
|
||||
|
||||
private void WriteEntry(int entryIndex, AllocationTableEntry entry)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[EntrySize];
|
||||
int offset = entryIndex * EntrySize;
|
||||
|
||||
ref AllocationTableEntry newEntry = ref GetEntryFromBytes(bytes);
|
||||
newEntry = entry;
|
||||
|
||||
BaseStorage.Write(bytes, offset);
|
||||
}
|
||||
|
||||
// ReSharper disable once UnusedMember.Local
|
||||
private int GetListHead(int entryIndex)
|
||||
{
|
||||
int headIndex = entryIndex;
|
||||
int tableSize = Header.AllocationTableBlockCount;
|
||||
int nodesTraversed = 0;
|
||||
|
||||
AllocationTableEntry entry = ReadEntry(entryIndex);
|
||||
|
||||
while (!entry.IsListStart())
|
||||
{
|
||||
nodesTraversed++;
|
||||
headIndex = entry.Prev & 0x7FFFFFFF;
|
||||
entry = ReadEntry(headIndex);
|
||||
|
||||
if (nodesTraversed > tableSize)
|
||||
{
|
||||
throw new InvalidDataException("Cycle detected in allocation table.");
|
||||
}
|
||||
}
|
||||
|
||||
return headIndex;
|
||||
}
|
||||
|
||||
private int GetListTail(int entryIndex)
|
||||
{
|
||||
int tailIndex = entryIndex;
|
||||
int tableSize = Header.AllocationTableBlockCount;
|
||||
int nodesTraversed = 0;
|
||||
|
||||
AllocationTableEntry entry = ReadEntry(entryIndex);
|
||||
|
||||
while (!entry.IsListEnd())
|
||||
{
|
||||
nodesTraversed++;
|
||||
tailIndex = entry.Next & 0x7FFFFFFF;
|
||||
entry = ReadEntry(tailIndex);
|
||||
|
||||
if (nodesTraversed > tableSize)
|
||||
{
|
||||
throw new InvalidDataException("Cycle detected in allocation table.");
|
||||
}
|
||||
}
|
||||
|
||||
return tailIndex;
|
||||
}
|
||||
|
||||
private static ref AllocationTableEntry GetEntryFromBytes(Span<byte> entry)
|
||||
{
|
||||
return ref MemoryMarshal.Cast<byte, AllocationTableEntry>(entry)[0];
|
||||
}
|
||||
|
||||
private static int EntryIndexToBlock(int entryIndex) => entryIndex - 1;
|
||||
private static int BlockToEntryIndex(int blockIndex) => blockIndex + 1;
|
||||
}
|
||||
|
||||
public class AllocationTableEntry
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct AllocationTableEntry
|
||||
{
|
||||
public int Prev { get; set; }
|
||||
public int Next { get; set; }
|
||||
public int Prev;
|
||||
public int Next;
|
||||
|
||||
public int GetPrev()
|
||||
{
|
||||
return Prev & 0x7FFFFFFF;
|
||||
}
|
||||
|
||||
public int GetNext()
|
||||
{
|
||||
return Next & 0x7FFFFFFF;
|
||||
}
|
||||
|
||||
public bool IsListStart()
|
||||
{
|
||||
@ -58,17 +453,61 @@ namespace LibHac.IO.Save
|
||||
return Next < 0;
|
||||
}
|
||||
|
||||
public void MakeMultiBlockSegment()
|
||||
{
|
||||
Next |= unchecked((int)0x80000000);
|
||||
}
|
||||
|
||||
public void MakeSingleBlockSegment()
|
||||
{
|
||||
Next &= 0x7FFFFFFF;
|
||||
}
|
||||
|
||||
public bool IsSingleBlockSegment()
|
||||
{
|
||||
return Next >= 0;
|
||||
}
|
||||
|
||||
public void MakeListStart()
|
||||
{
|
||||
Prev = int.MinValue;
|
||||
}
|
||||
|
||||
public void MakeRangeEntry()
|
||||
{
|
||||
Prev |= unchecked((int)0x80000000);
|
||||
}
|
||||
|
||||
public void SetNext(int value)
|
||||
{
|
||||
Debug.Assert(value >= 0);
|
||||
|
||||
Next = Next & unchecked((int)0x80000000) | value;
|
||||
}
|
||||
|
||||
public void SetPrev(int value)
|
||||
{
|
||||
Debug.Assert(value >= 0);
|
||||
|
||||
Prev = value;
|
||||
}
|
||||
|
||||
public void SetRange(int startIndex, int endIndex)
|
||||
{
|
||||
Debug.Assert(startIndex > 0);
|
||||
Debug.Assert(endIndex > 0);
|
||||
|
||||
Next = endIndex;
|
||||
Prev = startIndex;
|
||||
MakeRangeEntry();
|
||||
}
|
||||
}
|
||||
|
||||
public class AllocationTableHeader
|
||||
{
|
||||
public long BlockSize { get; }
|
||||
public long AllocationTableOffset { get; }
|
||||
public long AllocationTableBlockCount { get; }
|
||||
public int AllocationTableBlockCount { get; }
|
||||
public long DataOffset { get; }
|
||||
public long DataBlockCount { get; }
|
||||
public int DirectoryTableBlock { get; }
|
||||
|
@ -8,7 +8,11 @@ namespace LibHac.IO.Save
|
||||
|
||||
public int VirtualBlock { get; private set; }
|
||||
public int PhysicalBlock { get; private set; }
|
||||
public int CurrentSegmentSize { get; private set; }
|
||||
public int CurrentSegmentSize => _currentSegmentSize;
|
||||
|
||||
private int _nextBlock;
|
||||
private int _prevBlock;
|
||||
private int _currentSegmentSize;
|
||||
|
||||
public AllocationTableIterator(AllocationTable table, int initialBlock)
|
||||
{
|
||||
@ -22,71 +26,34 @@ namespace LibHac.IO.Save
|
||||
|
||||
public bool BeginIteration(int initialBlock)
|
||||
{
|
||||
AllocationTableEntry tableEntry = Fat.Entries[initialBlock + 1];
|
||||
|
||||
if (!tableEntry.IsListStart() && initialBlock != -1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (tableEntry.IsSingleBlockSegment())
|
||||
{
|
||||
CurrentSegmentSize = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
AllocationTableEntry lengthEntry = Fat.Entries[initialBlock + 2];
|
||||
CurrentSegmentSize = lengthEntry.Next - initialBlock;
|
||||
}
|
||||
|
||||
PhysicalBlock = initialBlock;
|
||||
Fat.ReadEntry(initialBlock, out _nextBlock, out _prevBlock, out _currentSegmentSize);
|
||||
|
||||
return true;
|
||||
return _prevBlock == -1;
|
||||
}
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
AllocationTableEntry currentEntry = Fat.Entries[PhysicalBlock + 1];
|
||||
if (currentEntry.IsListEnd()) return false;
|
||||
int newBlock = currentEntry.Next & 0x7FFFFFFF;
|
||||
if (_nextBlock == -1) return false;
|
||||
|
||||
AllocationTableEntry newEntry = Fat.Entries[newBlock];
|
||||
VirtualBlock += CurrentSegmentSize;
|
||||
VirtualBlock += _currentSegmentSize;
|
||||
PhysicalBlock = _nextBlock;
|
||||
|
||||
if (newEntry.IsSingleBlockSegment())
|
||||
{
|
||||
CurrentSegmentSize = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
AllocationTableEntry lengthEntry = Fat.Entries[newBlock + 1];
|
||||
CurrentSegmentSize = lengthEntry.Next - (newBlock - 1);
|
||||
}
|
||||
|
||||
PhysicalBlock = newBlock - 1;
|
||||
Fat.ReadEntry(_nextBlock, out _nextBlock, out _prevBlock, out _currentSegmentSize);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool MovePrevious()
|
||||
{
|
||||
AllocationTableEntry currentEntry = Fat.Entries[PhysicalBlock + 1];
|
||||
if (currentEntry.IsListStart()) return false;
|
||||
int newBlock = currentEntry.Prev & 0x7FFFFFFF;
|
||||
if (_prevBlock == -1) return false;
|
||||
|
||||
AllocationTableEntry newEntry = Fat.Entries[newBlock];
|
||||
PhysicalBlock = _prevBlock;
|
||||
|
||||
if (newEntry.IsSingleBlockSegment())
|
||||
{
|
||||
CurrentSegmentSize = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
AllocationTableEntry lengthEntry = Fat.Entries[newBlock + 1];
|
||||
CurrentSegmentSize = lengthEntry.Next - (newBlock - 1);
|
||||
}
|
||||
Fat.ReadEntry(_prevBlock, out _nextBlock, out _prevBlock, out _currentSegmentSize);
|
||||
|
||||
VirtualBlock -= CurrentSegmentSize;
|
||||
PhysicalBlock = newBlock - 1;
|
||||
VirtualBlock -= _currentSegmentSize;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -6,18 +6,19 @@ namespace LibHac.IO.Save
|
||||
{
|
||||
private IStorage BaseStorage { get; }
|
||||
private int BlockSize { get; }
|
||||
private int InitialBlock { get; }
|
||||
internal int InitialBlock { get; private set; }
|
||||
private AllocationTable Fat { get; }
|
||||
|
||||
private long _length;
|
||||
|
||||
public AllocationTableStorage(IStorage data, AllocationTable table, int blockSize, int initialBlock, long length)
|
||||
public AllocationTableStorage(IStorage data, AllocationTable table, int blockSize, int initialBlock)
|
||||
{
|
||||
BaseStorage = data;
|
||||
BlockSize = blockSize;
|
||||
_length = length;
|
||||
Fat = table;
|
||||
InitialBlock = initialBlock;
|
||||
|
||||
_length = initialBlock == -1 ? 0 : table.GetListLength(initialBlock) * blockSize;
|
||||
}
|
||||
|
||||
protected override void ReadImpl(Span<byte> destination, long offset)
|
||||
@ -80,5 +81,44 @@ namespace LibHac.IO.Save
|
||||
}
|
||||
|
||||
public override long GetSize() => _length;
|
||||
|
||||
public override void SetSize(long size)
|
||||
{
|
||||
int oldBlockCount = (int)Util.DivideByRoundUp(_length, BlockSize);
|
||||
int newBlockCount = (int)Util.DivideByRoundUp(size, BlockSize);
|
||||
|
||||
if (oldBlockCount == newBlockCount) return;
|
||||
|
||||
if (oldBlockCount == 0)
|
||||
{
|
||||
InitialBlock = Fat.Allocate(newBlockCount);
|
||||
_length = newBlockCount * BlockSize;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (newBlockCount == 0)
|
||||
{
|
||||
Fat.Free(InitialBlock);
|
||||
|
||||
InitialBlock = -1;
|
||||
_length = 0;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (newBlockCount > oldBlockCount)
|
||||
{
|
||||
int newBlocks = Fat.Allocate(newBlockCount - oldBlockCount);
|
||||
Fat.Join(InitialBlock, newBlocks);
|
||||
}
|
||||
else
|
||||
{
|
||||
int oldBlocks = Fat.Trim(InitialBlock, newBlockCount);
|
||||
Fat.Free(oldBlocks);
|
||||
}
|
||||
|
||||
_length = newBlockCount * BlockSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,26 +1,31 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace LibHac.IO.Save
|
||||
{
|
||||
public class HierarchicalSaveFileTable
|
||||
{
|
||||
private SaveFsList<FileSaveEntry> FileTable { get; }
|
||||
private SaveFsList<DirectorySaveEntry> DirectoryTable { get; }
|
||||
private SaveFsList<TableEntry<SaveFileInfo>> FileTable { get; }
|
||||
private SaveFsList<TableEntry<SaveFindPosition>> DirectoryTable { get; }
|
||||
|
||||
public HierarchicalSaveFileTable(IStorage dirTable, IStorage fileTable)
|
||||
{
|
||||
FileTable = new SaveFsList<FileSaveEntry>(fileTable);
|
||||
DirectoryTable = new SaveFsList<DirectorySaveEntry>(dirTable);
|
||||
FileTable = new SaveFsList<TableEntry<SaveFileInfo>>(fileTable);
|
||||
DirectoryTable = new SaveFsList<TableEntry<SaveFindPosition>>(dirTable);
|
||||
}
|
||||
|
||||
public bool TryOpenFile(string path, out SaveFileInfo fileInfo)
|
||||
{
|
||||
FindPathRecursive(Util.GetUtf8Bytes(path), out SaveEntryKey key);
|
||||
|
||||
if (FileTable.TryGetValue(ref key, out FileSaveEntry value))
|
||||
if (!FindPathRecursive(Util.GetUtf8Bytes(path), out SaveEntryKey key))
|
||||
{
|
||||
fileInfo = value.Info;
|
||||
fileInfo = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (FileTable.TryGetValue(ref key, out TableEntry<SaveFileInfo> value))
|
||||
{
|
||||
fileInfo = value.Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -39,7 +44,7 @@ namespace LibHac.IO.Save
|
||||
|
||||
Span<byte> nameBytes = stackalloc byte[FileTable.MaxNameLength];
|
||||
|
||||
bool success = FileTable.TryGetValue((int)position.NextFile, out FileSaveEntry entry, ref nameBytes);
|
||||
bool success = FileTable.TryGetValue(position.NextFile, out TableEntry<SaveFileInfo> entry, ref nameBytes);
|
||||
|
||||
// todo error message
|
||||
if (!success)
|
||||
@ -50,7 +55,7 @@ namespace LibHac.IO.Save
|
||||
}
|
||||
|
||||
position.NextFile = entry.NextSibling;
|
||||
info = entry.Info;
|
||||
info = entry.Value;
|
||||
|
||||
name = Util.GetUtf8StringNullTerminated(nameBytes);
|
||||
|
||||
@ -65,9 +70,9 @@ namespace LibHac.IO.Save
|
||||
return false;
|
||||
}
|
||||
|
||||
Span<byte> nameBytes = stackalloc byte[FileTable.MaxNameLength];
|
||||
Span<byte> nameBytes = stackalloc byte[DirectoryTable.MaxNameLength];
|
||||
|
||||
bool success = DirectoryTable.TryGetValue(position.NextDirectory, out DirectorySaveEntry entry, ref nameBytes);
|
||||
bool success = DirectoryTable.TryGetValue(position.NextDirectory, out TableEntry<SaveFindPosition> entry, ref nameBytes);
|
||||
|
||||
// todo error message
|
||||
if (!success)
|
||||
@ -83,13 +88,129 @@ namespace LibHac.IO.Save
|
||||
return true;
|
||||
}
|
||||
|
||||
public void AddFile(string path, ref SaveFileInfo fileInfo)
|
||||
{
|
||||
path = PathTools.Normalize(path);
|
||||
ReadOnlySpan<byte> pathBytes = Util.GetUtf8Bytes(path);
|
||||
|
||||
if (path == "/") throw new ArgumentException("Path cannot be empty");
|
||||
|
||||
CreateFileRecursiveInternal(pathBytes, ref fileInfo);
|
||||
}
|
||||
|
||||
private void CreateFileRecursiveInternal(ReadOnlySpan<byte> path, ref SaveFileInfo fileInfo)
|
||||
{
|
||||
var parser = new PathParser(path);
|
||||
var key = new SaveEntryKey(parser.GetCurrent(), 0);
|
||||
|
||||
int prevIndex = 0;
|
||||
|
||||
while (!parser.IsFinished())
|
||||
{
|
||||
int index = DirectoryTable.GetIndexFromKey(ref key).Index;
|
||||
|
||||
if (index < 0)
|
||||
{
|
||||
var newEntry = new TableEntry<SaveFindPosition>();
|
||||
index = DirectoryTable.Add(ref key, ref newEntry);
|
||||
|
||||
if (prevIndex > 0)
|
||||
{
|
||||
DirectoryTable.GetValue(prevIndex, out TableEntry<SaveFindPosition> parentEntry);
|
||||
|
||||
newEntry.NextSibling = parentEntry.Value.NextDirectory;
|
||||
parentEntry.Value.NextDirectory = index;
|
||||
|
||||
DirectoryTable.SetValue(prevIndex, ref parentEntry);
|
||||
DirectoryTable.SetValue(index, ref newEntry);
|
||||
}
|
||||
}
|
||||
|
||||
prevIndex = index;
|
||||
key.Parent = index;
|
||||
parser.TryGetNext(out key.Name);
|
||||
}
|
||||
|
||||
{
|
||||
int index = FileTable.GetIndexFromKey(ref key).Index;
|
||||
var fileEntry = new TableEntry<SaveFileInfo>();
|
||||
|
||||
if (index < 0)
|
||||
{
|
||||
index = FileTable.Add(ref key, ref fileEntry);
|
||||
|
||||
DirectoryTable.GetValue(prevIndex, out TableEntry<SaveFindPosition> parentEntry);
|
||||
|
||||
fileEntry.NextSibling = parentEntry.Value.NextFile;
|
||||
parentEntry.Value.NextFile = index;
|
||||
|
||||
DirectoryTable.SetValue(prevIndex, ref parentEntry);
|
||||
}
|
||||
|
||||
fileEntry.Value = fileInfo;
|
||||
FileTable.SetValue(index, ref fileEntry);
|
||||
}
|
||||
}
|
||||
|
||||
public void DeleteFile(string path)
|
||||
{
|
||||
path = PathTools.Normalize(path);
|
||||
ReadOnlySpan<byte> pathBytes = Util.GetUtf8Bytes(path);
|
||||
|
||||
FindPathRecursive(pathBytes, out SaveEntryKey key);
|
||||
int parentIndex = key.Parent;
|
||||
|
||||
DirectoryTable.GetValue(parentIndex, out TableEntry<SaveFindPosition> parentEntry);
|
||||
|
||||
int toDeleteIndex = FileTable.GetIndexFromKey(ref key).Index;
|
||||
if (toDeleteIndex < 0) throw new FileNotFoundException();
|
||||
|
||||
FileTable.GetValue(toDeleteIndex, out TableEntry<SaveFileInfo> toDeleteEntry);
|
||||
|
||||
if (parentEntry.Value.NextFile == toDeleteIndex)
|
||||
{
|
||||
parentEntry.Value.NextFile = toDeleteEntry.NextSibling;
|
||||
DirectoryTable.SetValue(parentIndex, ref parentEntry);
|
||||
FileTable.Remove(ref key);
|
||||
return;
|
||||
}
|
||||
|
||||
int prevIndex = parentEntry.Value.NextFile;
|
||||
FileTable.GetValue(prevIndex, out TableEntry<SaveFileInfo> prevEntry);
|
||||
int curIndex = prevEntry.NextSibling;
|
||||
|
||||
while (curIndex != 0)
|
||||
{
|
||||
FileTable.GetValue(curIndex, out TableEntry<SaveFileInfo> curEntry);
|
||||
|
||||
if (curIndex == toDeleteIndex)
|
||||
{
|
||||
prevEntry.NextSibling = curEntry.NextSibling;
|
||||
FileTable.SetValue(prevIndex, ref prevEntry);
|
||||
|
||||
FileTable.Remove(ref key);
|
||||
return;
|
||||
}
|
||||
|
||||
prevIndex = curIndex;
|
||||
prevEntry = curEntry;
|
||||
curIndex = prevEntry.NextSibling;
|
||||
}
|
||||
|
||||
throw new FileNotFoundException();
|
||||
}
|
||||
|
||||
public bool TryOpenDirectory(string path, out SaveFindPosition position)
|
||||
{
|
||||
FindPathRecursive(Util.GetUtf8Bytes(path), out SaveEntryKey key);
|
||||
|
||||
if (DirectoryTable.TryGetValue(ref key, out DirectorySaveEntry value))
|
||||
if (!FindPathRecursive(Util.GetUtf8Bytes(path), out SaveEntryKey key))
|
||||
{
|
||||
position = value.Pos;
|
||||
position = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (DirectoryTable.TryGetValue(ref key, out TableEntry<SaveFindPosition> entry))
|
||||
{
|
||||
position = entry.Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -97,32 +218,28 @@ namespace LibHac.IO.Save
|
||||
return false;
|
||||
}
|
||||
|
||||
private void FindPathRecursive(ReadOnlySpan<byte> path, out SaveEntryKey key)
|
||||
private bool FindPathRecursive(ReadOnlySpan<byte> path, out SaveEntryKey key)
|
||||
{
|
||||
var parser = new PathParser(path);
|
||||
key = new SaveEntryKey(parser.GetCurrent(), 0);
|
||||
|
||||
while (!parser.IsFinished())
|
||||
{
|
||||
key.Parent = DirectoryTable.GetOffsetFromKey(ref key);
|
||||
key.Parent = DirectoryTable.GetIndexFromKey(ref key).Index;
|
||||
|
||||
if (key.Parent < 0) return false;
|
||||
|
||||
parser.TryGetNext(out key.Name);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
private struct DirectorySaveEntry
|
||||
private struct TableEntry<T> where T : struct
|
||||
{
|
||||
public int NextSibling;
|
||||
public SaveFindPosition Pos;
|
||||
public long Field10;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
private struct FileSaveEntry
|
||||
{
|
||||
public int NextSibling;
|
||||
public SaveFileInfo Info;
|
||||
public long Field10;
|
||||
public T Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,18 +1,21 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace LibHac.IO.Save
|
||||
{
|
||||
public class SaveDataFile : FileBase
|
||||
{
|
||||
private AllocationTableStorage BaseStorage { get; }
|
||||
private long Offset { get; }
|
||||
private long Size { get; }
|
||||
private string Path { get; }
|
||||
private HierarchicalSaveFileTable FileTable { get; }
|
||||
private long Size { get; set; }
|
||||
|
||||
public SaveDataFile(AllocationTableStorage baseStorage, long offset, long size, OpenMode mode)
|
||||
public SaveDataFile(AllocationTableStorage baseStorage, string path, HierarchicalSaveFileTable fileTable, long size, OpenMode mode)
|
||||
{
|
||||
Mode = mode;
|
||||
BaseStorage = baseStorage;
|
||||
Offset = offset;
|
||||
Path = path;
|
||||
FileTable = fileTable;
|
||||
Size = size;
|
||||
}
|
||||
|
||||
@ -20,8 +23,7 @@ namespace LibHac.IO.Save
|
||||
{
|
||||
int toRead = ValidateReadParamsAndGetSize(destination, offset);
|
||||
|
||||
long storageOffset = Offset + offset;
|
||||
BaseStorage.Read(destination.Slice(0, toRead), storageOffset);
|
||||
BaseStorage.Read(destination.Slice(0, toRead), offset);
|
||||
|
||||
return toRead;
|
||||
}
|
||||
@ -45,7 +47,22 @@ namespace LibHac.IO.Save
|
||||
|
||||
public override void SetSize(long size)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
if (size < 0) throw new ArgumentOutOfRangeException(nameof(size));
|
||||
if (Size == size) return;
|
||||
|
||||
BaseStorage.SetSize(size);
|
||||
|
||||
if (!FileTable.TryOpenFile(Path, out SaveFileInfo fileInfo))
|
||||
{
|
||||
throw new FileNotFoundException();
|
||||
}
|
||||
|
||||
fileInfo.StartBlock = BaseStorage.InitialBlock;
|
||||
fileInfo.Length = size;
|
||||
|
||||
FileTable.AddFile(Path, ref fileInfo);
|
||||
|
||||
Size = size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,6 @@ namespace LibHac.IO.Save
|
||||
public IStorage BaseStorage { get; }
|
||||
public bool LeaveOpen { get; }
|
||||
|
||||
public HierarchicalIntegrityVerificationStorage IvfcStorage { get; }
|
||||
public SaveDataFileSystemCore SaveDataFileSystemCore { get; }
|
||||
|
||||
public RemapStorage DataRemapStorage { get; }
|
||||
@ -18,6 +17,9 @@ namespace LibHac.IO.Save
|
||||
public HierarchicalDuplexStorage DuplexStorage { get; }
|
||||
public JournalStorage JournalStorage { get; }
|
||||
|
||||
public HierarchicalIntegrityVerificationStorage JournalIvfcStorage { get; }
|
||||
public HierarchicalIntegrityVerificationStorage FatIvfcStorage { get; }
|
||||
|
||||
private Keyset Keyset { get; }
|
||||
|
||||
public SaveDataFileSystem(Keyset keyset, IStorage storage, IntegrityCheckLevel integrityCheckLevel, bool leaveOpen)
|
||||
@ -52,16 +54,17 @@ namespace LibHac.IO.Save
|
||||
|
||||
JournalStorage = new JournalStorage(journalData, Header.JournalHeader, journalMapInfo, leaveOpen);
|
||||
|
||||
IvfcStorage = InitJournalIvfcStorage(integrityCheckLevel);
|
||||
JournalIvfcStorage = InitJournalIvfcStorage(integrityCheckLevel);
|
||||
|
||||
IStorage fatStorage = MetaRemapStorage.Slice(layout.FatOffset, layout.FatSize);
|
||||
|
||||
if (Header.Layout.Version >= 0x50000)
|
||||
{
|
||||
fatStorage = InitFatIvfcStorage(integrityCheckLevel);
|
||||
FatIvfcStorage = InitFatIvfcStorage(integrityCheckLevel);
|
||||
fatStorage = FatIvfcStorage;
|
||||
}
|
||||
|
||||
SaveDataFileSystemCore = new SaveDataFileSystemCore(IvfcStorage, fatStorage, Header.SaveHeader);
|
||||
SaveDataFileSystemCore = new SaveDataFileSystemCore(JournalIvfcStorage, fatStorage, Header.SaveHeader);
|
||||
}
|
||||
|
||||
private static HierarchicalDuplexStorage InitDuplexStorage(IStorage baseStorage, Header header)
|
||||
@ -119,22 +122,22 @@ namespace LibHac.IO.Save
|
||||
|
||||
public void CreateDirectory(string path)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
SaveDataFileSystemCore.CreateDirectory(path);
|
||||
}
|
||||
|
||||
public void CreateFile(string path, long size, CreateFileOptions options)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
SaveDataFileSystemCore.CreateFile(path, size, options);
|
||||
}
|
||||
|
||||
public void DeleteDirectory(string path)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
SaveDataFileSystemCore.DeleteDirectory(path);
|
||||
}
|
||||
|
||||
public void DeleteFile(string path)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
SaveDataFileSystemCore.DeleteFile(path);
|
||||
}
|
||||
|
||||
public IDirectory OpenDirectory(string path, OpenDirectoryMode mode)
|
||||
@ -149,12 +152,12 @@ namespace LibHac.IO.Save
|
||||
|
||||
public void RenameDirectory(string srcPath, string dstPath)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
SaveDataFileSystemCore.RenameDirectory(srcPath, dstPath);
|
||||
}
|
||||
|
||||
public void RenameFile(string srcPath, string dstPath)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
SaveDataFileSystemCore.RenameFile(srcPath, dstPath);
|
||||
}
|
||||
|
||||
public bool DirectoryExists(string path) => SaveDataFileSystemCore.DirectoryExists(path);
|
||||
@ -172,6 +175,9 @@ namespace LibHac.IO.Save
|
||||
|
||||
public bool Commit(Keyset keyset)
|
||||
{
|
||||
JournalIvfcStorage.Flush();
|
||||
FatIvfcStorage.Flush();
|
||||
|
||||
Stream headerStream = BaseStorage.AsStream();
|
||||
|
||||
var hashData = new byte[0x3d00];
|
||||
@ -200,12 +206,25 @@ namespace LibHac.IO.Save
|
||||
return true;
|
||||
}
|
||||
|
||||
public void FsTrim()
|
||||
{
|
||||
SaveDataFileSystemCore.FsTrim();
|
||||
}
|
||||
|
||||
public Validity Verify(IProgressReport logger = null)
|
||||
{
|
||||
Validity validity = IvfcStorage.Validate(true, logger);
|
||||
IvfcStorage.SetLevelValidities(Header.Ivfc);
|
||||
Validity journalValidity = JournalIvfcStorage.Validate(true, logger);
|
||||
JournalIvfcStorage.SetLevelValidities(Header.Ivfc);
|
||||
|
||||
return validity;
|
||||
if (FatIvfcStorage == null)return journalValidity;
|
||||
|
||||
Validity fatValidity = FatIvfcStorage.Validate(true, logger);
|
||||
FatIvfcStorage.SetLevelValidities(Header.Ivfc);
|
||||
|
||||
if (journalValidity != Validity.Valid) return journalValidity;
|
||||
if (fatValidity != Validity.Valid) return fatValidity;
|
||||
|
||||
return journalValidity;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,10 +19,9 @@ namespace LibHac.IO.Save
|
||||
AllocationTable = new AllocationTable(allocationTable, header.Slice(0x18, 0x30));
|
||||
|
||||
Header = new SaveHeader(HeaderStorage);
|
||||
|
||||
// todo: Query the FAT for the file size when none is given
|
||||
AllocationTableStorage dirTableStorage = OpenFatBlock(AllocationTable.Header.DirectoryTableBlock, 1000000);
|
||||
AllocationTableStorage fileTableStorage = OpenFatBlock(AllocationTable.Header.FileTableBlock, 1000000);
|
||||
|
||||
AllocationTableStorage dirTableStorage = OpenFatStorage(AllocationTable.Header.DirectoryTableBlock);
|
||||
AllocationTableStorage fileTableStorage = OpenFatStorage(AllocationTable.Header.FileTableBlock);
|
||||
|
||||
FileTable = new HierarchicalSaveFileTable(dirTableStorage, fileTableStorage);
|
||||
}
|
||||
@ -34,7 +33,14 @@ namespace LibHac.IO.Save
|
||||
|
||||
public void CreateFile(string path, long size, CreateFileOptions options)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
path = PathTools.Normalize(path);
|
||||
|
||||
int blockCount = (int)Util.DivideByRoundUp(size, AllocationTable.Header.BlockSize);
|
||||
int startBlock = AllocationTable.Allocate(blockCount);
|
||||
|
||||
var fileEntry = new SaveFileInfo { StartBlock = startBlock, Length = size };
|
||||
|
||||
FileTable.AddFile(path, ref fileEntry);
|
||||
}
|
||||
|
||||
public void DeleteDirectory(string path)
|
||||
@ -44,7 +50,19 @@ namespace LibHac.IO.Save
|
||||
|
||||
public void DeleteFile(string path)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
path = PathTools.Normalize(path);
|
||||
|
||||
if (!FileTable.TryOpenFile(path, out SaveFileInfo fileInfo))
|
||||
{
|
||||
throw new FileNotFoundException();
|
||||
}
|
||||
|
||||
if (fileInfo.StartBlock != int.MinValue)
|
||||
{
|
||||
AllocationTable.Free(fileInfo.StartBlock);
|
||||
}
|
||||
|
||||
FileTable.DeleteFile(path);
|
||||
}
|
||||
|
||||
public IDirectory OpenDirectory(string path, OpenDirectoryMode mode)
|
||||
@ -68,14 +86,9 @@ namespace LibHac.IO.Save
|
||||
throw new FileNotFoundException();
|
||||
}
|
||||
|
||||
if (file.StartBlock < 0)
|
||||
{
|
||||
return new NullFile();
|
||||
}
|
||||
AllocationTableStorage storage = OpenFatStorage(file.StartBlock);
|
||||
|
||||
AllocationTableStorage storage = OpenFatBlock(file.StartBlock, file.Length);
|
||||
|
||||
return new SaveDataFile(storage, 0, file.Length, mode);
|
||||
return new SaveDataFile(storage, path, FileTable, file.Length, mode);
|
||||
}
|
||||
|
||||
public void RenameDirectory(string srcPath, string dstPath)
|
||||
@ -114,15 +127,37 @@ namespace LibHac.IO.Save
|
||||
|
||||
public void Commit()
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
|
||||
}
|
||||
|
||||
public IStorage GetBaseStorage() => BaseStorage.AsReadOnly();
|
||||
public IStorage GetHeaderStorage() => HeaderStorage.AsReadOnly();
|
||||
|
||||
private AllocationTableStorage OpenFatBlock(int blockIndex, long size)
|
||||
public void FsTrim()
|
||||
{
|
||||
return new AllocationTableStorage(BaseStorage, AllocationTable, (int)Header.BlockSize, blockIndex, size);
|
||||
AllocationTable.FsTrim();
|
||||
|
||||
foreach (DirectoryEntry file in this.EnumerateEntries("*", SearchOptions.RecurseSubdirectories))
|
||||
{
|
||||
if (FileTable.TryOpenFile(file.FullPath, out SaveFileInfo fileInfo) && fileInfo.StartBlock >= 0)
|
||||
{
|
||||
AllocationTable.FsTrimList(fileInfo.StartBlock);
|
||||
|
||||
OpenFatStorage(fileInfo.StartBlock).Slice(fileInfo.Length).Fill(0);
|
||||
}
|
||||
}
|
||||
|
||||
int freeIndex = AllocationTable.GetFreeListBlockIndex();
|
||||
if (freeIndex == 0) return;
|
||||
|
||||
AllocationTable.FsTrimList(freeIndex);
|
||||
|
||||
OpenFatStorage(freeIndex).Fill(0);
|
||||
}
|
||||
|
||||
private AllocationTableStorage OpenFatStorage(int blockIndex)
|
||||
{
|
||||
return new AllocationTableStorage(BaseStorage, AllocationTable, (int)Header.BlockSize, blockIndex);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -15,22 +15,23 @@ namespace LibHac.IO.Save
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0x14)]
|
||||
public struct SaveFileInfo
|
||||
{
|
||||
public int StartBlock;
|
||||
public long Length;
|
||||
public long Reserved;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the current position when enumerating a directory's contents.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0x14)]
|
||||
public struct SaveFindPosition
|
||||
{
|
||||
/// <summary>The ID of the next directory to be enumerated.</summary>
|
||||
public int NextDirectory;
|
||||
/// <summary>The ID of the next file to be enumerated.</summary>
|
||||
public long NextFile;
|
||||
public int NextFile;
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ using System.Runtime.InteropServices;
|
||||
|
||||
namespace LibHac.IO.Save
|
||||
{
|
||||
internal class SaveFsList<T> where T : unmanaged
|
||||
internal class SaveFsList<T> where T : struct
|
||||
{
|
||||
private const int FreeListHeadIndex = 0;
|
||||
private const int UsedListHeadIndex = 1;
|
||||
@ -20,7 +20,7 @@ namespace LibHac.IO.Save
|
||||
Storage = tableStorage;
|
||||
}
|
||||
|
||||
public int GetOffsetFromKey(ref SaveEntryKey key)
|
||||
public (int Index, int PreviousIndex) GetIndexFromKey(ref SaveEntryKey key)
|
||||
{
|
||||
Span<byte> entryBytes = stackalloc byte[_sizeOfEntry];
|
||||
Span<byte> name = entryBytes.Slice(4, MaxNameLength);
|
||||
@ -29,26 +29,104 @@ namespace LibHac.IO.Save
|
||||
int capacity = GetListCapacity();
|
||||
|
||||
ReadEntry(UsedListHeadIndex, entryBytes);
|
||||
int prevIndex = UsedListHeadIndex;
|
||||
int index = entry.Next;
|
||||
|
||||
while (entry.Next > 0)
|
||||
while (index > 0)
|
||||
{
|
||||
if (entry.Next > capacity) throw new IndexOutOfRangeException("Save entry index out of range");
|
||||
if (index > capacity) throw new IndexOutOfRangeException("Save entry index out of range");
|
||||
|
||||
int entryId = entry.Next;
|
||||
ReadEntry(entry.Next, out entry);
|
||||
ReadEntry(index, out entry);
|
||||
|
||||
if (entry.Parent == key.Parent && Util.StringSpansEqual(name, key.Name))
|
||||
{
|
||||
return entryId;
|
||||
return (index, prevIndex);
|
||||
}
|
||||
|
||||
prevIndex = index;
|
||||
index = entry.Next;
|
||||
}
|
||||
|
||||
return -1;
|
||||
return (-1, -1);
|
||||
}
|
||||
|
||||
public int Add(ref SaveEntryKey key, ref T value)
|
||||
{
|
||||
int index = GetIndexFromKey(ref key).Index;
|
||||
|
||||
if (index != -1)
|
||||
{
|
||||
SetValue(index, ref value);
|
||||
return index;
|
||||
}
|
||||
|
||||
index = AllocateEntry();
|
||||
|
||||
ReadEntry(index, out SaveFsEntry entry);
|
||||
entry.Value = value;
|
||||
WriteEntry(index, ref entry, ref key);
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
private int AllocateEntry()
|
||||
{
|
||||
ReadEntry(FreeListHeadIndex, out SaveFsEntry freeListHead);
|
||||
ReadEntry(UsedListHeadIndex, out SaveFsEntry usedListHead);
|
||||
|
||||
if (freeListHead.Next != 0)
|
||||
{
|
||||
ReadEntry(freeListHead.Next, out SaveFsEntry firstFreeEntry);
|
||||
|
||||
int allocatedIndex = freeListHead.Next;
|
||||
|
||||
freeListHead.Next = firstFreeEntry.Next;
|
||||
firstFreeEntry.Next = usedListHead.Next;
|
||||
usedListHead.Next = allocatedIndex;
|
||||
|
||||
WriteEntry(FreeListHeadIndex, ref freeListHead);
|
||||
WriteEntry(UsedListHeadIndex, ref usedListHead);
|
||||
WriteEntry(allocatedIndex, ref firstFreeEntry);
|
||||
|
||||
return allocatedIndex;
|
||||
}
|
||||
|
||||
int length = GetListLength();
|
||||
int capacity = GetListCapacity();
|
||||
|
||||
if (capacity == 0 || length >= capacity)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
SetListLength(length + 1);
|
||||
|
||||
ReadEntry(length, out SaveFsEntry newEntry);
|
||||
|
||||
newEntry.Next = usedListHead.Next;
|
||||
usedListHead.Next = length;
|
||||
|
||||
WriteEntry(UsedListHeadIndex, ref usedListHead);
|
||||
WriteEntry(length, ref newEntry);
|
||||
|
||||
return length;
|
||||
}
|
||||
|
||||
private void Free(int entryIndex)
|
||||
{
|
||||
ReadEntry(FreeListHeadIndex, out SaveFsEntry freeEntry);
|
||||
ReadEntry(entryIndex, out SaveFsEntry entry);
|
||||
|
||||
entry.Next = freeEntry.Next;
|
||||
freeEntry.Next = entryIndex;
|
||||
|
||||
WriteEntry(FreeListHeadIndex, ref freeEntry);
|
||||
WriteEntry(entryIndex, ref entry);
|
||||
}
|
||||
|
||||
public bool TryGetValue(ref SaveEntryKey key, out T value)
|
||||
{
|
||||
int index = GetOffsetFromKey(ref key);
|
||||
int index = GetIndexFromKey(ref key).Index;
|
||||
|
||||
if (index < 0)
|
||||
{
|
||||
@ -123,6 +201,31 @@ namespace LibHac.IO.Save
|
||||
value = entry.Value;
|
||||
}
|
||||
|
||||
public void SetValue(int index, ref T value)
|
||||
{
|
||||
Span<byte> entryBytes = stackalloc byte[_sizeOfEntry];
|
||||
ref SaveFsEntry entry = ref GetEntryFromBytes(entryBytes);
|
||||
|
||||
ReadEntry(index, out entry);
|
||||
|
||||
entry.Value = value;
|
||||
|
||||
WriteEntry(index, ref entry);
|
||||
}
|
||||
|
||||
public void Remove(ref SaveEntryKey key)
|
||||
{
|
||||
(int index, int previousIndex) = GetIndexFromKey(ref key);
|
||||
|
||||
ReadEntry(previousIndex, out SaveFsEntry prevEntry);
|
||||
ReadEntry(index, out SaveFsEntry entryToDel);
|
||||
|
||||
prevEntry.Next = entryToDel.Next;
|
||||
WriteEntry(previousIndex, ref prevEntry);
|
||||
|
||||
Free(index);
|
||||
}
|
||||
|
||||
private int GetListCapacity()
|
||||
{
|
||||
Span<byte> buf = stackalloc byte[sizeof(int)];
|
||||
@ -139,6 +242,23 @@ namespace LibHac.IO.Save
|
||||
return MemoryMarshal.Read<int>(buf);
|
||||
}
|
||||
|
||||
// ReSharper disable once UnusedMember.Local
|
||||
private void SetListCapacity(int capacity)
|
||||
{
|
||||
Span<byte> buf = stackalloc byte[sizeof(int)];
|
||||
MemoryMarshal.Write(buf, ref capacity);
|
||||
|
||||
Storage.Write(buf, 4);
|
||||
}
|
||||
|
||||
private void SetListLength(int length)
|
||||
{
|
||||
Span<byte> buf = stackalloc byte[sizeof(int)];
|
||||
MemoryMarshal.Write(buf, ref length);
|
||||
|
||||
Storage.Write(buf, 0);
|
||||
}
|
||||
|
||||
private void ReadEntry(int index, out SaveFsEntry entry)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[_sizeOfEntry];
|
||||
@ -147,6 +267,34 @@ namespace LibHac.IO.Save
|
||||
entry = GetEntryFromBytes(bytes);
|
||||
}
|
||||
|
||||
private void WriteEntry(int index, ref SaveFsEntry entry, ref SaveEntryKey key)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[_sizeOfEntry];
|
||||
Span<byte> nameSpan = bytes.Slice(4, MaxNameLength);
|
||||
|
||||
// Copy needed for .NET Framework compat
|
||||
ref SaveFsEntry newEntry = ref GetEntryFromBytes(bytes);
|
||||
newEntry = entry;
|
||||
|
||||
newEntry.Parent = key.Parent;
|
||||
key.Name.CopyTo(nameSpan);
|
||||
|
||||
nameSpan.Slice(key.Name.Length).Fill(0);
|
||||
|
||||
WriteEntry(index, bytes);
|
||||
}
|
||||
|
||||
private void WriteEntry(int index, ref SaveFsEntry entry)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[_sizeOfEntry];
|
||||
|
||||
// Copy needed for .NET Framework compat
|
||||
ref SaveFsEntry newEntry = ref GetEntryFromBytes(bytes);
|
||||
newEntry = entry;
|
||||
|
||||
WriteEntry(index, bytes);
|
||||
}
|
||||
|
||||
private void ReadEntry(int index, Span<byte> entry)
|
||||
{
|
||||
Debug.Assert(entry.Length == _sizeOfEntry);
|
||||
@ -155,6 +303,14 @@ namespace LibHac.IO.Save
|
||||
Storage.Read(entry, offset);
|
||||
}
|
||||
|
||||
private void WriteEntry(int index, Span<byte> entry)
|
||||
{
|
||||
Debug.Assert(entry.Length == _sizeOfEntry);
|
||||
|
||||
int offset = index * _sizeOfEntry;
|
||||
Storage.Write(entry, offset);
|
||||
}
|
||||
|
||||
private ref SaveFsEntry GetEntryFromBytes(Span<byte> entry)
|
||||
{
|
||||
return ref MemoryMarshal.Cast<byte, SaveFsEntry>(entry)[0];
|
||||
|
@ -97,6 +97,59 @@ namespace LibHac.IO
|
||||
progress?.SetTotal(0);
|
||||
}
|
||||
|
||||
public static void Fill(this IStorage input, byte value, IProgressReport progress = null)
|
||||
{
|
||||
const int threshold = 0x400;
|
||||
|
||||
long length = input.GetSize();
|
||||
if (length > threshold)
|
||||
{
|
||||
input.FillLarge(value, progress);
|
||||
return;
|
||||
}
|
||||
|
||||
Span<byte> buf = stackalloc byte[(int)length];
|
||||
buf.Fill(value);
|
||||
|
||||
input.Write(buf, 0);
|
||||
}
|
||||
|
||||
private static void FillLarge(this IStorage input, byte value, IProgressReport progress = null)
|
||||
{
|
||||
const int bufferSize = 0x4000;
|
||||
|
||||
long remaining = input.GetSize();
|
||||
if (remaining < 0) throw new ArgumentException("Storage must have an explicit length");
|
||||
progress?.SetTotal(remaining);
|
||||
|
||||
long pos = 0;
|
||||
|
||||
byte[] buffer = ArrayPool<byte>.Shared.Rent(bufferSize);
|
||||
try
|
||||
{
|
||||
buffer.AsSpan(0, (int)Math.Min(remaining, bufferSize)).Fill(value);
|
||||
|
||||
while (remaining > 0)
|
||||
{
|
||||
int toFill = (int)Math.Min(bufferSize, remaining);
|
||||
Span<byte> buf = buffer.AsSpan(0, toFill);
|
||||
|
||||
input.Write(buf, pos);
|
||||
|
||||
remaining -= toFill;
|
||||
pos += toFill;
|
||||
|
||||
progress?.ReportAdd(toFill);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
|
||||
progress?.SetTotal(0);
|
||||
}
|
||||
|
||||
public static void WriteAllBytes(this IStorage input, string filename, IProgressReport progress = null)
|
||||
{
|
||||
using (var outFile = new FileStream(filename, FileMode.Create, FileAccess.Write))
|
||||
|
@ -52,6 +52,7 @@ namespace hactoolnet
|
||||
new CliOption("listromfs", 0, (o, a) => o.ListRomFs = true),
|
||||
new CliOption("listfiles", 0, (o, a) => o.ListFiles = true),
|
||||
new CliOption("sign", 0, (o, a) => o.SignSave = true),
|
||||
new CliOption("trim", 0, (o, a) => o.TrimSave = true),
|
||||
new CliOption("readbench", 0, (o, a) => o.ReadBench = true),
|
||||
new CliOption("hashedfs", 0, (o, a) => o.BuildHfs = true),
|
||||
new CliOption("title", 1, (o, a) => o.TitleId = ParseTitleId(a[0])),
|
||||
@ -232,6 +233,7 @@ namespace hactoolnet
|
||||
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.");
|
||||
sb.AppendLine(" --sign Sign the save file. (Requires device_key in key file)");
|
||||
sb.AppendLine(" --trim Trim garbage data in the save file. (Requires device_key in key file)");
|
||||
sb.AppendLine(" --listfiles List files in save file.");
|
||||
sb.AppendLine(" --replacefile <filename in save> <file> Replaces a file in the save data");
|
||||
sb.AppendLine("NDV0 (Delta) options:");
|
||||
|
@ -45,6 +45,7 @@ namespace hactoolnet
|
||||
public bool ListRomFs;
|
||||
public bool ListFiles;
|
||||
public bool SignSave;
|
||||
public bool TrimSave;
|
||||
public bool ReadBench;
|
||||
public bool BuildHfs;
|
||||
public ulong TitleId;
|
||||
|
@ -32,68 +32,8 @@ namespace hactoolnet
|
||||
{
|
||||
// todo
|
||||
string dir = ctx.Options.DebugOutDir;
|
||||
Directory.CreateDirectory(dir);
|
||||
|
||||
FsLayout layout = save.Header.Layout;
|
||||
|
||||
string mainRemapDir = Path.Combine(dir, "main_remap");
|
||||
Directory.CreateDirectory(mainRemapDir);
|
||||
|
||||
save.DataRemapStorage.GetBaseStorage().WriteAllBytes(Path.Combine(mainRemapDir, "Data"));
|
||||
save.DataRemapStorage.GetHeaderStorage().WriteAllBytes(Path.Combine(mainRemapDir, "Header"));
|
||||
save.DataRemapStorage.GetMapEntryStorage().WriteAllBytes(Path.Combine(mainRemapDir, "Map entries"));
|
||||
|
||||
string metadataRemapDir = Path.Combine(dir, "metadata_remap");
|
||||
Directory.CreateDirectory(metadataRemapDir);
|
||||
|
||||
save.MetaRemapStorage.GetBaseStorage().WriteAllBytes(Path.Combine(metadataRemapDir, "Data"));
|
||||
save.MetaRemapStorage.GetHeaderStorage().WriteAllBytes(Path.Combine(metadataRemapDir, "Header"));
|
||||
save.MetaRemapStorage.GetMapEntryStorage().WriteAllBytes(Path.Combine(metadataRemapDir, "Map entries"));
|
||||
|
||||
string journalDir = Path.Combine(dir, "journal");
|
||||
Directory.CreateDirectory(journalDir);
|
||||
|
||||
save.JournalStorage.GetBaseStorage().WriteAllBytes(Path.Combine(journalDir, "Data"));
|
||||
save.JournalStorage.GetHeaderStorage().WriteAllBytes(Path.Combine(journalDir, "Header"));
|
||||
save.JournalStorage.Map.GetHeaderStorage().WriteAllBytes(Path.Combine(journalDir, "Map_header"));
|
||||
save.JournalStorage.Map.GetMapStorage().WriteAllBytes(Path.Combine(journalDir, "Map"));
|
||||
save.JournalStorage.Map.GetModifiedPhysicalBlocksStorage().WriteAllBytes(Path.Combine(journalDir, "ModifiedPhysicalBlocks"));
|
||||
save.JournalStorage.Map.GetModifiedVirtualBlocksStorage().WriteAllBytes(Path.Combine(journalDir, "ModifiedVirtualBlocks"));
|
||||
save.JournalStorage.Map.GetFreeBlocksStorage().WriteAllBytes(Path.Combine(journalDir, "FreeBlocks"));
|
||||
|
||||
string saveDir = Path.Combine(dir, "save");
|
||||
Directory.CreateDirectory(saveDir);
|
||||
|
||||
save.SaveDataFileSystemCore.GetHeaderStorage().WriteAllBytes(Path.Combine(saveDir, "Save_Header"));
|
||||
save.SaveDataFileSystemCore.GetBaseStorage().WriteAllBytes(Path.Combine(saveDir, "Save_Data"));
|
||||
save.SaveDataFileSystemCore.AllocationTable.GetHeaderStorage().WriteAllBytes(Path.Combine(saveDir, "FAT_header"));
|
||||
save.SaveDataFileSystemCore.AllocationTable.GetBaseStorage().WriteAllBytes(Path.Combine(saveDir, "FAT_Data"));
|
||||
|
||||
save.Header.DataIvfcMaster.WriteAllBytes(Path.Combine(saveDir, "Save_MasterHash"));
|
||||
|
||||
IStorage saveLayer1Hash = save.MetaRemapStorage.Slice(layout.IvfcL1Offset, layout.IvfcL1Size);
|
||||
IStorage saveLayer2Hash = save.MetaRemapStorage.Slice(layout.IvfcL2Offset, layout.IvfcL2Size);
|
||||
IStorage saveLayer3Hash = save.MetaRemapStorage.Slice(layout.IvfcL3Offset, layout.IvfcL3Size);
|
||||
|
||||
saveLayer1Hash.WriteAllBytes(Path.Combine(saveDir, "Save_Layer1Hash"), ctx.Logger);
|
||||
saveLayer2Hash.WriteAllBytes(Path.Combine(saveDir, "Save_Layer2Hash"), ctx.Logger);
|
||||
saveLayer3Hash.WriteAllBytes(Path.Combine(saveDir, "Save_Layer3Hash"), ctx.Logger);
|
||||
|
||||
string duplexDir = Path.Combine(dir, "duplex");
|
||||
Directory.CreateDirectory(duplexDir);
|
||||
|
||||
save.Header.DuplexMasterBitmapA.WriteAllBytes(Path.Combine(duplexDir, "MasterBitmapA"));
|
||||
save.Header.DuplexMasterBitmapB.WriteAllBytes(Path.Combine(duplexDir, "MasterBitmapB"));
|
||||
|
||||
IStorage duplexL1A = save.DataRemapStorage.Slice(layout.DuplexL1OffsetA, layout.DuplexL1Size);
|
||||
IStorage duplexL1B = save.DataRemapStorage.Slice(layout.DuplexL1OffsetB, layout.DuplexL1Size);
|
||||
IStorage duplexDataA = save.DataRemapStorage.Slice(layout.DuplexDataOffsetA, layout.DuplexDataSize);
|
||||
IStorage duplexDataB = save.DataRemapStorage.Slice(layout.DuplexDataOffsetB, layout.DuplexDataSize);
|
||||
|
||||
duplexL1A.WriteAllBytes(Path.Combine(duplexDir, "L1BitmapA"), ctx.Logger);
|
||||
duplexL1B.WriteAllBytes(Path.Combine(duplexDir, "L1BitmapB"), ctx.Logger);
|
||||
duplexDataA.WriteAllBytes(Path.Combine(duplexDir, "DataA"), ctx.Logger);
|
||||
duplexDataB.WriteAllBytes(Path.Combine(duplexDir, "DataB"), ctx.Logger);
|
||||
ExportSaveDebug(ctx, dir, save);
|
||||
}
|
||||
|
||||
if (ctx.Options.ReplaceFileDest != null && ctx.Options.ReplaceFileSource != null)
|
||||
@ -107,8 +47,7 @@ namespace hactoolnet
|
||||
{
|
||||
if (inFile.GetSize() != outFile.GetSize())
|
||||
{
|
||||
ctx.Logger.LogMessage($"Replacement file must be the same size as the original file. ({outFile.GetSize()} bytes)");
|
||||
return;
|
||||
outFile.SetSize(inFile.GetSize());
|
||||
}
|
||||
|
||||
inFile.CopyTo(outFile, ctx.Logger);
|
||||
@ -129,8 +68,14 @@ namespace hactoolnet
|
||||
return;
|
||||
}
|
||||
|
||||
if (ctx.Options.SignSave)
|
||||
if (ctx.Options.SignSave || ctx.Options.TrimSave)
|
||||
{
|
||||
if (ctx.Options.TrimSave)
|
||||
{
|
||||
save.FsTrim();
|
||||
ctx.Logger.LogMessage("Trimmed save file");
|
||||
}
|
||||
|
||||
if (save.Commit(ctx.Keyset))
|
||||
{
|
||||
ctx.Logger.LogMessage("Successfully signed save file");
|
||||
@ -156,30 +101,113 @@ namespace hactoolnet
|
||||
}
|
||||
}
|
||||
|
||||
internal static void ExportSaveDebug(Context ctx, string dir, SaveDataFileSystem save)
|
||||
{
|
||||
Directory.CreateDirectory(dir);
|
||||
|
||||
FsLayout layout = save.Header.Layout;
|
||||
|
||||
string mainRemapDir = Path.Combine(dir, "main_remap");
|
||||
Directory.CreateDirectory(mainRemapDir);
|
||||
|
||||
save.DataRemapStorage.GetBaseStorage().WriteAllBytes(Path.Combine(mainRemapDir, "Data"));
|
||||
save.DataRemapStorage.GetHeaderStorage().WriteAllBytes(Path.Combine(mainRemapDir, "Header"));
|
||||
save.DataRemapStorage.GetMapEntryStorage().WriteAllBytes(Path.Combine(mainRemapDir, "Map entries"));
|
||||
|
||||
string metadataRemapDir = Path.Combine(dir, "metadata_remap");
|
||||
Directory.CreateDirectory(metadataRemapDir);
|
||||
|
||||
save.MetaRemapStorage.GetBaseStorage().WriteAllBytes(Path.Combine(metadataRemapDir, "Data"));
|
||||
save.MetaRemapStorage.GetHeaderStorage().WriteAllBytes(Path.Combine(metadataRemapDir, "Header"));
|
||||
save.MetaRemapStorage.GetMapEntryStorage().WriteAllBytes(Path.Combine(metadataRemapDir, "Map entries"));
|
||||
|
||||
string journalDir = Path.Combine(dir, "journal");
|
||||
Directory.CreateDirectory(journalDir);
|
||||
|
||||
save.JournalStorage.GetBaseStorage().WriteAllBytes(Path.Combine(journalDir, "Data"));
|
||||
save.JournalStorage.GetHeaderStorage().WriteAllBytes(Path.Combine(journalDir, "Header"));
|
||||
save.JournalStorage.Map.GetHeaderStorage().WriteAllBytes(Path.Combine(journalDir, "Map_header"));
|
||||
save.JournalStorage.Map.GetMapStorage().WriteAllBytes(Path.Combine(journalDir, "Map"));
|
||||
save.JournalStorage.Map.GetModifiedPhysicalBlocksStorage()
|
||||
.WriteAllBytes(Path.Combine(journalDir, "ModifiedPhysicalBlocks"));
|
||||
save.JournalStorage.Map.GetModifiedVirtualBlocksStorage()
|
||||
.WriteAllBytes(Path.Combine(journalDir, "ModifiedVirtualBlocks"));
|
||||
save.JournalStorage.Map.GetFreeBlocksStorage().WriteAllBytes(Path.Combine(journalDir, "FreeBlocks"));
|
||||
|
||||
string saveDir = Path.Combine(dir, "save");
|
||||
Directory.CreateDirectory(saveDir);
|
||||
|
||||
save.SaveDataFileSystemCore.GetHeaderStorage().WriteAllBytes(Path.Combine(saveDir, "Save_Header"));
|
||||
save.SaveDataFileSystemCore.GetBaseStorage().WriteAllBytes(Path.Combine(saveDir, "Save_Data"));
|
||||
save.SaveDataFileSystemCore.AllocationTable.GetHeaderStorage().WriteAllBytes(Path.Combine(saveDir, "FAT_header"));
|
||||
save.SaveDataFileSystemCore.AllocationTable.GetBaseStorage().WriteAllBytes(Path.Combine(saveDir, "FAT_Data"));
|
||||
|
||||
save.Header.DataIvfcMaster.WriteAllBytes(Path.Combine(saveDir, "Save_MasterHash"));
|
||||
|
||||
IStorage saveLayer1Hash = save.MetaRemapStorage.Slice(layout.IvfcL1Offset, layout.IvfcL1Size);
|
||||
IStorage saveLayer2Hash = save.MetaRemapStorage.Slice(layout.IvfcL2Offset, layout.IvfcL2Size);
|
||||
IStorage saveLayer3Hash = save.MetaRemapStorage.Slice(layout.IvfcL3Offset, layout.IvfcL3Size);
|
||||
|
||||
saveLayer1Hash.WriteAllBytes(Path.Combine(saveDir, "Save_Layer1Hash"), ctx.Logger);
|
||||
saveLayer2Hash.WriteAllBytes(Path.Combine(saveDir, "Save_Layer2Hash"), ctx.Logger);
|
||||
saveLayer3Hash.WriteAllBytes(Path.Combine(saveDir, "Save_Layer3Hash"), ctx.Logger);
|
||||
|
||||
if (layout.Version >= 0x50000)
|
||||
{
|
||||
save.Header.FatIvfcMaster.WriteAllBytes(Path.Combine(saveDir, "Fat_MasterHash"));
|
||||
|
||||
IStorage fatLayer1Hash = save.MetaRemapStorage.Slice(layout.FatIvfcL1Offset, layout.FatIvfcL1Size);
|
||||
IStorage fatLayer2Hash = save.MetaRemapStorage.Slice(layout.FatIvfcL2Offset, layout.FatIvfcL1Size);
|
||||
|
||||
fatLayer1Hash.WriteAllBytes(Path.Combine(saveDir, "Fat_Layer1Hash"), ctx.Logger);
|
||||
fatLayer2Hash.WriteAllBytes(Path.Combine(saveDir, "Fat_Layer2Hash"), ctx.Logger);
|
||||
}
|
||||
|
||||
string duplexDir = Path.Combine(dir, "duplex");
|
||||
Directory.CreateDirectory(duplexDir);
|
||||
|
||||
save.Header.DuplexMasterBitmapA.WriteAllBytes(Path.Combine(duplexDir, "MasterBitmapA"));
|
||||
save.Header.DuplexMasterBitmapB.WriteAllBytes(Path.Combine(duplexDir, "MasterBitmapB"));
|
||||
|
||||
IStorage duplexL1A = save.DataRemapStorage.Slice(layout.DuplexL1OffsetA, layout.DuplexL1Size);
|
||||
IStorage duplexL1B = save.DataRemapStorage.Slice(layout.DuplexL1OffsetB, layout.DuplexL1Size);
|
||||
IStorage duplexDataA = save.DataRemapStorage.Slice(layout.DuplexDataOffsetA, layout.DuplexDataSize);
|
||||
IStorage duplexDataB = save.DataRemapStorage.Slice(layout.DuplexDataOffsetB, layout.DuplexDataSize);
|
||||
|
||||
duplexL1A.WriteAllBytes(Path.Combine(duplexDir, "L1BitmapA"), ctx.Logger);
|
||||
duplexL1B.WriteAllBytes(Path.Combine(duplexDir, "L1BitmapB"), ctx.Logger);
|
||||
duplexDataA.WriteAllBytes(Path.Combine(duplexDir, "DataA"), ctx.Logger);
|
||||
duplexDataB.WriteAllBytes(Path.Combine(duplexDir, "DataB"), ctx.Logger);
|
||||
}
|
||||
|
||||
// ReSharper disable once UnusedMember.Local
|
||||
private static string PrintFatLayout(this SaveDataFileSystem save)
|
||||
public static string PrintFatLayout(this SaveDataFileSystemCore save)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
foreach (DirectoryEntry entry in save.EnumerateEntries().Where(x => x.Type == DirectoryEntryType.File))
|
||||
{
|
||||
save.SaveDataFileSystemCore.FileTable.TryOpenFile(entry.FullPath, out SaveFileInfo fileInfo);
|
||||
save.FileTable.TryOpenFile(entry.FullPath, out SaveFileInfo fileInfo);
|
||||
if (fileInfo.StartBlock < 0) continue;
|
||||
|
||||
IEnumerable<(int block, int length)> chain = save.SaveDataFileSystemCore.AllocationTable.DumpChain(fileInfo.StartBlock);
|
||||
IEnumerable<(int block, int length)> chain = save.AllocationTable.DumpChain(fileInfo.StartBlock);
|
||||
|
||||
sb.AppendLine(entry.FullPath);
|
||||
sb.AppendLine(PrintBlockChain(chain));
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine("Directory Table");
|
||||
sb.AppendLine(PrintBlockChain(save.SaveDataFileSystemCore.AllocationTable.DumpChain(0)));
|
||||
sb.AppendLine(PrintBlockChain(save.AllocationTable.DumpChain(0)));
|
||||
sb.AppendLine();
|
||||
|
||||
sb.AppendLine("File Table");
|
||||
sb.AppendLine(PrintBlockChain(save.SaveDataFileSystemCore.AllocationTable.DumpChain(1)));
|
||||
sb.AppendLine(PrintBlockChain(save.AllocationTable.DumpChain(1)));
|
||||
sb.AppendLine();
|
||||
|
||||
sb.AppendLine("Free blocks");
|
||||
sb.AppendLine(PrintBlockChain(save.SaveDataFileSystemCore.AllocationTable.DumpChain(-1)));
|
||||
sb.AppendLine(PrintBlockChain(save.AllocationTable.DumpChain(-1)));
|
||||
sb.AppendLine();
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user