Add direct read support for the savedata file table

This commit is contained in:
Alex Barney 2019-03-11 23:45:05 -05:00
parent 1110c32cb1
commit f0c09d7712
8 changed files with 457 additions and 202 deletions

View file

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

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

View file

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

View file

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

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

View 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 { }
}
}

View file

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

View file

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