diff --git a/src/LibHac/Fs/SaveDataStructs.cs b/src/LibHac/Fs/SaveDataStructs.cs index 581dc4cd..62fb2856 100644 --- a/src/LibHac/Fs/SaveDataStructs.cs +++ b/src/LibHac/Fs/SaveDataStructs.cs @@ -7,7 +7,7 @@ using LibHac.Ncm; namespace LibHac.Fs { [StructLayout(LayoutKind.Explicit, Size = 0x40)] - public struct SaveDataAttribute + public struct SaveDataAttribute : IEquatable, IComparable { [FieldOffset(0x00)] public ulong TitleId; [FieldOffset(0x08)] public UserId UserId; @@ -15,6 +15,50 @@ namespace LibHac.Fs [FieldOffset(0x20)] public SaveDataType Type; [FieldOffset(0x21)] public byte Rank; [FieldOffset(0x22)] public short Index; + + public override bool Equals(object obj) + { + return obj is SaveDataAttribute attribute && Equals(attribute); + } + + public bool Equals(SaveDataAttribute other) + { + return TitleId == other.TitleId && + Type == other.Type && + UserId.Equals(other.UserId) && + SaveDataId == other.SaveDataId && + Rank == other.Rank && + Index == other.Index; + } + + public override int GetHashCode() + { + // ReSharper disable NonReadonlyMemberInGetHashCode + int hashCode = 487790375; + hashCode = hashCode * -1521134295 + TitleId.GetHashCode(); + hashCode = hashCode * -1521134295 + Type.GetHashCode(); + hashCode = hashCode * -1521134295 + UserId.GetHashCode(); + hashCode = hashCode * -1521134295 + SaveDataId.GetHashCode(); + hashCode = hashCode * -1521134295 + Rank.GetHashCode(); + hashCode = hashCode * -1521134295 + Index.GetHashCode(); + return hashCode; + // ReSharper restore NonReadonlyMemberInGetHashCode + } + + public int CompareTo(SaveDataAttribute other) + { + int titleIdComparison = TitleId.CompareTo(other.TitleId); + if (titleIdComparison != 0) return titleIdComparison; + int typeComparison = Type.CompareTo(other.Type); + if (typeComparison != 0) return typeComparison; + int userIdComparison = UserId.CompareTo(other.UserId); + if (userIdComparison != 0) return userIdComparison; + int saveDataIdComparison = SaveDataId.CompareTo(other.SaveDataId); + if (saveDataIdComparison != 0) return saveDataIdComparison; + int rankComparison = Rank.CompareTo(other.Rank); + if (rankComparison != 0) return rankComparison; + return Index.CompareTo(other.Index); + } } [StructLayout(LayoutKind.Explicit, Size = 0x48)] diff --git a/src/LibHac/FsService/SaveDataIndexerEntry.cs b/src/LibHac/FsService/SaveDataIndexerEntry.cs new file mode 100644 index 00000000..80283776 --- /dev/null +++ b/src/LibHac/FsService/SaveDataIndexerEntry.cs @@ -0,0 +1,15 @@ +using System.Runtime.InteropServices; +using LibHac.Fs; + +namespace LibHac.FsService +{ + [StructLayout(LayoutKind.Explicit, Size = 0x40)] + public struct SaveDataIndexerEntry + { + [FieldOffset(0x00)] public ulong SaveDataId; + [FieldOffset(0x08)] public ulong Size; + [FieldOffset(0x10)] public ulong Field10; + [FieldOffset(0x18)] public SaveDataSpaceId SpaceId; + [FieldOffset(0x19)] public byte State; + } +} diff --git a/src/LibHac/FsService/SaveIndexerStruct.cs b/src/LibHac/FsService/SaveIndexerStruct.cs deleted file mode 100644 index e78ff3a6..00000000 --- a/src/LibHac/FsService/SaveIndexerStruct.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Buffers.Binary; -using LibHac.Kvdb; - -namespace LibHac.FsService -{ - public class SaveIndexerStruct : IExportable - { - public ulong SaveId { get; private set; } - public ulong Size { get; private set; } - public byte SpaceId { get; private set; } - public byte Field19 { get; private set; } - - public int ExportSize => 0x40; - private bool _isFrozen; - - public void ToBytes(Span output) - { - if(output.Length < ExportSize) throw new InvalidOperationException("Output buffer is too small."); - - BinaryPrimitives.WriteUInt64LittleEndian(output, SaveId); - BinaryPrimitives.WriteUInt64LittleEndian(output.Slice(8), Size); - output[0x18] = SpaceId; - output[0x19] = Field19; - } - - public void FromBytes(ReadOnlySpan input) - { - if(_isFrozen) throw new InvalidOperationException("Unable to modify frozen object."); - if (input.Length < ExportSize) throw new InvalidOperationException("Input data is too short."); - - SaveId = BinaryPrimitives.ReadUInt64LittleEndian(input); - Size = BinaryPrimitives.ReadUInt64LittleEndian(input.Slice(8)); - SpaceId = input[0x18]; - Field19 = input[0x19]; - } - - public void Freeze() => _isFrozen = true; - } -} diff --git a/src/LibHac/Kvdb/GenericValue.cs b/src/LibHac/Kvdb/GenericValue.cs deleted file mode 100644 index 1d599384..00000000 --- a/src/LibHac/Kvdb/GenericValue.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; - -namespace LibHac.Kvdb -{ - /// - /// A class for handling any value used by - /// - public class GenericValue : IExportable - { - private bool _isFrozen; - private byte[] _value; - - public int ExportSize => _value?.Length ?? 0; - - public void ToBytes(Span output) - { - if (output.Length < ExportSize) throw new InvalidOperationException("Output buffer is too small."); - - _value.CopyTo(output); - } - - public void FromBytes(ReadOnlySpan input) - { - if (_isFrozen) throw new InvalidOperationException("Unable to modify frozen object."); - - _value = input.ToArray(); - } - - public void Freeze() => _isFrozen = true; - } -} diff --git a/src/LibHac/Kvdb/ImkvdbReader.cs b/src/LibHac/Kvdb/ImkvdbReader.cs index e0d96df1..1956affb 100644 --- a/src/LibHac/Kvdb/ImkvdbReader.cs +++ b/src/LibHac/Kvdb/ImkvdbReader.cs @@ -5,7 +5,7 @@ namespace LibHac.Kvdb { public ref struct ImkvdbReader { - private ReadOnlySpan _data; + private readonly ReadOnlySpan _data; private int _position; public ImkvdbReader(ReadOnlySpan data) @@ -18,13 +18,14 @@ namespace LibHac.Kvdb { entryCount = default; - if (_position + Unsafe.SizeOf() > _data.Length) return ResultKvdb.InvalidKeyValue; + if (_position + Unsafe.SizeOf() > _data.Length) + return ResultKvdb.InvalidKeyValue.Log(); ref ImkvdbHeader header = ref Unsafe.As(ref Unsafe.AsRef(_data[_position])); if (header.Magic != ImkvdbHeader.ExpectedMagic) { - return ResultKvdb.InvalidKeyValue; + return ResultKvdb.InvalidKeyValue.Log(); } entryCount = header.EntryCount; @@ -38,13 +39,14 @@ namespace LibHac.Kvdb keySize = default; valueSize = default; - if (_position + Unsafe.SizeOf() > _data.Length) return ResultKvdb.InvalidKeyValue; + if (_position + Unsafe.SizeOf() > _data.Length) + return ResultKvdb.InvalidKeyValue.Log(); ref ImkvdbEntryHeader header = ref Unsafe.As(ref Unsafe.AsRef(_data[_position])); if (header.Magic != ImkvdbEntryHeader.ExpectedMagic) { - return ResultKvdb.InvalidKeyValue; + return ResultKvdb.InvalidKeyValue.Log(); } keySize = header.KeySize; @@ -63,7 +65,8 @@ namespace LibHac.Kvdb _position += Unsafe.SizeOf(); - if (_position + keySize + valueSize > _data.Length) return ResultKvdb.InvalidKeyValue; + if (_position + keySize + valueSize > _data.Length) + return ResultKvdb.InvalidKeyValue.Log(); key = _data.Slice(_position, keySize); value = _data.Slice(_position + keySize, valueSize); diff --git a/src/LibHac/Kvdb/ImkvdbWriter.cs b/src/LibHac/Kvdb/ImkvdbWriter.cs index fd060662..5e5815fb 100644 --- a/src/LibHac/Kvdb/ImkvdbWriter.cs +++ b/src/LibHac/Kvdb/ImkvdbWriter.cs @@ -5,7 +5,7 @@ namespace LibHac.Kvdb { public ref struct ImkvdbWriter { - private Span _data; + private readonly Span _data; private int _position; public ImkvdbWriter(Span data) @@ -27,9 +27,9 @@ namespace LibHac.Kvdb _position += Unsafe.SizeOf(); } - public void WriteEntry(IExportable key, IExportable value) + public void WriteEntry(ReadOnlySpan key, ReadOnlySpan value) { - WriteEntryHeader(key.ExportSize, value.ExportSize); + WriteEntryHeader(key.Length, value.Length); Write(key); Write(value); } @@ -47,13 +47,13 @@ namespace LibHac.Kvdb _position += Unsafe.SizeOf(); } - private void Write(IExportable value) + private void Write(ReadOnlySpan value) { - int valueSize = value.ExportSize; + int valueSize = value.Length; if (_position + valueSize > _data.Length) throw new InvalidOperationException(); Span dest = _data.Slice(_position, valueSize); - value.ToBytes(dest); + value.CopyTo(dest); _position += valueSize; } diff --git a/src/LibHac/Kvdb/KeyValueDatabase.cs b/src/LibHac/Kvdb/KeyValueDatabase.cs index e2043074..1c2d07a5 100644 --- a/src/LibHac/Kvdb/KeyValueDatabase.cs +++ b/src/LibHac/Kvdb/KeyValueDatabase.cs @@ -1,33 +1,51 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Runtime.CompilerServices; +using LibHac.Common; +using LibHac.Fs; namespace LibHac.Kvdb { - // Todo: Save and load from file - public class KeyValueDatabase - where TKey : IComparable, IEquatable, IExportable, new() - where TValue : IExportable, new() + public class KeyValueDatabase where TKey : unmanaged, IComparable, IEquatable { - private Dictionary KvDict { get; } = new Dictionary(); + public Dictionary KvDict { get; } = new Dictionary(); - public int Count => KvDict.Count; + private FileSystemClient FsClient { get; } + private string FileName { get; } - public Result Get(TKey key, out TValue value) + public KeyValueDatabase() { } + + public KeyValueDatabase(FileSystemClient fsClient, string fileName) + { + FsClient = fsClient; + FileName = fileName; + } + + public Result Get(ref TKey key, Span valueBuffer) + { + Result rc = GetValue(ref key, out byte[] value); + if (rc.IsFailure()) return rc; + + int size = Math.Min(valueBuffer.Length, value.Length); + + value.AsSpan(0, size).CopyTo(valueBuffer); + return Result.Success; + } + + public Result GetValue(ref TKey key, out byte[] value) { if (!KvDict.TryGetValue(key, out value)) { - return ResultKvdb.KeyNotFound; + return ResultKvdb.KeyNotFound.Log(); } return Result.Success; } - public Result Set(TKey key, TValue value) + public Result Set(ref TKey key, byte[] value) { - key.Freeze(); - KvDict[key] = value; return Result.Success; @@ -35,6 +53,8 @@ namespace LibHac.Kvdb public Result ReadDatabaseFromBuffer(ReadOnlySpan data) { + KvDict.Clear(); + var reader = new ImkvdbReader(data); Result rc = reader.ReadHeader(out int entryCount); @@ -45,13 +65,12 @@ namespace LibHac.Kvdb rc = reader.ReadEntry(out ReadOnlySpan keyBytes, out ReadOnlySpan valueBytes); if (rc.IsFailure()) return rc; + Debug.Assert(keyBytes.Length == Unsafe.SizeOf()); + var key = new TKey(); - var value = new TValue(); + keyBytes.CopyTo(SpanHelpers.AsByteSpan(ref key)); - key.FromBytes(keyBytes); - value.FromBytes(valueBytes); - - key.Freeze(); + byte[] value = valueBytes.ToArray(); KvDict.Add(key, value); } @@ -65,26 +84,97 @@ namespace LibHac.Kvdb writer.WriteHeader(KvDict.Count); - foreach (KeyValuePair entry in KvDict.OrderBy(x => x.Key)) + foreach (KeyValuePair entry in KvDict.OrderBy(x => x.Key)) { - writer.WriteEntry(entry.Key, entry.Value); + TKey key = entry.Key; + writer.WriteEntry(SpanHelpers.AsByteSpan(ref key), entry.Value); } return Result.Success; } + public Result ReadDatabaseFromFile() + { + if (FsClient == null || FileName == null) + return ResultFs.PreconditionViolation.Log(); + + Result rc = ReadFile(out byte[] data); + + if (rc.IsFailure()) + { + return rc == ResultFs.PathNotFound ? Result.Success : rc; + } + + return ReadDatabaseFromBuffer(data); + } + + public Result WriteDatabaseToFile() + { + if (FsClient == null || FileName == null) + return ResultFs.PreconditionViolation.Log(); + + var buffer = new byte[GetExportedSize()]; + + Result rc = WriteDatabaseToBuffer(buffer); + if (rc.IsFailure()) return rc; + + return WriteFile(buffer); + } + public int GetExportedSize() { int size = Unsafe.SizeOf(); - foreach (KeyValuePair entry in KvDict) + foreach (byte[] value in KvDict.Values) { size += Unsafe.SizeOf(); - size += entry.Key.ExportSize; - size += entry.Value.ExportSize; + size += Unsafe.SizeOf(); + size += value.Length; } return size; } + + private Result ReadFile(out byte[] data) + { + Debug.Assert(FsClient != null); + Debug.Assert(!string.IsNullOrWhiteSpace(FileName)); + + data = default; + + Result rc = FsClient.OpenFile(out FileHandle handle, FileName, OpenMode.Read); + if (rc.IsFailure()) return rc; + + rc = FsClient.GetFileSize(out long fileSize, handle); + + if (rc.IsSuccess()) + { + data = new byte[fileSize]; + + rc = FsClient.ReadFile(handle, 0, data); + } + + FsClient.CloseFile(handle); + return rc; + } + + private Result WriteFile(ReadOnlySpan data) + { + Debug.Assert(FsClient != null); + Debug.Assert(!string.IsNullOrWhiteSpace(FileName)); + + FsClient.DeleteFile(FileName); + + Result rc = FsClient.CreateFile(FileName, data.Length); + if (rc.IsFailure()) return rc; + + rc = FsClient.OpenFile(out FileHandle handle, FileName, OpenMode.Write); + if (rc.IsFailure()) return rc; + + rc = FsClient.WriteFile(handle, 0, data, WriteOption.Flush); + FsClient.CloseFile(handle); + + return rc; + } } }