Process the file allocation table in savefiles

This commit is contained in:
Alex Barney 2018-09-02 21:37:23 -05:00
parent 1a7e452a89
commit 7bc718c975
4 changed files with 262 additions and 13 deletions

View File

@ -0,0 +1,52 @@
using System.IO;
namespace LibHac.Savefile
{
public class AllocationTable
{
public AllocationTableEntry[] Entries { get; }
public AllocationTable(Stream tableStream)
{
int blockCount = (int)(tableStream.Length / 8);
Entries = new AllocationTableEntry[blockCount];
tableStream.Position = 0;
var reader = new BinaryReader(tableStream);
for (int i = 0; i < blockCount; i++)
{
int parent = reader.ReadInt32();
int child = reader.ReadInt32();
Entries[i] = new AllocationTableEntry { Next = child, Prev = parent };
}
}
}
public class AllocationTableEntry
{
public int Prev { get; set; }
public int Next { get; set; }
public bool IsListStart()
{
return Prev == int.MinValue;
}
public bool IsListEnd()
{
return (Next & 0x7FFFFFFF) == 0;
}
public bool IsMultiBlockSegment()
{
return Next < 0;
}
public bool IsSingleBlockSegment()
{
return Next >= 0;
}
}
}

View File

@ -0,0 +1,88 @@
namespace LibHac.Savefile
{
public class AllocationTableIterator
{
private AllocationTable Fat { get; }
public bool IsValid { get; }
public int VirtualBlock { get; private set; }
public int PhysicalBlock { get; private set; }
public int CurrentSegmentSize { get; private set; }
public AllocationTableIterator(AllocationTable table, int initialBlock)
{
Fat = table;
IsValid = BeginIteration(initialBlock);
}
public bool BeginIteration(int initialBlock)
{
var tableEntry = Fat.Entries[initialBlock + 1];
if (!tableEntry.IsListStart())
{
return false;
}
if (tableEntry.IsSingleBlockSegment())
{
CurrentSegmentSize = 1;
}
else
{
var lengthEntry = Fat.Entries[initialBlock + 2];
CurrentSegmentSize = lengthEntry.Next - initialBlock;
}
PhysicalBlock = initialBlock;
return true;
}
public bool MoveNext()
{
var currentEntry = Fat.Entries[PhysicalBlock + 1];
if (currentEntry.IsListEnd()) return false;
int newBlock = currentEntry.Next & 0x7FFFFFFF;
var newEntry = Fat.Entries[newBlock];
VirtualBlock += CurrentSegmentSize;
if (newEntry.IsSingleBlockSegment())
{
CurrentSegmentSize = 1;
}
else
{
var lengthEntry = Fat.Entries[newBlock + 1];
CurrentSegmentSize = lengthEntry.Next - (newBlock - 1);
}
PhysicalBlock = newBlock - 1;
return true;
}
public bool MovePrevious()
{
var currentEntry = Fat.Entries[PhysicalBlock + 1];
if (currentEntry.IsListStart()) return false;
int newBlock = currentEntry.Prev & 0x7FFFFFFF;
var newEntry = Fat.Entries[newBlock];
if (newEntry.IsSingleBlockSegment())
{
CurrentSegmentSize = 1;
}
else
{
var lengthEntry = Fat.Entries[newBlock + 1];
CurrentSegmentSize = lengthEntry.Next - (newBlock - 1);
}
VirtualBlock -= CurrentSegmentSize;
PhysicalBlock = newBlock - 1;
return true;
}
}
}

View File

@ -0,0 +1,100 @@
using System;
using System.IO;
namespace LibHac.Savefile
{
public class AllocationTableStream : Stream
{
private int SegmentPos => (int)(Data.Position - (Iterator.PhysicalBlock * BlockSize));
private Stream Data { get; }
private int BlockSize { get; }
private AllocationTableIterator Iterator { get; }
public AllocationTableStream(Stream data, AllocationTable table, int blockSize, int initialBlock, long length)
{
Data = data;
BlockSize = blockSize;
Length = length;
Iterator = new AllocationTableIterator(table, initialBlock);
if (!Iterator.IsValid) Length = 0;
Data.Position = Iterator.PhysicalBlock * BlockSize;
}
public override void Flush()
{
throw new NotImplementedException();
}
public override int Read(byte[] buffer, int offset, int count)
{
int remaining = count;
int outOffset = offset;
int totalBytesRead = 0;
while (remaining > 0)
{
var remainingInSegment = Iterator.CurrentSegmentSize * BlockSize - SegmentPos;
int bytesToRead = Math.Min(remaining, remainingInSegment);
int bytesRead = Data.Read(buffer, outOffset, bytesToRead);
outOffset += bytesRead;
totalBytesRead += bytesRead;
remaining -= bytesRead;
if (SegmentPos >= Iterator.CurrentSegmentSize * BlockSize)
{
if (!Iterator.MoveNext()) return totalBytesRead;
Data.Position = Iterator.PhysicalBlock * BlockSize;
}
}
return totalBytesRead;
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotImplementedException();
}
public override void SetLength(long value)
{
throw new NotImplementedException();
}
public override void Write(byte[] buffer, int offset, int count)
{
throw new NotImplementedException();
}
public override bool CanRead => true;
public override bool CanSeek => true;
public override bool CanWrite => false;
public override long Length { get; }
public override long Position
{
get => Iterator.VirtualBlock * BlockSize + (Data.Position - (Iterator.PhysicalBlock * BlockSize));
set
{
long blockIndex = value / BlockSize;
while (Iterator.VirtualBlock > blockIndex ||
Iterator.VirtualBlock + Iterator.CurrentSegmentSize <= blockIndex)
{
if (Iterator.VirtualBlock > blockIndex)
{
Iterator.MovePrevious();
}
else
{
Iterator.MoveNext();
}
}
var segmentPos = value - (Iterator.VirtualBlock * BlockSize);
Data.Position = Iterator.PhysicalBlock * BlockSize + segmentPos;
}
}
}
}

View File

@ -15,6 +15,7 @@ namespace LibHac.Savefile
public SharedStreamSource MetaRemapSource { get; }
private JournalStream JournalStream { get; }
public SharedStreamSource JournalStreamSource { get; }
private AllocationTable AllocationTable { get; }
public Stream DuplexL1A { get; }
public Stream DuplexL1B { get; }
@ -88,6 +89,7 @@ namespace LibHac.Savefile
JournalLayer2Hash = MetaRemapSource.CreateStream(layout.Layer2HashOffset, layout.Layer2HashSize);
JournalLayer3Hash = MetaRemapSource.CreateStream(layout.Layer3HashOffset, layout.Layer3HashSize);
JournalFat = MetaRemapSource.CreateStream(layout.Field148, layout.Field150);
AllocationTable = new AllocationTable(JournalFat);
var journalMap = JournalStream.ReadMappingEntries(JournalTable, Header.Journal.MappingEntryCount);
@ -118,7 +120,18 @@ namespace LibHac.Savefile
public Stream OpenFile(FileEntry file)
{
return JournalStreamSource.CreateStream(file.Offset, file.Size);
if (file.BlockIndex < 0)
{
//todo replace
return JournalStreamSource.CreateStream(0, 0);
}
return OpenFatBlock(file.BlockIndex, file.Size);
}
private AllocationTableStream OpenFatBlock(int blockIndex, long size)
{
return new AllocationTableStream(JournalStreamSource.CreateStream(), AllocationTable, (int)Header.Save.BlockSize, blockIndex, size);
}
public bool FileExists(string filename) => FileDict.ContainsKey(filename);
@ -126,19 +139,13 @@ namespace LibHac.Savefile
private void ReadFileInfo()
{
var blockSize = Header.Save.BlockSize;
var dirOffset = Header.Save.DirectoryTableBlock * blockSize;
var fileOffset = Header.Save.FileTableBlock * blockSize;
FileEntry[] dirEntries;
FileEntry[] fileEntries;
using (var reader = new BinaryReader(JournalStreamSource.CreateStream(), Encoding.Default, true))
{
reader.BaseStream.Position = dirOffset;
dirEntries = ReadFileEntries(reader);
// todo: Query the FAT for the file size when none is given
var dirTableStream = OpenFatBlock(Header.Save.DirectoryTableBlock, 1000000);
var fileTableStream = OpenFatBlock(Header.Save.FileTableBlock, 1000000);
reader.BaseStream.Position = fileOffset;
fileEntries = ReadFileEntries(reader);
}
FileEntry[] dirEntries = ReadFileEntries(dirTableStream);
FileEntry[] fileEntries = ReadFileEntries(fileTableStream);
foreach (var dir in dirEntries)
{
@ -161,9 +168,11 @@ namespace LibHac.Savefile
FileEntry.ResolveFilenames(Files);
}
private FileEntry[] ReadFileEntries(BinaryReader reader)
private FileEntry[] ReadFileEntries(Stream stream)
{
var reader = new BinaryReader(stream);
var count = reader.ReadInt32();
reader.BaseStream.Position -= 4;
var entries = new FileEntry[count];