diff --git a/src/LibHac/IO/Save/AllocationTable.cs b/src/LibHac/IO/Save/AllocationTable.cs
index c92825ee..7b3d3b4d 100644
--- a/src/LibHac/IO/Save/AllocationTable.cs
+++ b/src/LibHac/IO/Save/AllocationTable.cs
@@ -7,6 +7,7 @@ namespace LibHac.IO.Save
{
public class AllocationTable
{
+ private const int FreeListEntryIndex = 0;
private const int EntrySize = 8;
private IStorage BaseStorage { get; }
@@ -37,7 +38,7 @@ namespace LibHac.IO.Save
}
else
{
- length = entries[1].Next - entryIndex;
+ length = entries[1].Next - entryIndex + 1;
}
if (entries[0].IsListEnd())
@@ -46,7 +47,7 @@ namespace LibHac.IO.Save
}
else
{
- next = EntryIndexToBlock(entries[0].Next & 0x7FFFFFFF);
+ next = EntryIndexToBlock(entries[0].GetNext());
}
if (entries[0].IsListStart())
@@ -55,10 +56,44 @@ namespace LibHac.IO.Save
}
else
{
- previous = EntryIndexToBlock(entries[0].Prev & 0x7FFFFFFF);
+ previous = EntryIndexToBlock(entries[0].GetPrev());
}
}
+ public int GetFreeListBlockIndex()
+ {
+ AllocationTableEntry freeList = ReadEntry(FreeListEntryIndex);
+ return EntryIndexToBlock(freeList.GetNext());
+ }
+
+ public void SetFreeListBlockIndex(int headBlockIndex)
+ {
+ var freeList = new AllocationTableEntry() { Next = BlockToEntryIndex(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;
+ }
+
+ ///
+ /// Combines 2 lists into one list. The second list will be attached to the end of the first list.
+ ///
+ /// The index of the first block.
+ /// The index of the second block.
public void Join(int frontListBlockIndex, int backListBlockIndex)
{
int frontEntryIndex = BlockToEntryIndex(frontListBlockIndex);
@@ -76,6 +111,136 @@ namespace LibHac.IO.Save
WriteEntry(backEntryIndex, backHead);
}
+ ///
+ /// Trims an existing list to the specified length and returns the excess blocks as a new list.
+ ///
+ /// The starting block of the list to trim.
+ /// The length in blocks that the list will be shortened to.
+ /// The index of the head node of the removed blocks.
+ 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);
+
+ int start = EntryIndexToBlock(currentEntryIndex);
+ int end = start + segmentLength - 1;
+
+ 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);
+ }
+
+ ///
+ /// Splits a single list segment into 2 segments. The sequence of blocks in the full list will remain the same.
+ ///
+ /// The block index of the segment to split.
+ /// The length of the first subsegment.
+ 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;
@@ -195,6 +360,16 @@ namespace LibHac.IO.Save
public int Prev;
public int Next;
+ public int GetPrev()
+ {
+ return Prev & 0x7FFFFFFF;
+ }
+
+ public int GetNext()
+ {
+ return Next & 0x7FFFFFFF;
+ }
+
public bool IsListStart()
{
return Prev == int.MinValue;
@@ -248,6 +423,16 @@ namespace LibHac.IO.Save
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
diff --git a/src/LibHac/IO/Save/SaveDataFileSystemCore.cs b/src/LibHac/IO/Save/SaveDataFileSystemCore.cs
index 4a714b8f..be2e857b 100644
--- a/src/LibHac/IO/Save/SaveDataFileSystemCore.cs
+++ b/src/LibHac/IO/Save/SaveDataFileSystemCore.cs
@@ -19,7 +19,7 @@ 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);
@@ -34,7 +34,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)