Merge pull request #52 from Thealexbarney/save-edit

Add more support for savedata FS editing
This commit is contained in:
Alex Barney 2019-04-23 15:14:10 -05:00 committed by GitHub
commit 6764dc7800
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 1094 additions and 219 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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];

View File

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

View File

@ -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:");

View File

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

View File

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