mirror of
https://github.com/Thealexbarney/LibHac.git
synced 2024-11-14 10:49:41 +01:00
Add direct read support for the savedata file table
This commit is contained in:
parent
1110c32cb1
commit
f0c09d7712
8 changed files with 457 additions and 202 deletions
|
@ -1,7 +1,5 @@
|
|||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace LibHac.IO.RomFs
|
||||
{
|
||||
|
@ -82,7 +80,7 @@ namespace LibHac.IO.RomFs
|
|||
|
||||
public bool TryOpenFile(string path, out RomFileInfo fileInfo)
|
||||
{
|
||||
FindFileRecursive(GetUtf8Bytes(path), out RomEntryKey key);
|
||||
FindPathRecursive(Util.GetUtf8Bytes(path), out RomEntryKey key);
|
||||
|
||||
if (FileTable.TryGetValue(ref key, out RomKeyValuePair<FileRomEntry> keyValuePair))
|
||||
{
|
||||
|
@ -115,7 +113,7 @@ namespace LibHac.IO.RomFs
|
|||
/// otherwise, <see langword="false"/>.</returns>
|
||||
public bool TryOpenDirectory(string path, out FindPosition position)
|
||||
{
|
||||
FindDirectoryRecursive(GetUtf8Bytes(path), out RomEntryKey key);
|
||||
FindPathRecursive(Util.GetUtf8Bytes(path), out RomEntryKey key);
|
||||
|
||||
if (DirectoryTable.TryGetValue(ref key, out RomKeyValuePair<DirectoryRomEntry> keyValuePair))
|
||||
{
|
||||
|
@ -168,7 +166,7 @@ namespace LibHac.IO.RomFs
|
|||
position.NextFile = entry.NextSibling;
|
||||
info = entry.Info;
|
||||
|
||||
name = GetUtf8String(nameBytes);
|
||||
name = Util.GetUtf8String(nameBytes);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -191,7 +189,8 @@ namespace LibHac.IO.RomFs
|
|||
|
||||
ref DirectoryRomEntry entry = ref DirectoryTable.GetValueReference(position.NextDirectory, out Span<byte> nameBytes);
|
||||
position.NextDirectory = entry.NextSibling;
|
||||
name = GetUtf8String(nameBytes);
|
||||
|
||||
name = Util.GetUtf8String(nameBytes);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -205,7 +204,7 @@ namespace LibHac.IO.RomFs
|
|||
public void AddFile(string path, ref RomFileInfo fileInfo)
|
||||
{
|
||||
path = PathTools.Normalize(path);
|
||||
ReadOnlySpan<byte> pathBytes = GetUtf8Bytes(path);
|
||||
ReadOnlySpan<byte> pathBytes = Util.GetUtf8Bytes(path);
|
||||
|
||||
if(path == "/") throw new ArgumentException("Path cannot be empty");
|
||||
|
||||
|
@ -221,7 +220,7 @@ namespace LibHac.IO.RomFs
|
|||
{
|
||||
path = PathTools.Normalize(path);
|
||||
|
||||
CreateDirectoryRecursive(GetUtf8Bytes(path));
|
||||
CreateDirectoryRecursive(Util.GetUtf8Bytes(path));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -237,21 +236,6 @@ namespace LibHac.IO.RomFs
|
|||
FileTable.TrimExcess();
|
||||
}
|
||||
|
||||
private static ReadOnlySpan<byte> GetUtf8Bytes(string value)
|
||||
{
|
||||
return Encoding.UTF8.GetBytes(value).AsSpan();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static string GetUtf8String(ReadOnlySpan<byte> value)
|
||||
{
|
||||
#if NETFRAMEWORK
|
||||
return Encoding.UTF8.GetString(value.ToArray());
|
||||
#else
|
||||
return Encoding.UTF8.GetString(value);
|
||||
#endif
|
||||
}
|
||||
|
||||
private void CreateRootDirectory()
|
||||
{
|
||||
var key = new RomEntryKey(ReadOnlySpan<byte>.Empty, 0);
|
||||
|
@ -371,26 +355,15 @@ namespace LibHac.IO.RomFs
|
|||
}
|
||||
}
|
||||
|
||||
private void FindFileRecursive(ReadOnlySpan<byte> path, out RomEntryKey key)
|
||||
private void FindPathRecursive(ReadOnlySpan<byte> path, out RomEntryKey key)
|
||||
{
|
||||
var parser = new PathParser(path);
|
||||
key = default;
|
||||
|
||||
while (parser.TryGetNext(out key.Name) && !parser.IsFinished())
|
||||
do
|
||||
{
|
||||
key.Parent = DirectoryTable.GetOffsetFromKey(ref key);
|
||||
}
|
||||
}
|
||||
|
||||
private void FindDirectoryRecursive(ReadOnlySpan<byte> path, out RomEntryKey key)
|
||||
{
|
||||
var parser = new PathParser(path);
|
||||
key = default;
|
||||
|
||||
while (parser.TryGetNext(out key.Name) && !parser.IsFinished())
|
||||
{
|
||||
key.Parent = DirectoryTable.GetOffsetFromKey(ref key);
|
||||
}
|
||||
} while (parser.TryGetNext(out key.Name) && !parser.IsFinished());
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
||||
|
|
127
src/LibHac/IO/Save/HierarchicalSaveFileTable.cs
Normal file
127
src/LibHac/IO/Save/HierarchicalSaveFileTable.cs
Normal file
|
@ -0,0 +1,127 @@
|
|||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace LibHac.IO.Save
|
||||
{
|
||||
public class HierarchicalSaveFileTable
|
||||
{
|
||||
private SaveFsList<FileSaveEntry> FileTable { get; }
|
||||
private SaveFsList<DirectorySaveEntry> DirectoryTable { get; }
|
||||
|
||||
public HierarchicalSaveFileTable(IStorage dirTable, IStorage fileTable)
|
||||
{
|
||||
FileTable = new SaveFsList<FileSaveEntry>(fileTable);
|
||||
DirectoryTable = new SaveFsList<DirectorySaveEntry>(dirTable);
|
||||
}
|
||||
|
||||
public bool TryOpenFile(string path, out SaveFileInfo fileInfo)
|
||||
{
|
||||
FindPathRecursive(Util.GetUtf8Bytes(path), out SaveEntryKey key);
|
||||
|
||||
if (FileTable.TryGetValue(ref key, out FileSaveEntry value))
|
||||
{
|
||||
fileInfo = value.Info;
|
||||
return true;
|
||||
}
|
||||
|
||||
fileInfo = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool FindNextFile(ref SaveFindPosition position, out SaveFileInfo info, out string name)
|
||||
{
|
||||
if (position.NextFile == 0)
|
||||
{
|
||||
info = default;
|
||||
name = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
Span<byte> nameBytes = stackalloc byte[FileTable.MaxNameLength];
|
||||
|
||||
bool success = FileTable.TryGetValue((int)position.NextFile, out FileSaveEntry entry, ref nameBytes);
|
||||
|
||||
// todo error message
|
||||
if (!success)
|
||||
{
|
||||
info = default;
|
||||
name = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
position.NextFile = entry.NextSibling;
|
||||
info = entry.Info;
|
||||
|
||||
name = Util.GetUtf8StringNullTerminated(nameBytes);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool FindNextDirectory(ref SaveFindPosition position, out string name)
|
||||
{
|
||||
if (position.NextDirectory == 0)
|
||||
{
|
||||
name = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
Span<byte> nameBytes = stackalloc byte[FileTable.MaxNameLength];
|
||||
|
||||
bool success = DirectoryTable.TryGetValue(position.NextDirectory, out DirectorySaveEntry entry, ref nameBytes);
|
||||
|
||||
// todo error message
|
||||
if (!success)
|
||||
{
|
||||
name = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
position.NextDirectory = entry.NextSibling;
|
||||
|
||||
name = Util.GetUtf8StringNullTerminated(nameBytes);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryOpenDirectory(string path, out SaveFindPosition position)
|
||||
{
|
||||
FindPathRecursive(Util.GetUtf8Bytes(path), out SaveEntryKey key);
|
||||
|
||||
if (DirectoryTable.TryGetValue(ref key, out DirectorySaveEntry value))
|
||||
{
|
||||
position = value.Pos;
|
||||
return true;
|
||||
}
|
||||
|
||||
position = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private void FindPathRecursive(ReadOnlySpan<byte> path, out SaveEntryKey key)
|
||||
{
|
||||
var parser = new PathParser(path);
|
||||
key = default;
|
||||
|
||||
do
|
||||
{
|
||||
key.Parent = DirectoryTable.GetOffsetFromKey(ref key);
|
||||
} while (parser.TryGetNext(out key.Name) && !parser.IsFinished());
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
private struct DirectorySaveEntry
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,43 +2,42 @@
|
|||
|
||||
namespace LibHac.IO.Save
|
||||
{
|
||||
class SaveDataDirectory : IDirectory
|
||||
public class SaveDataDirectory : IDirectory
|
||||
{
|
||||
public IFileSystem ParentFileSystem { get; }
|
||||
IFileSystem IDirectory.ParentFileSystem => ParentFileSystem;
|
||||
public SaveDataFileSystemCore ParentFileSystem { get; }
|
||||
public string FullPath { get; }
|
||||
|
||||
public OpenDirectoryMode Mode { get; }
|
||||
private SaveDirectoryEntry Directory { get; }
|
||||
|
||||
public SaveDataDirectory(SaveDataFileSystemCore fs, string path, SaveDirectoryEntry dir, OpenDirectoryMode mode)
|
||||
private SaveFindPosition InitialPosition { get; }
|
||||
|
||||
public SaveDataDirectory(SaveDataFileSystemCore fs, string path, SaveFindPosition position, OpenDirectoryMode mode)
|
||||
{
|
||||
ParentFileSystem = fs;
|
||||
Directory = dir;
|
||||
InitialPosition = position;
|
||||
FullPath = path;
|
||||
Mode = mode;
|
||||
}
|
||||
|
||||
public IEnumerable<DirectoryEntry> Read()
|
||||
{
|
||||
SaveFindPosition position = InitialPosition;
|
||||
HierarchicalSaveFileTable tab = ParentFileSystem.FileTable;
|
||||
|
||||
if (Mode.HasFlag(OpenDirectoryMode.Directories))
|
||||
{
|
||||
SaveDirectoryEntry dirEntry = Directory.FirstChild;
|
||||
|
||||
while (dirEntry != null)
|
||||
while (tab.FindNextDirectory(ref position, out string name))
|
||||
{
|
||||
yield return new DirectoryEntry(dirEntry.Name, FullPath + '/' + dirEntry.Name, DirectoryEntryType.Directory, 0);
|
||||
dirEntry = dirEntry.NextSibling;
|
||||
yield return new DirectoryEntry(name, FullPath + '/' + name, DirectoryEntryType.Directory, 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (Mode.HasFlag(OpenDirectoryMode.Files))
|
||||
{
|
||||
SaveFileEntry fileEntry = Directory.FirstFile;
|
||||
|
||||
while (fileEntry != null)
|
||||
while (tab.FindNextFile(ref position, out SaveFileInfo info, out string name))
|
||||
{
|
||||
yield return new DirectoryEntry(fileEntry.Name, FullPath + '/' + fileEntry.Name, DirectoryEntryType.File, fileEntry.FileSize);
|
||||
fileEntry = fileEntry.NextSibling;
|
||||
yield return new DirectoryEntry(name, FullPath + '/' + name, DirectoryEntryType.File, info.Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -47,25 +46,22 @@ namespace LibHac.IO.Save
|
|||
{
|
||||
int count = 0;
|
||||
|
||||
SaveFindPosition position = InitialPosition;
|
||||
HierarchicalSaveFileTable tab = ParentFileSystem.FileTable;
|
||||
|
||||
if (Mode.HasFlag(OpenDirectoryMode.Directories))
|
||||
{
|
||||
SaveDirectoryEntry dirEntry = Directory.FirstChild;
|
||||
|
||||
while (dirEntry != null)
|
||||
while (tab.FindNextDirectory(ref position, out string _))
|
||||
{
|
||||
count++;
|
||||
dirEntry = dirEntry.NextSibling;
|
||||
}
|
||||
}
|
||||
|
||||
if (Mode.HasFlag(OpenDirectoryMode.Files))
|
||||
{
|
||||
SaveFileEntry fileEntry = Directory.FirstFile;
|
||||
|
||||
while (fileEntry != null)
|
||||
while (tab.FindNextFile(ref position, out SaveFileInfo _, out string _))
|
||||
{
|
||||
count++;
|
||||
fileEntry = fileEntry.NextSibling;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO;
|
||||
|
||||
namespace LibHac.IO.Save
|
||||
{
|
||||
|
@ -11,11 +10,7 @@ namespace LibHac.IO.Save
|
|||
public AllocationTable AllocationTable { get; }
|
||||
private SaveHeader Header { get; }
|
||||
|
||||
public SaveDirectoryEntry RootDirectory { get; private set; }
|
||||
private SaveFileEntry[] Files { get; set; }
|
||||
private SaveDirectoryEntry[] Directories { get; set; }
|
||||
private Dictionary<string, SaveFileEntry> FileDictionary { get; }
|
||||
private Dictionary<string, SaveDirectoryEntry> DirDictionary { get; }
|
||||
public HierarchicalSaveFileTable FileTable { get; }
|
||||
|
||||
public SaveDataFileSystemCore(IStorage storage, IStorage allocationTable, IStorage header)
|
||||
{
|
||||
|
@ -24,40 +19,12 @@ 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);
|
||||
|
||||
ReadFileInfo();
|
||||
|
||||
FileDictionary = new Dictionary<string, SaveFileEntry>();
|
||||
foreach (SaveFileEntry entry in Files)
|
||||
{
|
||||
FileDictionary[entry.FullPath] = entry;
|
||||
}
|
||||
|
||||
DirDictionary = new Dictionary<string, SaveDirectoryEntry>();
|
||||
foreach (SaveDirectoryEntry entry in Directories)
|
||||
{
|
||||
DirDictionary[entry.FullPath] = entry;
|
||||
}
|
||||
}
|
||||
|
||||
public IStorage OpenFile(string filename)
|
||||
{
|
||||
if (!FileDictionary.TryGetValue(filename, out SaveFileEntry file))
|
||||
{
|
||||
throw new FileNotFoundException();
|
||||
}
|
||||
|
||||
return OpenFile(file);
|
||||
}
|
||||
|
||||
public IStorage OpenFile(SaveFileEntry file)
|
||||
{
|
||||
if (file.BlockIndex < 0)
|
||||
{
|
||||
return new NullStorage(0);
|
||||
}
|
||||
|
||||
return OpenFatBlock(file.BlockIndex, file.FileSize);
|
||||
FileTable = new HierarchicalSaveFileTable(dirTableStorage, fileTableStorage);
|
||||
}
|
||||
|
||||
public void CreateDirectory(string path)
|
||||
|
@ -84,31 +51,31 @@ namespace LibHac.IO.Save
|
|||
{
|
||||
path = PathTools.Normalize(path);
|
||||
|
||||
if (!DirDictionary.TryGetValue(path, out SaveDirectoryEntry dir))
|
||||
if (!FileTable.TryOpenDirectory(path, out SaveFindPosition position))
|
||||
{
|
||||
throw new DirectoryNotFoundException(path);
|
||||
throw new DirectoryNotFoundException();
|
||||
}
|
||||
|
||||
return new SaveDataDirectory(this, path, dir, mode);
|
||||
return new SaveDataDirectory(this, path, position, mode);
|
||||
}
|
||||
|
||||
public IFile OpenFile(string path, OpenMode mode)
|
||||
{
|
||||
path = PathTools.Normalize(path);
|
||||
|
||||
if (!FileDictionary.TryGetValue(path, out SaveFileEntry file))
|
||||
if (!FileTable.TryOpenFile(path, out SaveFileInfo file))
|
||||
{
|
||||
throw new FileNotFoundException();
|
||||
}
|
||||
|
||||
if (file.BlockIndex < 0)
|
||||
if (file.StartBlock < 0)
|
||||
{
|
||||
return new NullFile();
|
||||
}
|
||||
|
||||
AllocationTableStorage storage = OpenFatBlock(file.BlockIndex, file.FileSize);
|
||||
AllocationTableStorage storage = OpenFatBlock(file.StartBlock, file.Length);
|
||||
|
||||
return new SaveDataFile(storage, 0, file.FileSize, mode);
|
||||
return new SaveDataFile(storage, 0, file.Length, mode);
|
||||
}
|
||||
|
||||
public void RenameDirectory(string srcPath, string dstPath)
|
||||
|
@ -125,22 +92,22 @@ namespace LibHac.IO.Save
|
|||
{
|
||||
path = PathTools.Normalize(path);
|
||||
|
||||
return DirDictionary.ContainsKey(path);
|
||||
return FileTable.TryOpenDirectory(path, out SaveFindPosition _);
|
||||
}
|
||||
|
||||
public bool FileExists(string path)
|
||||
{
|
||||
path = PathTools.Normalize(path);
|
||||
|
||||
return FileDictionary.ContainsKey(path);
|
||||
return FileTable.TryOpenFile(path, out SaveFileInfo _);
|
||||
}
|
||||
|
||||
public DirectoryEntryType GetEntryType(string path)
|
||||
{
|
||||
path = PathTools.Normalize(path);
|
||||
|
||||
if (DirDictionary.ContainsKey(path)) return DirectoryEntryType.Directory;
|
||||
if (FileDictionary.ContainsKey(path)) return DirectoryEntryType.File;
|
||||
if (FileExists(path)) return DirectoryEntryType.File;
|
||||
if (DirectoryExists(path)) return DirectoryEntryType.Directory;
|
||||
|
||||
throw new FileNotFoundException(path);
|
||||
}
|
||||
|
@ -152,91 +119,6 @@ namespace LibHac.IO.Save
|
|||
|
||||
public IStorage GetBaseStorage() => BaseStorage.AsReadOnly();
|
||||
public IStorage GetHeaderStorage() => HeaderStorage.AsReadOnly();
|
||||
public SaveFileEntry GetFileEntry(string path) => FileDictionary[path];
|
||||
|
||||
private void ReadFileInfo()
|
||||
{
|
||||
// todo: Query the FAT for the file size when none is given
|
||||
AllocationTableStorage dirTableStream = OpenFatBlock(AllocationTable.Header.DirectoryTableBlock, 1000000);
|
||||
AllocationTableStorage fileTableStream = OpenFatBlock(AllocationTable.Header.FileTableBlock, 1000000);
|
||||
|
||||
SaveDirectoryEntry[] dirEntries = ReadDirEntries(dirTableStream);
|
||||
SaveFileEntry[] fileEntries = ReadFileEntries(fileTableStream);
|
||||
|
||||
foreach (SaveDirectoryEntry dir in dirEntries)
|
||||
{
|
||||
if (dir.NextSiblingIndex != 0) dir.NextSibling = dirEntries[dir.NextSiblingIndex];
|
||||
if (dir.FirstChildIndex != 0) dir.FirstChild = dirEntries[dir.FirstChildIndex];
|
||||
if (dir.FirstFileIndex != 0) dir.FirstFile = fileEntries[dir.FirstFileIndex];
|
||||
if (dir.NextInChainIndex != 0) dir.NextInChain = dirEntries[dir.NextInChainIndex];
|
||||
if (dir.ParentDirIndex != 0 && dir.ParentDirIndex < dirEntries.Length)
|
||||
dir.ParentDir = dirEntries[dir.ParentDirIndex];
|
||||
}
|
||||
|
||||
foreach (SaveFileEntry file in fileEntries)
|
||||
{
|
||||
if (file.NextSiblingIndex != 0) file.NextSibling = fileEntries[file.NextSiblingIndex];
|
||||
if (file.NextInChainIndex != 0) file.NextInChain = fileEntries[file.NextInChainIndex];
|
||||
if (file.ParentDirIndex != 0 && file.ParentDirIndex < dirEntries.Length)
|
||||
file.ParentDir = dirEntries[file.ParentDirIndex];
|
||||
}
|
||||
|
||||
RootDirectory = dirEntries[2];
|
||||
|
||||
SaveFileEntry fileChain = fileEntries[1].NextInChain;
|
||||
var files = new List<SaveFileEntry>();
|
||||
while (fileChain != null)
|
||||
{
|
||||
files.Add(fileChain);
|
||||
fileChain = fileChain.NextInChain;
|
||||
}
|
||||
|
||||
SaveDirectoryEntry dirChain = dirEntries[1].NextInChain;
|
||||
var dirs = new List<SaveDirectoryEntry>();
|
||||
while (dirChain != null)
|
||||
{
|
||||
dirs.Add(dirChain);
|
||||
dirChain = dirChain.NextInChain;
|
||||
}
|
||||
|
||||
Files = files.ToArray();
|
||||
Directories = dirs.ToArray();
|
||||
|
||||
SaveFsEntry.ResolveFilenames(Files);
|
||||
SaveFsEntry.ResolveFilenames(Directories);
|
||||
}
|
||||
|
||||
private SaveFileEntry[] ReadFileEntries(IStorage storage)
|
||||
{
|
||||
var reader = new BinaryReader(storage.AsStream());
|
||||
int count = reader.ReadInt32();
|
||||
|
||||
reader.BaseStream.Position -= 4;
|
||||
|
||||
var entries = new SaveFileEntry[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
entries[i] = new SaveFileEntry(reader);
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
private SaveDirectoryEntry[] ReadDirEntries(IStorage storage)
|
||||
{
|
||||
var reader = new BinaryReader(storage.AsStream());
|
||||
int count = reader.ReadInt32();
|
||||
|
||||
reader.BaseStream.Position -= 4;
|
||||
|
||||
var entries = new SaveDirectoryEntry[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
entries[i] = new SaveDirectoryEntry(reader);
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
private AllocationTableStorage OpenFatBlock(int blockIndex, long size)
|
||||
{
|
||||
|
|
36
src/LibHac/IO/Save/SaveFsEntries.cs
Normal file
36
src/LibHac/IO/Save/SaveFsEntries.cs
Normal file
|
@ -0,0 +1,36 @@
|
|||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace LibHac.IO.Save
|
||||
{
|
||||
internal ref struct SaveEntryKey
|
||||
{
|
||||
public ReadOnlySpan<byte> Name;
|
||||
public int Parent;
|
||||
|
||||
public SaveEntryKey(ReadOnlySpan<byte> name, int parent)
|
||||
{
|
||||
Name = name;
|
||||
Parent = parent;
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public struct SaveFileInfo
|
||||
{
|
||||
public int StartBlock;
|
||||
public long Length;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the current position when enumerating a directory's contents.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
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;
|
||||
}
|
||||
}
|
176
src/LibHac/IO/Save/SaveFsList.cs
Normal file
176
src/LibHac/IO/Save/SaveFsList.cs
Normal file
|
@ -0,0 +1,176 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace LibHac.IO.Save
|
||||
{
|
||||
internal class SaveFsList<T> where T : unmanaged
|
||||
{
|
||||
private const int FreeListHeadIndex = 0;
|
||||
private const int UsedListHeadIndex = 1;
|
||||
public int MaxNameLength { get; } = 0x40;
|
||||
|
||||
private IStorage Storage { get; }
|
||||
|
||||
private readonly int _sizeOfEntry = Unsafe.SizeOf<SaveFsEntry>();
|
||||
|
||||
public SaveFsList(IStorage tableStorage)
|
||||
{
|
||||
Storage = tableStorage;
|
||||
}
|
||||
|
||||
public int GetOffsetFromKey(ref SaveEntryKey key)
|
||||
{
|
||||
Span<byte> entryBytes = stackalloc byte[_sizeOfEntry];
|
||||
Span<byte> name = entryBytes.Slice(4, MaxNameLength);
|
||||
ref SaveFsEntry entry = ref GetEntryFromBytes(entryBytes);
|
||||
|
||||
int capacity = GetListCapacity();
|
||||
int entryId = -1;
|
||||
|
||||
ReadEntry(UsedListHeadIndex, entryBytes);
|
||||
|
||||
while (entry.Next > 0)
|
||||
{
|
||||
if (entry.Next > capacity) throw new IndexOutOfRangeException("Save entry index out of range");
|
||||
|
||||
entryId = entry.Next;
|
||||
ReadEntry(entry.Next, out entry);
|
||||
|
||||
if (entry.Parent == key.Parent && Util.StringSpansEqual(name, key.Name))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return entryId;
|
||||
}
|
||||
|
||||
public bool TryGetValue(ref SaveEntryKey key, out T value)
|
||||
{
|
||||
int index = GetOffsetFromKey(ref key);
|
||||
|
||||
if (index < 0)
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
return TryGetValue(index, out value);
|
||||
}
|
||||
|
||||
public bool TryGetValue(int index, out T value)
|
||||
{
|
||||
if (index < 0 || index >= GetListCapacity())
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
GetValue(index, out value);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void GetValue(int index, out T value)
|
||||
{
|
||||
ReadEntry(index, out SaveFsEntry entry);
|
||||
value = entry.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value and name associated with the specific index.
|
||||
/// </summary>
|
||||
/// <param name="index">The index of the value to get.</param>
|
||||
/// <param name="value">Contains the corresponding value if the method returns <see langword="true"/>.</param>
|
||||
/// <param name="name">The name of the given index will be written to this span if the method returns <see langword="true"/>.
|
||||
/// This span must be at least <see cref="MaxNameLength"/> bytes long.</param>
|
||||
/// <returns><see langword="true"/> if the <see cref="SaveFsList{T}"/> contains an element with
|
||||
/// the specified key; otherwise, <see langword="false"/>.</returns>
|
||||
public bool TryGetValue(int index, out T value, ref Span<byte> name)
|
||||
{
|
||||
Debug.Assert(name.Length >= MaxNameLength);
|
||||
|
||||
if (index < 0 || index >= GetListCapacity())
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
GetValue(index, out value, ref name);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value and name associated with the specific index.
|
||||
/// </summary>
|
||||
/// <param name="index">The index of the value to get.</param>
|
||||
/// <param name="value">Contains the corresponding value when the method returns.</param>
|
||||
/// <param name="name">The name of the given index will be written to this span when the method returns.
|
||||
/// This span must be at least <see cref="MaxNameLength"/> bytes long.</param>
|
||||
public void GetValue(int index, out T value, ref Span<byte> name)
|
||||
{
|
||||
Debug.Assert(name.Length >= MaxNameLength);
|
||||
|
||||
Span<byte> entryBytes = stackalloc byte[_sizeOfEntry];
|
||||
Span<byte> nameSpan = entryBytes.Slice(4, MaxNameLength);
|
||||
ref SaveFsEntry entry = ref GetEntryFromBytes(entryBytes);
|
||||
|
||||
ReadEntry(index, out entry);
|
||||
|
||||
nameSpan.CopyTo(name);
|
||||
value = entry.Value;
|
||||
}
|
||||
|
||||
private int GetListCapacity()
|
||||
{
|
||||
Span<byte> buf = stackalloc byte[sizeof(int)];
|
||||
Storage.Read(buf, 4);
|
||||
|
||||
return MemoryMarshal.Read<int>(buf);
|
||||
}
|
||||
|
||||
private int GetListLength()
|
||||
{
|
||||
Span<byte> buf = stackalloc byte[sizeof(int)];
|
||||
Storage.Read(buf, 0);
|
||||
|
||||
return MemoryMarshal.Read<int>(buf);
|
||||
}
|
||||
|
||||
private void ReadEntry(int index, out SaveFsEntry entry)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[_sizeOfEntry];
|
||||
ReadEntry(index, bytes);
|
||||
|
||||
entry = GetEntryFromBytes(bytes);
|
||||
}
|
||||
|
||||
private void ReadEntry(int index, Span<byte> entry)
|
||||
{
|
||||
Debug.Assert(entry.Length == _sizeOfEntry);
|
||||
|
||||
int offset = index * _sizeOfEntry;
|
||||
Storage.Read(entry, offset);
|
||||
}
|
||||
|
||||
private ref SaveFsEntry GetEntryFromBytes(Span<byte> entry)
|
||||
{
|
||||
return ref MemoryMarshal.Cast<byte, SaveFsEntry>(entry)[0];
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
private struct SaveFsEntry
|
||||
{
|
||||
public int Parent;
|
||||
private NameDummy Name;
|
||||
public T Value;
|
||||
public int Next;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Size = 0x40)]
|
||||
private struct NameDummy { }
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
|
@ -64,6 +65,71 @@ namespace LibHac
|
|||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares two strings stored int byte spans. For the strings to be equal,
|
||||
/// they must terminate in the same place.
|
||||
/// A string can be terminated by either a null character or the end of the span.
|
||||
/// </summary>
|
||||
/// <param name="s1">The first string to be compared.</param>
|
||||
/// <param name="s2">The first string to be compared.</param>
|
||||
/// <returns><see langword="true"/> if the strings are equal;
|
||||
/// otherwise <see langword="false"/>.</returns>
|
||||
public static bool StringSpansEqual(ReadOnlySpan<byte> s1, ReadOnlySpan<byte> s2)
|
||||
{
|
||||
// Make s1 the long string for simplicity
|
||||
if (s1.Length < s2.Length)
|
||||
{
|
||||
ReadOnlySpan<byte> tmp = s1;
|
||||
s1 = s2;
|
||||
s2 = tmp;
|
||||
}
|
||||
|
||||
int shortLength = s2.Length;
|
||||
int i;
|
||||
|
||||
for (i = 0; i < shortLength; i++)
|
||||
{
|
||||
if (s1[i] != s2[i]) return false;
|
||||
|
||||
// Both strings are null-terminated
|
||||
if (s1[i] == 0) return true;
|
||||
}
|
||||
|
||||
// The bytes in the short string equal those in the long.
|
||||
// Check if the strings are the same length or if the next
|
||||
// character in the long string is a null character
|
||||
return s1.Length == s2.Length || s1[i] == 0;
|
||||
}
|
||||
|
||||
public static ReadOnlySpan<byte> GetUtf8Bytes(string value)
|
||||
{
|
||||
return Encoding.UTF8.GetBytes(value).AsSpan();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static string GetUtf8String(ReadOnlySpan<byte> value)
|
||||
{
|
||||
#if NETFRAMEWORK
|
||||
return Encoding.UTF8.GetString(value.ToArray());
|
||||
#else
|
||||
return Encoding.UTF8.GetString(value);
|
||||
#endif
|
||||
}
|
||||
|
||||
public static string GetUtf8StringNullTerminated(ReadOnlySpan<byte> value)
|
||||
{
|
||||
int i;
|
||||
for (i = 0; i < value.Length && value[i] != 0; i++) { }
|
||||
|
||||
value = value.Slice(0, i);
|
||||
|
||||
#if NETFRAMEWORK
|
||||
return Encoding.UTF8.GetString(value.ToArray());
|
||||
#else
|
||||
return Encoding.UTF8.GetString(value);
|
||||
#endif
|
||||
}
|
||||
|
||||
public static bool IsEmpty(this byte[] array) => ((ReadOnlySpan<byte>)array).IsEmpty();
|
||||
|
||||
public static bool IsEmpty(this ReadOnlySpan<byte> span)
|
||||
|
@ -128,7 +194,7 @@ namespace LibHac
|
|||
}
|
||||
}
|
||||
|
||||
public static string ReadAsciiZ(this BinaryReader reader, int maxLength = int.MaxValue)
|
||||
public static string ReadAsciiZ(this BinaryReader reader, int maxLength = Int32.MaxValue)
|
||||
{
|
||||
long start = reader.BaseStream.Position;
|
||||
int size = 0;
|
||||
|
@ -145,7 +211,7 @@ namespace LibHac
|
|||
return text;
|
||||
}
|
||||
|
||||
public static string ReadUtf8Z(this BinaryReader reader, int maxLength = int.MaxValue)
|
||||
public static string ReadUtf8Z(this BinaryReader reader, int maxLength = Int32.MaxValue)
|
||||
{
|
||||
long start = reader.BaseStream.Position;
|
||||
int size = 0;
|
||||
|
|
|
@ -163,11 +163,10 @@ namespace hactoolnet
|
|||
|
||||
foreach (DirectoryEntry entry in save.EnumerateEntries().Where(x => x.Type == DirectoryEntryType.File))
|
||||
{
|
||||
SaveFileEntry saveEntry = save.SaveDataFileSystemCore.GetFileEntry(entry.FullPath);
|
||||
save.SaveDataFileSystemCore.FileTable.TryOpenFile(entry.FullPath, out SaveFileInfo fileInfo);
|
||||
if (fileInfo.StartBlock < 0) continue;
|
||||
|
||||
if (saveEntry.BlockIndex < 0) continue;
|
||||
|
||||
IEnumerable<(int block, int length)> chain = save.SaveDataFileSystemCore.AllocationTable.DumpChain(saveEntry.BlockIndex);
|
||||
IEnumerable<(int block, int length)> chain = save.SaveDataFileSystemCore.AllocationTable.DumpChain(fileInfo.StartBlock);
|
||||
|
||||
sb.AppendLine(entry.FullPath);
|
||||
sb.AppendLine(PrintBlockChain(chain));
|
||||
|
|
Loading…
Reference in a new issue