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;
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace LibHac.IO.RomFs
|
namespace LibHac.IO.RomFs
|
||||||
{
|
{
|
||||||
|
@ -82,7 +80,7 @@ namespace LibHac.IO.RomFs
|
||||||
|
|
||||||
public bool TryOpenFile(string path, out RomFileInfo fileInfo)
|
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))
|
if (FileTable.TryGetValue(ref key, out RomKeyValuePair<FileRomEntry> keyValuePair))
|
||||||
{
|
{
|
||||||
|
@ -115,7 +113,7 @@ namespace LibHac.IO.RomFs
|
||||||
/// otherwise, <see langword="false"/>.</returns>
|
/// otherwise, <see langword="false"/>.</returns>
|
||||||
public bool TryOpenDirectory(string path, out FindPosition position)
|
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))
|
if (DirectoryTable.TryGetValue(ref key, out RomKeyValuePair<DirectoryRomEntry> keyValuePair))
|
||||||
{
|
{
|
||||||
|
@ -168,7 +166,7 @@ namespace LibHac.IO.RomFs
|
||||||
position.NextFile = entry.NextSibling;
|
position.NextFile = entry.NextSibling;
|
||||||
info = entry.Info;
|
info = entry.Info;
|
||||||
|
|
||||||
name = GetUtf8String(nameBytes);
|
name = Util.GetUtf8String(nameBytes);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -191,7 +189,8 @@ namespace LibHac.IO.RomFs
|
||||||
|
|
||||||
ref DirectoryRomEntry entry = ref DirectoryTable.GetValueReference(position.NextDirectory, out Span<byte> nameBytes);
|
ref DirectoryRomEntry entry = ref DirectoryTable.GetValueReference(position.NextDirectory, out Span<byte> nameBytes);
|
||||||
position.NextDirectory = entry.NextSibling;
|
position.NextDirectory = entry.NextSibling;
|
||||||
name = GetUtf8String(nameBytes);
|
|
||||||
|
name = Util.GetUtf8String(nameBytes);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -205,7 +204,7 @@ namespace LibHac.IO.RomFs
|
||||||
public void AddFile(string path, ref RomFileInfo fileInfo)
|
public void AddFile(string path, ref RomFileInfo fileInfo)
|
||||||
{
|
{
|
||||||
path = PathTools.Normalize(path);
|
path = PathTools.Normalize(path);
|
||||||
ReadOnlySpan<byte> pathBytes = GetUtf8Bytes(path);
|
ReadOnlySpan<byte> pathBytes = Util.GetUtf8Bytes(path);
|
||||||
|
|
||||||
if(path == "/") throw new ArgumentException("Path cannot be empty");
|
if(path == "/") throw new ArgumentException("Path cannot be empty");
|
||||||
|
|
||||||
|
@ -221,7 +220,7 @@ namespace LibHac.IO.RomFs
|
||||||
{
|
{
|
||||||
path = PathTools.Normalize(path);
|
path = PathTools.Normalize(path);
|
||||||
|
|
||||||
CreateDirectoryRecursive(GetUtf8Bytes(path));
|
CreateDirectoryRecursive(Util.GetUtf8Bytes(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -237,21 +236,6 @@ namespace LibHac.IO.RomFs
|
||||||
FileTable.TrimExcess();
|
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()
|
private void CreateRootDirectory()
|
||||||
{
|
{
|
||||||
var key = new RomEntryKey(ReadOnlySpan<byte>.Empty, 0);
|
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);
|
var parser = new PathParser(path);
|
||||||
key = default;
|
key = default;
|
||||||
|
|
||||||
while (parser.TryGetNext(out key.Name) && !parser.IsFinished())
|
do
|
||||||
{
|
{
|
||||||
key.Parent = DirectoryTable.GetOffsetFromKey(ref key);
|
key.Parent = DirectoryTable.GetOffsetFromKey(ref key);
|
||||||
}
|
} while (parser.TryGetNext(out key.Name) && !parser.IsFinished());
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
[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
|
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 string FullPath { get; }
|
||||||
|
|
||||||
public OpenDirectoryMode Mode { 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;
|
ParentFileSystem = fs;
|
||||||
Directory = dir;
|
InitialPosition = position;
|
||||||
FullPath = path;
|
FullPath = path;
|
||||||
Mode = mode;
|
Mode = mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<DirectoryEntry> Read()
|
public IEnumerable<DirectoryEntry> Read()
|
||||||
{
|
{
|
||||||
|
SaveFindPosition position = InitialPosition;
|
||||||
|
HierarchicalSaveFileTable tab = ParentFileSystem.FileTable;
|
||||||
|
|
||||||
if (Mode.HasFlag(OpenDirectoryMode.Directories))
|
if (Mode.HasFlag(OpenDirectoryMode.Directories))
|
||||||
{
|
{
|
||||||
SaveDirectoryEntry dirEntry = Directory.FirstChild;
|
while (tab.FindNextDirectory(ref position, out string name))
|
||||||
|
|
||||||
while (dirEntry != null)
|
|
||||||
{
|
{
|
||||||
yield return new DirectoryEntry(dirEntry.Name, FullPath + '/' + dirEntry.Name, DirectoryEntryType.Directory, 0);
|
yield return new DirectoryEntry(name, FullPath + '/' + name, DirectoryEntryType.Directory, 0);
|
||||||
dirEntry = dirEntry.NextSibling;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Mode.HasFlag(OpenDirectoryMode.Files))
|
if (Mode.HasFlag(OpenDirectoryMode.Files))
|
||||||
{
|
{
|
||||||
SaveFileEntry fileEntry = Directory.FirstFile;
|
while (tab.FindNextFile(ref position, out SaveFileInfo info, out string name))
|
||||||
|
|
||||||
while (fileEntry != null)
|
|
||||||
{
|
{
|
||||||
yield return new DirectoryEntry(fileEntry.Name, FullPath + '/' + fileEntry.Name, DirectoryEntryType.File, fileEntry.FileSize);
|
yield return new DirectoryEntry(name, FullPath + '/' + name, DirectoryEntryType.File, info.Length);
|
||||||
fileEntry = fileEntry.NextSibling;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,25 +46,22 @@ namespace LibHac.IO.Save
|
||||||
{
|
{
|
||||||
int count = 0;
|
int count = 0;
|
||||||
|
|
||||||
|
SaveFindPosition position = InitialPosition;
|
||||||
|
HierarchicalSaveFileTable tab = ParentFileSystem.FileTable;
|
||||||
|
|
||||||
if (Mode.HasFlag(OpenDirectoryMode.Directories))
|
if (Mode.HasFlag(OpenDirectoryMode.Directories))
|
||||||
{
|
{
|
||||||
SaveDirectoryEntry dirEntry = Directory.FirstChild;
|
while (tab.FindNextDirectory(ref position, out string _))
|
||||||
|
|
||||||
while (dirEntry != null)
|
|
||||||
{
|
{
|
||||||
count++;
|
count++;
|
||||||
dirEntry = dirEntry.NextSibling;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Mode.HasFlag(OpenDirectoryMode.Files))
|
if (Mode.HasFlag(OpenDirectoryMode.Files))
|
||||||
{
|
{
|
||||||
SaveFileEntry fileEntry = Directory.FirstFile;
|
while (tab.FindNextFile(ref position, out SaveFileInfo _, out string _))
|
||||||
|
|
||||||
while (fileEntry != null)
|
|
||||||
{
|
{
|
||||||
count++;
|
count++;
|
||||||
fileEntry = fileEntry.NextSibling;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
using System.Collections.Generic;
|
using System.IO;
|
||||||
using System.IO;
|
|
||||||
|
|
||||||
namespace LibHac.IO.Save
|
namespace LibHac.IO.Save
|
||||||
{
|
{
|
||||||
|
@ -11,11 +10,7 @@ namespace LibHac.IO.Save
|
||||||
public AllocationTable AllocationTable { get; }
|
public AllocationTable AllocationTable { get; }
|
||||||
private SaveHeader Header { get; }
|
private SaveHeader Header { get; }
|
||||||
|
|
||||||
public SaveDirectoryEntry RootDirectory { get; private set; }
|
public HierarchicalSaveFileTable FileTable { get; }
|
||||||
private SaveFileEntry[] Files { get; set; }
|
|
||||||
private SaveDirectoryEntry[] Directories { get; set; }
|
|
||||||
private Dictionary<string, SaveFileEntry> FileDictionary { get; }
|
|
||||||
private Dictionary<string, SaveDirectoryEntry> DirDictionary { get; }
|
|
||||||
|
|
||||||
public SaveDataFileSystemCore(IStorage storage, IStorage allocationTable, IStorage header)
|
public SaveDataFileSystemCore(IStorage storage, IStorage allocationTable, IStorage header)
|
||||||
{
|
{
|
||||||
|
@ -24,40 +19,12 @@ namespace LibHac.IO.Save
|
||||||
AllocationTable = new AllocationTable(allocationTable, header.Slice(0x18, 0x30));
|
AllocationTable = new AllocationTable(allocationTable, header.Slice(0x18, 0x30));
|
||||||
|
|
||||||
Header = new SaveHeader(HeaderStorage);
|
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();
|
FileTable = new HierarchicalSaveFileTable(dirTableStorage, fileTableStorage);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void CreateDirectory(string path)
|
public void CreateDirectory(string path)
|
||||||
|
@ -84,31 +51,31 @@ namespace LibHac.IO.Save
|
||||||
{
|
{
|
||||||
path = PathTools.Normalize(path);
|
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)
|
public IFile OpenFile(string path, OpenMode mode)
|
||||||
{
|
{
|
||||||
path = PathTools.Normalize(path);
|
path = PathTools.Normalize(path);
|
||||||
|
|
||||||
if (!FileDictionary.TryGetValue(path, out SaveFileEntry file))
|
if (!FileTable.TryOpenFile(path, out SaveFileInfo file))
|
||||||
{
|
{
|
||||||
throw new FileNotFoundException();
|
throw new FileNotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file.BlockIndex < 0)
|
if (file.StartBlock < 0)
|
||||||
{
|
{
|
||||||
return new NullFile();
|
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)
|
public void RenameDirectory(string srcPath, string dstPath)
|
||||||
|
@ -125,22 +92,22 @@ namespace LibHac.IO.Save
|
||||||
{
|
{
|
||||||
path = PathTools.Normalize(path);
|
path = PathTools.Normalize(path);
|
||||||
|
|
||||||
return DirDictionary.ContainsKey(path);
|
return FileTable.TryOpenDirectory(path, out SaveFindPosition _);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool FileExists(string path)
|
public bool FileExists(string path)
|
||||||
{
|
{
|
||||||
path = PathTools.Normalize(path);
|
path = PathTools.Normalize(path);
|
||||||
|
|
||||||
return FileDictionary.ContainsKey(path);
|
return FileTable.TryOpenFile(path, out SaveFileInfo _);
|
||||||
}
|
}
|
||||||
|
|
||||||
public DirectoryEntryType GetEntryType(string path)
|
public DirectoryEntryType GetEntryType(string path)
|
||||||
{
|
{
|
||||||
path = PathTools.Normalize(path);
|
path = PathTools.Normalize(path);
|
||||||
|
|
||||||
if (DirDictionary.ContainsKey(path)) return DirectoryEntryType.Directory;
|
if (FileExists(path)) return DirectoryEntryType.File;
|
||||||
if (FileDictionary.ContainsKey(path)) return DirectoryEntryType.File;
|
if (DirectoryExists(path)) return DirectoryEntryType.Directory;
|
||||||
|
|
||||||
throw new FileNotFoundException(path);
|
throw new FileNotFoundException(path);
|
||||||
}
|
}
|
||||||
|
@ -152,91 +119,6 @@ namespace LibHac.IO.Save
|
||||||
|
|
||||||
public IStorage GetBaseStorage() => BaseStorage.AsReadOnly();
|
public IStorage GetBaseStorage() => BaseStorage.AsReadOnly();
|
||||||
public IStorage GetHeaderStorage() => HeaderStorage.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)
|
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.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
|
@ -64,6 +65,71 @@ namespace LibHac
|
||||||
return true;
|
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 byte[] array) => ((ReadOnlySpan<byte>)array).IsEmpty();
|
||||||
|
|
||||||
public static bool IsEmpty(this ReadOnlySpan<byte> span)
|
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;
|
long start = reader.BaseStream.Position;
|
||||||
int size = 0;
|
int size = 0;
|
||||||
|
@ -145,7 +211,7 @@ namespace LibHac
|
||||||
return text;
|
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;
|
long start = reader.BaseStream.Position;
|
||||||
int size = 0;
|
int size = 0;
|
||||||
|
|
|
@ -163,11 +163,10 @@ namespace hactoolnet
|
||||||
|
|
||||||
foreach (DirectoryEntry entry in save.EnumerateEntries().Where(x => x.Type == DirectoryEntryType.File))
|
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(fileInfo.StartBlock);
|
||||||
|
|
||||||
IEnumerable<(int block, int length)> chain = save.SaveDataFileSystemCore.AllocationTable.DumpChain(saveEntry.BlockIndex);
|
|
||||||
|
|
||||||
sb.AppendLine(entry.FullPath);
|
sb.AppendLine(entry.FullPath);
|
||||||
sb.AppendLine(PrintBlockChain(chain));
|
sb.AppendLine(PrintBlockChain(chain));
|
||||||
|
|
Loading…
Reference in a new issue