mirror of
https://github.com/Thealexbarney/LibHac.git
synced 2024-11-14 10:49:41 +01:00
Update IResultConvertFile and use the new save cache manager
This commit is contained in:
parent
7dfcebfc28
commit
e24ee1b956
15 changed files with 352 additions and 628 deletions
|
@ -672,6 +672,7 @@ Module,DescriptionStart,DescriptionEnd,Flags,Namespace,Name,Summary
|
|||
2,4301,4499,,,SaveDataCorrupted,
|
||||
2,4302,,,,UnsupportedSaveDataVersion,
|
||||
2,4303,,,,InvalidSaveDataEntryType,
|
||||
2,4304,,,,ReconstructibleSaveDataCorrupted,
|
||||
|
||||
2,4311,4319,,,SaveDataFileSystemCorrupted,
|
||||
2,4312,,,,InvalidJournalIntegritySaveDataHashSize,
|
||||
|
|
|
|
@ -170,6 +170,12 @@ public enum SaveDataRank : byte
|
|||
Secondary = 1
|
||||
}
|
||||
|
||||
public enum SaveDataFormatType : byte
|
||||
{
|
||||
Normal = 0,
|
||||
NoJournal = 1
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum SaveDataFlags
|
||||
{
|
||||
|
|
|
@ -1210,6 +1210,8 @@ public static class ResultFs
|
|||
public static Result.Base UnsupportedSaveDataVersion => new Result.Base(ModuleFs, 4302);
|
||||
/// <summary>Error code: 2002-4303; Inner value: 0x219e02</summary>
|
||||
public static Result.Base InvalidSaveDataEntryType => new Result.Base(ModuleFs, 4303);
|
||||
/// <summary>Error code: 2002-4304; Inner value: 0x21a002</summary>
|
||||
public static Result.Base ReconstructibleSaveDataCorrupted => new Result.Base(ModuleFs, 4304);
|
||||
|
||||
/// <summary>Error code: 2002-4311; Range: 4311-4319; Inner value: 0x21ae02</summary>
|
||||
public static Result.Base SaveDataFileSystemCorrupted { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new Result.Base(ModuleFs, 4311, 4319); }
|
||||
|
|
|
@ -10,12 +10,10 @@ public interface ISaveDataFileSystemCreator
|
|||
{
|
||||
Result CreateFile(out IFile file, IFileSystem sourceFileSystem, ulong saveDataId, OpenMode openMode);
|
||||
|
||||
Result Create(ref SharedRef<IFileSystem> outFileSystem,
|
||||
ref SharedRef<ISaveDataExtraDataAccessor> outExtraDataAccessor,
|
||||
ISaveDataFileSystemCacheManager cacheManager, ref SharedRef<IFileSystem> baseFileSystem,
|
||||
SaveDataSpaceId spaceId, ulong saveDataId, bool allowDirectorySaveData, bool useDeviceUniqueMac,
|
||||
Result Create(ref SharedRef<ISaveDataFileSystem> outFileSystem, ref SharedRef<IFileSystem> baseFileSystem,
|
||||
SaveDataSpaceId spaceId, ulong saveDataId, bool allowDirectorySaveData, bool isDeviceUniqueMac,
|
||||
bool isJournalingSupported, bool isMultiCommitSupported, bool openReadOnly, bool openShared,
|
||||
ISaveDataCommitTimeStampGetter timeStampGetter);
|
||||
ISaveDataCommitTimeStampGetter timeStampGetter, bool isReconstructible);
|
||||
|
||||
Result CreateExtraDataAccessor(ref SharedRef<ISaveDataExtraDataAccessor> outExtraDataAccessor,
|
||||
ref SharedRef<IFileSystem> baseFileSystem);
|
||||
|
|
|
@ -3,12 +3,9 @@ using System.Runtime.CompilerServices;
|
|||
using LibHac.Common;
|
||||
using LibHac.Common.FixedArrays;
|
||||
using LibHac.Common.Keys;
|
||||
using LibHac.Diag;
|
||||
using LibHac.Fs;
|
||||
using LibHac.Fs.Fsa;
|
||||
using LibHac.FsSystem;
|
||||
using LibHac.Tools.FsSystem;
|
||||
using LibHac.Tools.FsSystem.Save;
|
||||
using LibHac.Util;
|
||||
|
||||
using OpenType = LibHac.FsSrv.SaveDataOpenTypeSetFileStorage.OpenType;
|
||||
|
@ -17,10 +14,15 @@ namespace LibHac.FsSrv.FsCreator;
|
|||
|
||||
public class SaveDataFileSystemCreator : ISaveDataFileSystemCreator
|
||||
{
|
||||
// Option to disable some restrictions enforced in actual FS.
|
||||
private static readonly bool EnforceSaveTypeRestrictions = false;
|
||||
|
||||
// ReSharper disable once NotAccessedField.Local
|
||||
private IBufferManager _bufferManager;
|
||||
private RandomDataGenerator _randomGenerator;
|
||||
|
||||
// LibHac Additions
|
||||
// ReSharper disable once NotAccessedField.Local
|
||||
private KeySet _keySet;
|
||||
private FileSystemServer _fsServer;
|
||||
|
||||
|
@ -38,54 +40,56 @@ public class SaveDataFileSystemCreator : ISaveDataFileSystemCreator
|
|||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Result Create(ref SharedRef<IFileSystem> outFileSystem,
|
||||
ref SharedRef<ISaveDataExtraDataAccessor> outExtraDataAccessor,
|
||||
ISaveDataFileSystemCacheManager cacheManager, ref SharedRef<IFileSystem> baseFileSystem,
|
||||
SaveDataSpaceId spaceId, ulong saveDataId, bool allowDirectorySaveData, bool useDeviceUniqueMac,
|
||||
public Result Create(ref SharedRef<ISaveDataFileSystem> outFileSystem, ref SharedRef<IFileSystem> baseFileSystem,
|
||||
SaveDataSpaceId spaceId, ulong saveDataId, bool allowDirectorySaveData, bool isDeviceUniqueMac,
|
||||
bool isJournalingSupported, bool isMultiCommitSupported, bool openReadOnly, bool openShared,
|
||||
ISaveDataCommitTimeStampGetter timeStampGetter)
|
||||
ISaveDataCommitTimeStampGetter timeStampGetter, bool isReconstructible)
|
||||
{
|
||||
Unsafe.SkipInit(out Array18<byte> saveImageNameBuffer);
|
||||
|
||||
Assert.SdkRequiresNotNull(cacheManager);
|
||||
|
||||
using var saveImageName = new Path();
|
||||
Result rc = PathFunctions.SetUpFixedPathSaveId(ref saveImageName.Ref(), saveImageNameBuffer.Items, saveDataId);
|
||||
if (rc.IsFailure()) return rc;
|
||||
if (rc.IsFailure()) return rc.Miss();
|
||||
|
||||
rc = baseFileSystem.Get.GetEntryType(out DirectoryEntryType type, in saveImageName);
|
||||
|
||||
if (rc.IsFailure())
|
||||
{
|
||||
return ResultFs.PathNotFound.Includes(rc) ? ResultFs.TargetNotFound.LogConverted(rc) : rc;
|
||||
return ResultFs.PathNotFound.Includes(rc) ? ResultFs.TargetNotFound.LogConverted(rc) : rc.Miss();
|
||||
}
|
||||
|
||||
using var saveDataFs = new SharedRef<ISaveDataFileSystem>();
|
||||
|
||||
if (type == DirectoryEntryType.Directory)
|
||||
{
|
||||
if (EnforceSaveTypeRestrictions)
|
||||
{
|
||||
if (!allowDirectorySaveData)
|
||||
return ResultFs.InvalidSaveDataEntryType.Log();
|
||||
}
|
||||
|
||||
using var baseFs =
|
||||
new UniqueRef<SubdirectoryFileSystem>(new SubdirectoryFileSystem(ref baseFileSystem));
|
||||
// Get a file system over the save directory
|
||||
using var baseFs = new UniqueRef<SubdirectoryFileSystem>(new SubdirectoryFileSystem(ref baseFileSystem));
|
||||
|
||||
if (!baseFs.HasValue)
|
||||
return ResultFs.AllocationMemoryFailedInSaveDataFileSystemCreatorA.Log();
|
||||
|
||||
rc = baseFs.Get.Initialize(in saveImageName);
|
||||
if (rc.IsFailure()) return rc;
|
||||
if (rc.IsFailure()) return rc.Miss();
|
||||
|
||||
// Create and initialize the directory save data FS
|
||||
using UniqueRef<IFileSystem> tempFs = UniqueRef<IFileSystem>.Create(ref baseFs.Ref());
|
||||
using var saveDirFs = new SharedRef<DirectorySaveDataFileSystem>(
|
||||
new DirectorySaveDataFileSystem(ref tempFs.Ref(), _fsServer.Hos.Fs));
|
||||
|
||||
if (!saveDirFs.HasValue)
|
||||
return ResultFs.AllocationMemoryFailedInSaveDataFileSystemCreatorB.Log();
|
||||
|
||||
rc = saveDirFs.Get.Initialize(isJournalingSupported, isMultiCommitSupported, !openReadOnly,
|
||||
timeStampGetter, _randomGenerator);
|
||||
if (rc.IsFailure()) return rc;
|
||||
if (rc.IsFailure()) return rc.Miss();
|
||||
|
||||
outFileSystem.SetByCopy(in saveDirFs);
|
||||
outExtraDataAccessor.SetByCopy(in saveDirFs);
|
||||
|
||||
return Result.Success;
|
||||
saveDataFs.SetByMove(ref saveDirFs.Ref());
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -98,19 +102,17 @@ public class SaveDataFileSystemCreator : ISaveDataFileSystemCreator
|
|||
OpenMode.ReadWrite, openType);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
if (!isJournalingSupported)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
using var saveFs = new SharedRef<SaveDataFileSystem>(new SaveDataFileSystem(_keySet, fileStorage.Get,
|
||||
IntegrityCheckLevel.ErrorOnInvalid, false));
|
||||
// Wrap the save FS in a result convert FS and set it as the output FS
|
||||
using var resultConvertFs = new SharedRef<SaveDataResultConvertFileSystem>(
|
||||
new SaveDataResultConvertFileSystem(ref saveDataFs.Ref(), isReconstructible));
|
||||
|
||||
// Todo: ISaveDataExtraDataAccessor
|
||||
outFileSystem.SetByMove(ref resultConvertFs.Ref());
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
}
|
||||
|
||||
public Result CreateExtraDataAccessor(ref SharedRef<ISaveDataExtraDataAccessor> outExtraDataAccessor,
|
||||
ref SharedRef<IFileSystem> baseFileSystem)
|
||||
|
|
127
src/LibHac/FsSrv/FsCreator/SaveDataResultConvertFileSystem.cs
Normal file
127
src/LibHac/FsSrv/FsCreator/SaveDataResultConvertFileSystem.cs
Normal file
|
@ -0,0 +1,127 @@
|
|||
using LibHac.Common;
|
||||
using LibHac.Fs;
|
||||
using LibHac.Fs.Fsa;
|
||||
using LibHac.FsSystem;
|
||||
using static LibHac.FsSrv.FsCreator.SaveDataResultConverter;
|
||||
|
||||
namespace LibHac.FsSrv.FsCreator;
|
||||
|
||||
/// <summary>
|
||||
/// Wraps an <see cref="IFile"/>, converting its returned <see cref="Result"/>s
|
||||
/// to save-data-specific <see cref="Result"/>s.
|
||||
/// </summary>
|
||||
/// <remarks>Based on FS 14.1.0 (nnSdk 14.3.0)</remarks>
|
||||
public class SaveDataResultConvertFile : IResultConvertFile
|
||||
{
|
||||
private bool _isReconstructible;
|
||||
|
||||
public SaveDataResultConvertFile(ref UniqueRef<IFile> baseFile, bool isReconstructible) : base(ref baseFile)
|
||||
{
|
||||
_isReconstructible = isReconstructible;
|
||||
}
|
||||
|
||||
protected override Result ConvertResult(Result result)
|
||||
{
|
||||
return ConvertSaveDataFsResult(result, _isReconstructible).Ret();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wraps an <see cref="IDirectory"/>, converting its returned <see cref="Result"/>s
|
||||
/// to save-data-specific <see cref="Result"/>s.
|
||||
/// </summary>
|
||||
/// <remarks>Based on FS 14.1.0 (nnSdk 14.3.0)</remarks>
|
||||
public class SaveDataResultConvertDirectory : IResultConvertDirectory
|
||||
{
|
||||
private bool _isReconstructible;
|
||||
|
||||
public SaveDataResultConvertDirectory(ref UniqueRef<IDirectory> baseDirectory, bool isReconstructible) : base(
|
||||
ref baseDirectory)
|
||||
{
|
||||
_isReconstructible = isReconstructible;
|
||||
}
|
||||
|
||||
protected override Result ConvertResult(Result result)
|
||||
{
|
||||
return ConvertSaveDataFsResult(result, _isReconstructible).Ret();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wraps an <see cref="ISaveDataFileSystem"/>, converting its returned <see cref="Result"/>s
|
||||
/// to save-data-specific <see cref="Result"/>s.
|
||||
/// </summary>
|
||||
/// <remarks>Based on FS 14.1.0 (nnSdk 14.3.0)</remarks>
|
||||
public class SaveDataResultConvertFileSystem : IResultConvertFileSystem<ISaveDataFileSystem>
|
||||
{
|
||||
private bool _isReconstructible;
|
||||
|
||||
public SaveDataResultConvertFileSystem(ref SharedRef<ISaveDataFileSystem> baseFileSystem, bool isReconstructible) :
|
||||
base(ref baseFileSystem)
|
||||
{
|
||||
_isReconstructible = isReconstructible;
|
||||
}
|
||||
|
||||
public override Result WriteExtraData(in SaveDataExtraData extraData)
|
||||
{
|
||||
return ConvertSaveDataFsResult(GetFileSystem().WriteExtraData(in extraData), _isReconstructible).Ret();
|
||||
}
|
||||
|
||||
public override Result CommitExtraData(bool updateTimeStamp)
|
||||
{
|
||||
return ConvertSaveDataFsResult(GetFileSystem().CommitExtraData(updateTimeStamp), _isReconstructible).Ret();
|
||||
}
|
||||
|
||||
public override Result ReadExtraData(out SaveDataExtraData extraData)
|
||||
{
|
||||
return ConvertSaveDataFsResult(GetFileSystem().ReadExtraData(out extraData), _isReconstructible).Ret();
|
||||
}
|
||||
|
||||
public override Result RollbackOnlyModified()
|
||||
{
|
||||
return ConvertSaveDataFsResult(GetFileSystem().RollbackOnlyModified(), _isReconstructible).Ret();
|
||||
}
|
||||
|
||||
protected override Result DoOpenFile(ref UniqueRef<IFile> outFile, in Path path, OpenMode mode)
|
||||
{
|
||||
using var file = new UniqueRef<IFile>();
|
||||
Result rc = ConvertResult(GetFileSystem().OpenFile(ref file.Ref(), in path, mode));
|
||||
if (rc.IsFailure()) return rc.Miss();
|
||||
|
||||
using UniqueRef<SaveDataResultConvertFile> resultConvertFile =
|
||||
new(new SaveDataResultConvertFile(ref file.Ref(), _isReconstructible));
|
||||
|
||||
outFile.Set(ref resultConvertFile.Ref());
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
protected override Result DoOpenDirectory(ref UniqueRef<IDirectory> outDirectory, in Path path,
|
||||
OpenDirectoryMode mode)
|
||||
{
|
||||
using var directory = new UniqueRef<IDirectory>();
|
||||
Result rc = ConvertResult(GetFileSystem().OpenDirectory(ref directory.Ref(), in path, mode));
|
||||
if (rc.IsFailure()) return rc.Miss();
|
||||
|
||||
using UniqueRef<SaveDataResultConvertDirectory> resultConvertDirectory =
|
||||
new(new SaveDataResultConvertDirectory(ref directory.Ref(), _isReconstructible));
|
||||
|
||||
outDirectory.Set(ref resultConvertDirectory.Ref());
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
protected override Result ConvertResult(Result result)
|
||||
{
|
||||
return ConvertSaveDataFsResult(result, _isReconstructible).Ret();
|
||||
}
|
||||
|
||||
public override bool IsSaveDataFileSystemCacheEnabled()
|
||||
{
|
||||
return GetFileSystem().IsSaveDataFileSystemCacheEnabled();
|
||||
}
|
||||
|
||||
public override void RegisterExtraDataAccessorObserver(ISaveDataExtraDataAccessorObserver observer,
|
||||
SaveDataSpaceId spaceId, ulong saveDataId)
|
||||
{
|
||||
GetFileSystem().RegisterExtraDataAccessorObserver(observer, spaceId, saveDataId);
|
||||
}
|
||||
}
|
|
@ -1,15 +1,13 @@
|
|||
using LibHac.Common;
|
||||
using LibHac.Diag;
|
||||
using LibHac.Diag;
|
||||
using LibHac.Fs;
|
||||
using LibHac.Fs.Fsa;
|
||||
|
||||
namespace LibHac.FsSrv.Impl;
|
||||
namespace LibHac.FsSrv.FsCreator;
|
||||
|
||||
/// <summary>
|
||||
/// Contains functions for converting internal save data <see cref="Result"/>s to external <see cref="Result"/>s.
|
||||
/// </summary>
|
||||
/// <remarks>Based on FS 13.1.0 (nnSdk 13.4.0)</remarks>
|
||||
public static class SaveDataResultConvert
|
||||
/// <remarks>Based on FS 14.1.0 (nnSdk 14.3.0)</remarks>
|
||||
public static class SaveDataResultConverter
|
||||
{
|
||||
private static Result ConvertCorruptedResult(Result result)
|
||||
{
|
||||
|
@ -32,8 +30,7 @@ public static class SaveDataResultConvert
|
|||
|
||||
Assert.SdkAssert(false);
|
||||
}
|
||||
|
||||
if (ResultFs.HostFileSystemCorrupted.Includes(result))
|
||||
else if (ResultFs.HostFileSystemCorrupted.Includes(result))
|
||||
{
|
||||
if (ResultFs.HostEntryCorrupted.Includes(result))
|
||||
return ResultFs.SaveDataHostEntryCorrupted.LogConverted(result);
|
||||
|
@ -49,8 +46,7 @@ public static class SaveDataResultConvert
|
|||
|
||||
Assert.SdkAssert(false);
|
||||
}
|
||||
|
||||
if (ResultFs.DatabaseCorrupted.Includes(result))
|
||||
else if (ResultFs.DatabaseCorrupted.Includes(result))
|
||||
{
|
||||
if (ResultFs.InvalidAllocationTableBlock.Includes(result))
|
||||
return ResultFs.InvalidSaveDataAllocationTableBlock.LogConverted(result);
|
||||
|
@ -75,8 +71,7 @@ public static class SaveDataResultConvert
|
|||
|
||||
Assert.SdkAssert(false);
|
||||
}
|
||||
|
||||
if (ResultFs.ZeroBitmapFileCorrupted.Includes(result))
|
||||
else if (ResultFs.ZeroBitmapFileCorrupted.Includes(result))
|
||||
{
|
||||
if (ResultFs.IncompleteBlockInZeroBitmapHashStorageFile.Includes(result))
|
||||
return ResultFs.IncompleteBlockInZeroBitmapHashStorageFileSaveData.LogConverted(result);
|
||||
|
@ -87,11 +82,8 @@ public static class SaveDataResultConvert
|
|||
return result;
|
||||
}
|
||||
|
||||
public static Result ConvertSaveFsDriverPrivateResult(Result result)
|
||||
private static Result ConvertResult(Result result)
|
||||
{
|
||||
if (result.IsSuccess())
|
||||
return result;
|
||||
|
||||
if (ResultFs.UnsupportedVersion.Includes(result))
|
||||
return ResultFs.UnsupportedSaveDataVersion.LogConverted(result);
|
||||
|
||||
|
@ -101,7 +93,7 @@ public static class SaveDataResultConvert
|
|||
ResultFs.DatabaseCorrupted.Includes(result) ||
|
||||
ResultFs.ZeroBitmapFileCorrupted.Includes(result))
|
||||
{
|
||||
return ConvertCorruptedResult(result);
|
||||
return ConvertCorruptedResult(result).Miss();
|
||||
}
|
||||
|
||||
if (ResultFs.FatFileSystemCorrupted.Includes(result))
|
||||
|
@ -116,9 +108,6 @@ public static class SaveDataResultConvert
|
|||
if (ResultFs.AlreadyExists.Includes(result))
|
||||
return ResultFs.PathAlreadyExists.LogConverted(result);
|
||||
|
||||
if (ResultFs.InvalidOffset.Includes(result))
|
||||
return ResultFs.OutOfRange.LogConverted(result);
|
||||
|
||||
if (ResultFs.IncompatiblePath.Includes(result) ||
|
||||
ResultFs.FileNotFound.Includes(result))
|
||||
{
|
||||
|
@ -127,77 +116,19 @@ public static class SaveDataResultConvert
|
|||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wraps an <see cref="IFile"/>, converting its returned <see cref="Result"/>s
|
||||
/// to save-data-specific <see cref="Result"/>s.
|
||||
/// </summary>
|
||||
/// <remarks>Based on FS 13.1.0 (nnSdk 13.4.0)</remarks>
|
||||
public class SaveDataResultConvertFile : IResultConvertFile
|
||||
public static Result ConvertSaveDataFsResult(Result result, bool isReconstructible)
|
||||
{
|
||||
public SaveDataResultConvertFile(ref UniqueRef<IFile> baseFile) : base(ref baseFile)
|
||||
{
|
||||
}
|
||||
|
||||
protected override Result ConvertResult(Result result)
|
||||
{
|
||||
return SaveDataResultConvert.ConvertSaveFsDriverPrivateResult(result);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wraps an <see cref="IDirectory"/>, converting its returned <see cref="Result"/>s
|
||||
/// to save-data-specific <see cref="Result"/>s.
|
||||
/// </summary>
|
||||
/// <remarks>Based on FS 13.1.0 (nnSdk 13.4.0)</remarks>
|
||||
public class SaveDataResultConvertDirectory : IResultConvertDirectory
|
||||
{
|
||||
public SaveDataResultConvertDirectory(ref UniqueRef<IDirectory> baseDirectory) : base(ref baseDirectory)
|
||||
{
|
||||
}
|
||||
|
||||
protected override Result ConvertResult(Result result)
|
||||
{
|
||||
return SaveDataResultConvert.ConvertSaveFsDriverPrivateResult(result);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wraps an <see cref="IFileSystem"/>, converting its returned <see cref="Result"/>s
|
||||
/// to save-data-specific <see cref="Result"/>s.
|
||||
/// </summary>
|
||||
/// <remarks>Based on FS 13.1.0 (nnSdk 13.4.0)</remarks>
|
||||
public class SaveDataResultConvertFileSystem : IResultConvertFileSystem
|
||||
{
|
||||
public SaveDataResultConvertFileSystem(ref SharedRef<IFileSystem> baseFileSystem)
|
||||
: base(ref baseFileSystem)
|
||||
{
|
||||
}
|
||||
|
||||
protected override Result DoOpenFile(ref UniqueRef<IFile> outFile, in Path path, OpenMode mode)
|
||||
{
|
||||
using var file = new UniqueRef<IFile>();
|
||||
Result rc = ConvertResult(BaseFileSystem.Get.OpenFile(ref file.Ref(), path, mode));
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
outFile.Reset(new SaveDataResultConvertFile(ref file.Ref()));
|
||||
if (result.IsSuccess())
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
protected override Result DoOpenDirectory(ref UniqueRef<IDirectory> outDirectory, in Path path,
|
||||
OpenDirectoryMode mode)
|
||||
Result convertedResult = ConvertResult(result);
|
||||
|
||||
if (isReconstructible && ResultFs.SaveDataCorrupted.Includes(convertedResult))
|
||||
{
|
||||
using var directory = new UniqueRef<IDirectory>();
|
||||
Result rc = ConvertResult(BaseFileSystem.Get.OpenDirectory(ref directory.Ref(), path, mode));
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
outDirectory.Reset(new SaveDataResultConvertDirectory(ref directory.Ref()));
|
||||
return Result.Success;
|
||||
return ResultFs.ReconstructibleSaveDataCorrupted.LogConverted(convertedResult);
|
||||
}
|
||||
|
||||
protected override Result ConvertResult(Result result)
|
||||
{
|
||||
return SaveDataResultConvert.ConvertSaveFsDriverPrivateResult(result);
|
||||
return convertedResult;
|
||||
}
|
||||
}
|
|
@ -7,7 +7,7 @@ namespace LibHac.FsSrv.Impl;
|
|||
|
||||
/// <summary>
|
||||
/// Wraps an <see cref="ISaveDataFileSystem"/>.
|
||||
/// Upon disposal the base file system is returned to the provided <see cref="ISaveDataFileSystemCacheManager"/>.
|
||||
/// Upon disposal the base file system is returned to the provided <see cref="SaveDataFileSystemCacheManager"/>.
|
||||
/// </summary>
|
||||
/// <remarks>Based on FS 14.1.0 (nnSdk 14.3.0)</remarks>
|
||||
public class SaveDataFileSystemCacheRegister : IFileSystem
|
||||
|
|
|
@ -8,6 +8,20 @@ public static class SaveDataProperties
|
|||
public const long DefaultSaveDataBlockSize = 0x4000;
|
||||
public const long BcatSaveDataJournalSize = 0x200000;
|
||||
|
||||
public static bool IsJournalingSupported(SaveDataFormatType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case SaveDataFormatType.Normal:
|
||||
return true;
|
||||
case SaveDataFormatType.NoJournal:
|
||||
return false;
|
||||
default:
|
||||
Abort.UnexpectedDefault();
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsJournalingSupported(SaveDataType type)
|
||||
{
|
||||
switch (type)
|
||||
|
@ -67,7 +81,6 @@ public static class SaveDataProperties
|
|||
switch (type)
|
||||
{
|
||||
case SaveDataType.System:
|
||||
case SaveDataType.SystemBcat:
|
||||
return true;
|
||||
case SaveDataType.Account:
|
||||
case SaveDataType.Bcat:
|
||||
|
@ -86,7 +99,6 @@ public static class SaveDataProperties
|
|||
switch (type)
|
||||
{
|
||||
case SaveDataType.System:
|
||||
case SaveDataType.SystemBcat:
|
||||
return true;
|
||||
case SaveDataType.Account:
|
||||
case SaveDataType.Bcat:
|
||||
|
@ -99,4 +111,36 @@ public static class SaveDataProperties
|
|||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsReconstructible(SaveDataType type, SaveDataSpaceId spaceId)
|
||||
{
|
||||
switch (spaceId)
|
||||
{
|
||||
case SaveDataSpaceId.System:
|
||||
case SaveDataSpaceId.User:
|
||||
case SaveDataSpaceId.ProperSystem:
|
||||
case SaveDataSpaceId.SafeMode:
|
||||
switch (type)
|
||||
{
|
||||
case SaveDataType.System:
|
||||
case SaveDataType.Account:
|
||||
case SaveDataType.Device:
|
||||
return false;
|
||||
case SaveDataType.Bcat:
|
||||
case SaveDataType.Temporary:
|
||||
case SaveDataType.Cache:
|
||||
return true;
|
||||
default:
|
||||
Abort.UnexpectedDefault();
|
||||
return default;
|
||||
}
|
||||
case SaveDataSpaceId.SdSystem:
|
||||
case SaveDataSpaceId.Temporary:
|
||||
case SaveDataSpaceId.SdUser:
|
||||
return true;
|
||||
default:
|
||||
Abort.UnexpectedDefault();
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -20,7 +20,7 @@ public class SaveDataFileSystemServiceImpl
|
|||
private Configuration _config;
|
||||
private EncryptionSeed _encryptionSeed;
|
||||
|
||||
private FsSystem.SaveDataFileSystemCacheManager _saveDataFsCacheManager;
|
||||
private SaveDataFileSystemCacheManager _saveDataFsCacheManager;
|
||||
private SaveDataExtraDataAccessorCacheManager _extraDataCacheManager;
|
||||
// Save data porter manager
|
||||
private bool _isSdCardAccessible;
|
||||
|
@ -47,7 +47,7 @@ public class SaveDataFileSystemServiceImpl
|
|||
public SaveDataFileSystemServiceImpl(in Configuration configuration)
|
||||
{
|
||||
_config = configuration;
|
||||
_saveDataFsCacheManager = new FsSystem.SaveDataFileSystemCacheManager();
|
||||
_saveDataFsCacheManager = new SaveDataFileSystemCacheManager();
|
||||
_extraDataCacheManager = new SaveDataExtraDataAccessorCacheManager();
|
||||
|
||||
_timeStampGetter = new TimeStampGetter(this);
|
||||
|
@ -116,6 +116,7 @@ public class SaveDataFileSystemServiceImpl
|
|||
}
|
||||
}
|
||||
|
||||
// 14.3.0
|
||||
public Result OpenSaveDataFileSystem(ref SharedRef<IFileSystem> outFileSystem, SaveDataSpaceId spaceId,
|
||||
ulong saveDataId, in Path saveDataRootPath, bool openReadOnly, SaveDataType type, bool cacheExtraData)
|
||||
{
|
||||
|
@ -124,66 +125,65 @@ public class SaveDataFileSystemServiceImpl
|
|||
Result rc = OpenSaveDataDirectoryFileSystem(ref fileSystem.Ref(), spaceId, in saveDataRootPath, true);
|
||||
if (rc.IsFailure()) return rc.Miss();
|
||||
|
||||
bool allowDirectorySaveData = IsAllowedDirectorySaveData2(spaceId, in saveDataRootPath);
|
||||
bool isEmulatedOnHost = IsAllowedDirectorySaveData(spaceId, in saveDataRootPath);
|
||||
|
||||
// Note: When directory save data is allowed, Nintendo creates the save directory if it doesn't exist.
|
||||
// This bypasses normal save data creation, leaving the save with empty extra data.
|
||||
// Instead, we return that the save doesn't exist if the directory is missing.
|
||||
|
||||
using var saveDataFs = new SharedRef<IFileSystem>();
|
||||
using var cachedFs = new SharedRef<SaveDataFileSystemHolder>();
|
||||
|
||||
// Note: Nintendo doesn't cache directory save data
|
||||
// if (!allowDirectorySaveData)
|
||||
if (isEmulatedOnHost)
|
||||
{
|
||||
// Check if we have the requested file system cached
|
||||
if (_saveDataFsCacheManager.GetCache(ref cachedFs.Ref(), spaceId, saveDataId))
|
||||
{
|
||||
using var registerBase = new SharedRef<IFileSystem>(
|
||||
new SaveDataFileSystemCacheRegisterBase<SaveDataFileSystemHolder>(ref cachedFs.Ref(),
|
||||
_saveDataFsCacheManager));
|
||||
|
||||
using var resultConvertFs = new SharedRef<SaveDataResultConvertFileSystem>(
|
||||
new SaveDataResultConvertFileSystem(ref registerBase.Ref()));
|
||||
|
||||
saveDataFs.SetByMove(ref resultConvertFs.Ref());
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new file system if it's not in the cache
|
||||
if (!saveDataFs.HasValue)
|
||||
{
|
||||
using UniqueLockRef<SdkRecursiveMutexType> scopedLock = _extraDataCacheManager.GetScopedLock();
|
||||
using var extraDataAccessor = new SharedRef<ISaveDataExtraDataAccessor>();
|
||||
|
||||
bool openShared = SaveDataProperties.IsSharedOpenNeeded(type);
|
||||
bool isMultiCommitSupported = SaveDataProperties.IsMultiCommitSupported(type);
|
||||
bool isJournalingSupported = SaveDataProperties.IsJournalingSupported(type);
|
||||
bool useDeviceUniqueMac = IsDeviceUniqueMac(spaceId);
|
||||
|
||||
rc = _config.SaveFsCreator.Create(ref saveDataFs.Ref(), ref extraDataAccessor.Ref(),
|
||||
_saveDataFsCacheManager, ref fileSystem.Ref(), spaceId, saveDataId, allowDirectorySaveData,
|
||||
useDeviceUniqueMac, isJournalingSupported, isMultiCommitSupported, openReadOnly, openShared,
|
||||
_timeStampGetter);
|
||||
// Create the save data directory on the host if needed.
|
||||
Unsafe.SkipInit(out Array18<byte> saveDirectoryNameBuffer);
|
||||
using var saveDirectoryName = new Path();
|
||||
rc = PathFunctions.SetUpFixedPathSaveId(ref saveDirectoryName.Ref(), saveDirectoryNameBuffer.Items, saveDataId);
|
||||
if (rc.IsFailure()) return rc.Miss();
|
||||
|
||||
// Cache the extra data accessor if needed
|
||||
if (cacheExtraData && extraDataAccessor.HasValue)
|
||||
rc = FsSystem.Utility.EnsureDirectory(fileSystem.Get, in saveDirectoryName);
|
||||
if (rc.IsFailure()) return rc.Miss();
|
||||
}
|
||||
|
||||
using var saveDataFs = new SharedRef<ISaveDataFileSystem>();
|
||||
|
||||
using (_saveDataFsCacheManager.GetScopedLock())
|
||||
using (_extraDataCacheManager.GetScopedLock())
|
||||
{
|
||||
extraDataAccessor.Get.RegisterExtraDataAccessorObserver(_extraDataCacheManager, spaceId, saveDataId);
|
||||
if (isEmulatedOnHost || !_saveDataFsCacheManager.GetCache(ref saveDataFs.Ref(), spaceId, saveDataId))
|
||||
{
|
||||
bool isDeviceUniqueMac = IsDeviceUniqueMac(spaceId);
|
||||
bool isJournalingSupported = SaveDataProperties.IsJournalingSupported(type);
|
||||
bool isMultiCommitSupported = SaveDataProperties.IsMultiCommitSupported(type);
|
||||
bool openShared = SaveDataProperties.IsSharedOpenNeeded(type);
|
||||
bool isReconstructible = SaveDataProperties.IsReconstructible(type, spaceId);
|
||||
|
||||
rc = _config.SaveFsCreator.Create(ref saveDataFs.Ref(), ref fileSystem.Ref(), spaceId, saveDataId,
|
||||
isEmulatedOnHost, isDeviceUniqueMac, isJournalingSupported, isMultiCommitSupported,
|
||||
openReadOnly, openShared, _timeStampGetter, isReconstructible);
|
||||
if (rc.IsFailure()) return rc.Miss();
|
||||
}
|
||||
|
||||
if (!isEmulatedOnHost && cacheExtraData)
|
||||
{
|
||||
using SharedRef<ISaveDataExtraDataAccessor> extraDataAccessor =
|
||||
SharedRef<ISaveDataExtraDataAccessor>.CreateCopy(in saveDataFs);
|
||||
|
||||
rc = _extraDataCacheManager.Register(in extraDataAccessor, spaceId, saveDataId);
|
||||
if (rc.IsFailure()) return rc.Miss();
|
||||
}
|
||||
}
|
||||
|
||||
using var registerFs = new SharedRef<SaveDataFileSystemCacheRegister>(
|
||||
new SaveDataFileSystemCacheRegister(ref saveDataFs.Ref(), _saveDataFsCacheManager, spaceId, saveDataId));
|
||||
|
||||
if (openReadOnly)
|
||||
{
|
||||
outFileSystem.Reset(new ReadOnlyFileSystem(ref saveDataFs.Ref()));
|
||||
using SharedRef<IFileSystem> tempFs = SharedRef<IFileSystem>.CreateMove(ref registerFs.Ref());
|
||||
using var readOnlyFileSystem = new SharedRef<ReadOnlyFileSystem>(new ReadOnlyFileSystem(ref tempFs.Ref()));
|
||||
|
||||
if (!readOnlyFileSystem.HasValue)
|
||||
return ResultFs.AllocationMemoryFailedInSaveDataFileSystemServiceImplB.Log();
|
||||
|
||||
outFileSystem.SetByMove(ref readOnlyFileSystem.Ref());
|
||||
}
|
||||
else
|
||||
{
|
||||
outFileSystem.SetByMove(ref saveDataFs.Ref());
|
||||
outFileSystem.SetByMove(ref registerFs.Ref());
|
||||
}
|
||||
|
||||
return Result.Success;
|
||||
|
@ -339,15 +339,15 @@ public class SaveDataFileSystemServiceImpl
|
|||
rc = FsSystem.Utility.EnsureDirectory(fileSystem.Get, in saveImageName);
|
||||
if (rc.IsFailure()) return rc.Miss();
|
||||
|
||||
using var saveFileSystem = new SharedRef<IFileSystem>();
|
||||
using var extraDataAccessor = new SharedRef<ISaveDataExtraDataAccessor>();
|
||||
using var saveFileSystem = new SharedRef<ISaveDataFileSystem>();
|
||||
|
||||
bool isJournalingSupported = SaveDataProperties.IsJournalingSupported(attribute.Type);
|
||||
bool isReconstructible = SaveDataProperties.IsReconstructible(attribute.Type, creationInfo.SpaceId);
|
||||
|
||||
rc = _config.SaveFsCreator.Create(ref saveFileSystem.Ref(), ref extraDataAccessor.Ref(),
|
||||
_saveDataFsCacheManager, ref fileSystem.Ref(), creationInfo.SpaceId, saveDataId,
|
||||
allowDirectorySaveData: true, useDeviceUniqueMac: false, isJournalingSupported,
|
||||
isMultiCommitSupported: false, openReadOnly: false, openShared: false, _timeStampGetter);
|
||||
rc = _config.SaveFsCreator.Create(ref saveFileSystem.Ref(), ref fileSystem.Ref(), creationInfo.SpaceId,
|
||||
saveDataId, allowDirectorySaveData: true, isDeviceUniqueMac: false, isJournalingSupported,
|
||||
isMultiCommitSupported: false, openReadOnly: false, openShared: false, _timeStampGetter,
|
||||
isReconstructible);
|
||||
if (rc.IsFailure()) return rc.Miss();
|
||||
|
||||
var extraData = new SaveDataExtraData();
|
||||
|
@ -365,10 +365,10 @@ public class SaveDataFileSystemServiceImpl
|
|||
extraData.DataSize = creationInfo.Size;
|
||||
extraData.JournalSize = creationInfo.JournalSize;
|
||||
|
||||
rc = extraDataAccessor.Get.WriteExtraData(in extraData);
|
||||
rc = saveFileSystem.Get.WriteExtraData(in extraData);
|
||||
if (rc.IsFailure()) return rc.Miss();
|
||||
|
||||
rc = extraDataAccessor.Get.CommitExtraData(true);
|
||||
rc = saveFileSystem.Get.CommitExtraData(true);
|
||||
if (rc.IsFailure()) return rc.Miss();
|
||||
}
|
||||
else
|
||||
|
@ -552,8 +552,8 @@ public class SaveDataFileSystemServiceImpl
|
|||
using (var tmFileSystem = new SharedRef<IFileSystem>())
|
||||
{
|
||||
// Ensure the target save data directory exists
|
||||
rc = _config.TargetManagerFsCreator.Create(ref tmFileSystem.Ref(), in saveDataRootPath, false, true,
|
||||
ResultFs.SaveDataRootPathUnavailable.Value);
|
||||
rc = _config.TargetManagerFsCreator.Create(ref tmFileSystem.Ref(), in saveDataRootPath,
|
||||
openCaseSensitive: false, ensureRootPathExists: true, ResultFs.SaveDataRootPathUnavailable.Value);
|
||||
if (rc.IsFailure()) return rc.Miss();
|
||||
}
|
||||
|
||||
|
@ -564,8 +564,8 @@ public class SaveDataFileSystemServiceImpl
|
|||
rc = _config.TargetManagerFsCreator.NormalizeCaseOfPath(out bool isTargetFsCaseSensitive, ref path.Ref());
|
||||
if (rc.IsFailure()) return rc.Miss();
|
||||
|
||||
rc = _config.TargetManagerFsCreator.Create(ref outFileSystem, in path, isTargetFsCaseSensitive, false,
|
||||
ResultFs.SaveDataRootPathUnavailable.Value);
|
||||
rc = _config.TargetManagerFsCreator.Create(ref outFileSystem, in path, isTargetFsCaseSensitive,
|
||||
ensureRootPathExists: false, ResultFs.SaveDataRootPathUnavailable.Value);
|
||||
if (rc.IsFailure()) return rc.Miss();
|
||||
|
||||
return Result.Success;
|
||||
|
@ -696,20 +696,14 @@ public class SaveDataFileSystemServiceImpl
|
|||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a save is to be stored on a host device.
|
||||
/// </summary>
|
||||
public bool IsAllowedDirectorySaveData(SaveDataSpaceId spaceId, in Path saveDataRootPath)
|
||||
{
|
||||
return spaceId == SaveDataSpaceId.User && IsSaveEmulated(in saveDataRootPath);
|
||||
}
|
||||
|
||||
// Todo: remove once file save data is supported
|
||||
// Used to always allow directory save data in OpenSaveDataFileSystem
|
||||
public bool IsAllowedDirectorySaveData2(SaveDataSpaceId spaceId, in Path saveDataRootPath)
|
||||
{
|
||||
// Todo: remove "|| true" once file save data is supported
|
||||
// ReSharper disable once ConditionIsAlwaysTrueOrFalse
|
||||
return spaceId == SaveDataSpaceId.User && IsSaveEmulated(in saveDataRootPath) || true;
|
||||
}
|
||||
|
||||
public bool IsDeviceUniqueMac(SaveDataSpaceId spaceId)
|
||||
{
|
||||
return spaceId == SaveDataSpaceId.System ||
|
||||
|
|
|
@ -3,61 +3,62 @@ using LibHac.Common;
|
|||
using LibHac.Fs;
|
||||
using LibHac.Fs.Fsa;
|
||||
|
||||
namespace LibHac.FsSrv.Impl;
|
||||
namespace LibHac.FsSystem;
|
||||
|
||||
// ReSharper disable once InconsistentNaming
|
||||
/// <summary>
|
||||
/// Wraps an <see cref="IFile"/>, converting its returned <see cref="Result"/>s to different
|
||||
/// <see cref="Result"/>s based on the <see cref="ConvertResult"/> function.
|
||||
/// </summary>
|
||||
/// <remarks>Based on FS 13.1.0 (nnSdk 13.4.0)</remarks>
|
||||
/// <remarks>Based on FS 14.1.0 (nnSdk 14.3.0)</remarks>
|
||||
public abstract class IResultConvertFile : IFile
|
||||
{
|
||||
protected UniqueRef<IFile> BaseFile;
|
||||
private UniqueRef<IFile> _baseFile;
|
||||
|
||||
protected IResultConvertFile(ref UniqueRef<IFile> baseFile)
|
||||
{
|
||||
BaseFile = new UniqueRef<IFile>(ref baseFile);
|
||||
_baseFile = new UniqueRef<IFile>(ref baseFile);
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
BaseFile.Destroy();
|
||||
_baseFile.Destroy();
|
||||
|
||||
base.Dispose();
|
||||
}
|
||||
|
||||
protected abstract Result ConvertResult(Result result);
|
||||
|
||||
protected override Result DoRead(out long bytesRead, long offset, Span<byte> destination, in ReadOption option)
|
||||
{
|
||||
return ConvertResult(BaseFile.Get.Read(out bytesRead, offset, destination, option));
|
||||
return ConvertResult(_baseFile.Get.Read(out bytesRead, offset, destination, in option)).Ret();
|
||||
}
|
||||
|
||||
protected override Result DoWrite(long offset, ReadOnlySpan<byte> source, in WriteOption option)
|
||||
{
|
||||
return ConvertResult(BaseFile.Get.Write(offset, source, option));
|
||||
return ConvertResult(_baseFile.Get.Write(offset, source, in option)).Ret();
|
||||
}
|
||||
|
||||
protected override Result DoFlush()
|
||||
{
|
||||
return ConvertResult(BaseFile.Get.Flush());
|
||||
return ConvertResult(_baseFile.Get.Flush()).Ret();
|
||||
}
|
||||
|
||||
protected override Result DoSetSize(long size)
|
||||
{
|
||||
return ConvertResult(BaseFile.Get.SetSize(size));
|
||||
return ConvertResult(_baseFile.Get.SetSize(size)).Ret();
|
||||
}
|
||||
|
||||
protected override Result DoGetSize(out long size)
|
||||
{
|
||||
return ConvertResult(BaseFile.Get.GetSize(out size));
|
||||
return ConvertResult(_baseFile.Get.GetSize(out size)).Ret();
|
||||
}
|
||||
|
||||
protected override Result DoOperateRange(Span<byte> outBuffer, OperationId operationId, long offset, long size,
|
||||
ReadOnlySpan<byte> inBuffer)
|
||||
{
|
||||
return ConvertResult(BaseFile.Get.OperateRange(outBuffer, operationId, offset, size, inBuffer));
|
||||
return ConvertResult(_baseFile.Get.OperateRange(outBuffer, operationId, offset, size, inBuffer)).Ret();
|
||||
}
|
||||
|
||||
protected abstract Result ConvertResult(Result result);
|
||||
}
|
||||
|
||||
// ReSharper disable once InconsistentNaming
|
||||
|
@ -65,33 +66,34 @@ public abstract class IResultConvertFile : IFile
|
|||
/// Wraps an <see cref="IDirectory"/>, converting its returned <see cref="Result"/>s to different
|
||||
/// <see cref="Result"/>s based on the <see cref="ConvertResult"/> function.
|
||||
/// </summary>
|
||||
/// <remarks>Based on FS 13.1.0 (nnSdk 13.4.0)</remarks>
|
||||
/// <remarks>Based on FS 14.1.0 (nnSdk 14.3.0)</remarks>
|
||||
public abstract class IResultConvertDirectory : IDirectory
|
||||
{
|
||||
protected UniqueRef<IDirectory> BaseDirectory;
|
||||
private UniqueRef<IDirectory> _baseDirectory;
|
||||
|
||||
protected IResultConvertDirectory(ref UniqueRef<IDirectory> baseDirectory)
|
||||
{
|
||||
BaseDirectory = new UniqueRef<IDirectory>(ref baseDirectory);
|
||||
_baseDirectory = new UniqueRef<IDirectory>(ref baseDirectory);
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
BaseDirectory.Destroy();
|
||||
_baseDirectory.Destroy();
|
||||
|
||||
base.Dispose();
|
||||
}
|
||||
|
||||
protected abstract Result ConvertResult(Result result);
|
||||
|
||||
protected override Result DoRead(out long entriesRead, Span<DirectoryEntry> entryBuffer)
|
||||
{
|
||||
return ConvertResult(BaseDirectory.Get.Read(out entriesRead, entryBuffer));
|
||||
return ConvertResult(_baseDirectory.Get.Read(out entriesRead, entryBuffer)).Ret();
|
||||
}
|
||||
|
||||
protected override Result DoGetEntryCount(out long entryCount)
|
||||
{
|
||||
return ConvertResult(BaseDirectory.Get.GetEntryCount(out entryCount));
|
||||
return ConvertResult(_baseDirectory.Get.GetEntryCount(out entryCount)).Ret();
|
||||
}
|
||||
|
||||
protected abstract Result ConvertResult(Result result);
|
||||
}
|
||||
|
||||
// ReSharper disable once InconsistentNaming
|
||||
|
@ -99,114 +101,110 @@ public abstract class IResultConvertDirectory : IDirectory
|
|||
/// Wraps an <see cref="IFileSystem"/>, converting its returned <see cref="Result"/>s to different
|
||||
/// <see cref="Result"/>s based on the <see cref="ConvertResult"/> function.
|
||||
/// </summary>
|
||||
/// <remarks>Based on FS 13.1.0 (nnSdk 13.4.0)</remarks>
|
||||
public abstract class IResultConvertFileSystem : IFileSystem
|
||||
/// <remarks>Based on FS 14.1.0 (nnSdk 14.3.0)</remarks>
|
||||
public abstract class IResultConvertFileSystem<T> : ISaveDataFileSystem where T : IFileSystem
|
||||
{
|
||||
protected SharedRef<IFileSystem> BaseFileSystem;
|
||||
private SharedRef<T> _baseFileSystem;
|
||||
|
||||
protected IResultConvertFileSystem(ref SharedRef<IFileSystem> baseFileSystem)
|
||||
protected IResultConvertFileSystem(ref SharedRef<T> baseFileSystem)
|
||||
{
|
||||
BaseFileSystem = SharedRef<IFileSystem>.CreateMove(ref baseFileSystem);
|
||||
_baseFileSystem = SharedRef<T>.CreateMove(ref baseFileSystem);
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
BaseFileSystem.Destroy();
|
||||
_baseFileSystem.Destroy();
|
||||
|
||||
base.Dispose();
|
||||
}
|
||||
|
||||
protected T GetFileSystem() => _baseFileSystem.Get;
|
||||
|
||||
protected abstract Result ConvertResult(Result result);
|
||||
|
||||
protected override Result DoCreateFile(in Path path, long size, CreateFileOptions option)
|
||||
{
|
||||
return ConvertResult(BaseFileSystem.Get.CreateFile(path, size, option));
|
||||
return ConvertResult(_baseFileSystem.Get.CreateFile(in path, size)).Ret();
|
||||
}
|
||||
|
||||
protected override Result DoDeleteFile(in Path path)
|
||||
{
|
||||
return ConvertResult(BaseFileSystem.Get.DeleteFile(path));
|
||||
return ConvertResult(_baseFileSystem.Get.DeleteFile(in path)).Ret();
|
||||
}
|
||||
|
||||
protected override Result DoCreateDirectory(in Path path)
|
||||
{
|
||||
return ConvertResult(BaseFileSystem.Get.CreateDirectory(path));
|
||||
return ConvertResult(_baseFileSystem.Get.CreateDirectory(in path)).Ret();
|
||||
}
|
||||
|
||||
protected override Result DoDeleteDirectory(in Path path)
|
||||
{
|
||||
return ConvertResult(BaseFileSystem.Get.DeleteDirectory(path));
|
||||
return ConvertResult(_baseFileSystem.Get.DeleteDirectory(in path)).Ret();
|
||||
}
|
||||
|
||||
protected override Result DoDeleteDirectoryRecursively(in Path path)
|
||||
{
|
||||
return ConvertResult(BaseFileSystem.Get.DeleteDirectoryRecursively(path));
|
||||
return ConvertResult(_baseFileSystem.Get.DeleteDirectoryRecursively(in path)).Ret();
|
||||
}
|
||||
|
||||
protected override Result DoCleanDirectoryRecursively(in Path path)
|
||||
{
|
||||
return ConvertResult(BaseFileSystem.Get.CleanDirectoryRecursively(path));
|
||||
return ConvertResult(_baseFileSystem.Get.CleanDirectoryRecursively(in path)).Ret();
|
||||
}
|
||||
|
||||
protected override Result DoRenameFile(in Path currentPath, in Path newPath)
|
||||
{
|
||||
return ConvertResult(BaseFileSystem.Get.RenameFile(currentPath, newPath));
|
||||
return ConvertResult(_baseFileSystem.Get.RenameFile(in currentPath, in newPath)).Ret();
|
||||
}
|
||||
|
||||
protected override Result DoRenameDirectory(in Path currentPath, in Path newPath)
|
||||
{
|
||||
return ConvertResult(BaseFileSystem.Get.RenameDirectory(currentPath, newPath));
|
||||
return ConvertResult(_baseFileSystem.Get.RenameDirectory(in currentPath, in newPath)).Ret();
|
||||
}
|
||||
|
||||
protected override Result DoGetEntryType(out DirectoryEntryType entryType, in Path path)
|
||||
{
|
||||
return ConvertResult(BaseFileSystem.Get.GetEntryType(out entryType, path));
|
||||
return ConvertResult(_baseFileSystem.Get.GetEntryType(out entryType, in path)).Ret();
|
||||
}
|
||||
|
||||
// Note: The original code uses templates to determine which type of IFile/IDirectory to return. To make things
|
||||
// easier in C# these two functions have been made abstract functions.
|
||||
protected abstract override Result DoOpenFile(ref UniqueRef<IFile> outFile, in Path path, OpenMode mode);
|
||||
|
||||
protected abstract override Result DoOpenDirectory(ref UniqueRef<IDirectory> outDirectory, in Path path,
|
||||
OpenDirectoryMode mode);
|
||||
|
||||
protected override Result DoCommit()
|
||||
{
|
||||
return ConvertResult(BaseFileSystem.Get.Commit());
|
||||
return ConvertResult(_baseFileSystem.Get.Commit()).Ret();
|
||||
}
|
||||
|
||||
protected override Result DoCommitProvisionally(long counter)
|
||||
{
|
||||
return ConvertResult(BaseFileSystem.Get.CommitProvisionally(counter));
|
||||
return ConvertResult(_baseFileSystem.Get.CommitProvisionally(counter)).Ret();
|
||||
}
|
||||
|
||||
protected override Result DoRollback()
|
||||
{
|
||||
return ConvertResult(BaseFileSystem.Get.Rollback());
|
||||
return ConvertResult(_baseFileSystem.Get.Rollback()).Ret();
|
||||
}
|
||||
|
||||
protected override Result DoFlush()
|
||||
{
|
||||
return ConvertResult(BaseFileSystem.Get.Flush());
|
||||
return ConvertResult(_baseFileSystem.Get.Flush()).Ret();
|
||||
}
|
||||
|
||||
protected override Result DoGetFileTimeStampRaw(out FileTimeStampRaw timeStamp, in Path path)
|
||||
{
|
||||
return ConvertResult(BaseFileSystem.Get.GetFileTimeStampRaw(out timeStamp, path));
|
||||
return ConvertResult(_baseFileSystem.Get.GetFileTimeStampRaw(out timeStamp, in path)).Ret();
|
||||
}
|
||||
|
||||
protected override Result DoQueryEntry(Span<byte> outBuffer, ReadOnlySpan<byte> inBuffer, QueryId queryId,
|
||||
in Path path)
|
||||
{
|
||||
return ConvertResult(BaseFileSystem.Get.QueryEntry(outBuffer, inBuffer, queryId, path));
|
||||
return ConvertResult(_baseFileSystem.Get.QueryEntry(outBuffer, inBuffer, queryId, in path)).Ret();
|
||||
}
|
||||
|
||||
protected override Result DoGetFreeSpaceSize(out long freeSpace, in Path path)
|
||||
{
|
||||
return ConvertResult(BaseFileSystem.Get.GetFreeSpaceSize(out freeSpace, path));
|
||||
return ConvertResult(_baseFileSystem.Get.GetFreeSpaceSize(out freeSpace, in path)).Ret();
|
||||
}
|
||||
|
||||
protected override Result DoGetTotalSpaceSize(out long totalSpace, in Path path)
|
||||
{
|
||||
return ConvertResult(BaseFileSystem.Get.GetTotalSpaceSize(out totalSpace, path));
|
||||
return ConvertResult(_baseFileSystem.Get.GetTotalSpaceSize(out totalSpace, in path)).Ret();
|
||||
}
|
||||
|
||||
protected abstract Result ConvertResult(Result result);
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
using System;
|
||||
using LibHac.Common;
|
||||
using LibHac.Fs;
|
||||
|
||||
namespace LibHac.FsSystem;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a mechanism for caching save data file systems.
|
||||
/// </summary>
|
||||
/// <remarks>Based on FS 13.1.0 (nnSdk 13.4.0)</remarks>
|
||||
public interface ISaveDataFileSystemCacheManager : IDisposable
|
||||
{
|
||||
bool GetCache(ref SharedRef<SaveDataFileSystemHolder> outFileSystem, SaveDataSpaceId spaceId, ulong saveDataId);
|
||||
void Register(ref SharedRef<SaveDataFileSystemHolder> fileSystem);
|
||||
void Register(ref SharedRef<ApplicationTemporaryFileSystem> fileSystem);
|
||||
void Unregister(SaveDataSpaceId spaceId, ulong saveDataId);
|
||||
}
|
|
@ -1,171 +0,0 @@
|
|||
using LibHac.Common;
|
||||
using LibHac.Diag;
|
||||
using LibHac.Fs;
|
||||
using LibHac.Os;
|
||||
|
||||
namespace LibHac.FsSystem;
|
||||
|
||||
/// <summary>
|
||||
/// Manages a list of cached save data file systems. Each file system is registered and retrieved
|
||||
/// based on its save data ID and save data space ID.
|
||||
/// </summary>
|
||||
/// <remarks>Based on FS 13.1.0 (nnSdk 13.4.0)</remarks>
|
||||
public class SaveDataFileSystemCacheManager : ISaveDataFileSystemCacheManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Holds a single cached file system identified by its save data ID and save data space ID.
|
||||
/// </summary>
|
||||
/// <remarks>Based on FS 13.1.0 (nnSdk 13.4.0)</remarks>
|
||||
[NonCopyable]
|
||||
private struct Cache
|
||||
{
|
||||
// Note: Nintendo only supports caching SaveDataFileSystem. We support DirectorySaveDataFileSystem too,
|
||||
// so we use a wrapper class to simplify the logic here.
|
||||
private SharedRef<SaveDataFileSystemHolder> _fileSystem;
|
||||
private ulong _saveDataId;
|
||||
private SaveDataSpaceId _spaceId;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_fileSystem.Destroy();
|
||||
}
|
||||
|
||||
public bool IsCached(SaveDataSpaceId spaceId, ulong saveDataId)
|
||||
{
|
||||
return _fileSystem.HasValue && _spaceId == spaceId && _saveDataId == saveDataId;
|
||||
}
|
||||
|
||||
public SharedRef<SaveDataFileSystemHolder> Move()
|
||||
{
|
||||
return SharedRef<SaveDataFileSystemHolder>.CreateMove(ref _fileSystem);
|
||||
}
|
||||
|
||||
public void Register(ref SharedRef<SaveDataFileSystemHolder> fileSystem)
|
||||
{
|
||||
_spaceId = fileSystem.Get.GetSaveDataSpaceId();
|
||||
_saveDataId = fileSystem.Get.GetSaveDataId();
|
||||
|
||||
_fileSystem.SetByMove(ref fileSystem);
|
||||
}
|
||||
|
||||
public void Unregister()
|
||||
{
|
||||
_fileSystem.Reset();
|
||||
}
|
||||
}
|
||||
|
||||
private SdkRecursiveMutexType _mutex;
|
||||
private Cache[] _cachedFileSystems;
|
||||
private int _maxCachedFileSystemCount;
|
||||
private int _nextCacheIndex;
|
||||
|
||||
public SaveDataFileSystemCacheManager()
|
||||
{
|
||||
_mutex = new SdkRecursiveMutexType();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Cache[] caches = Shared.Move(ref _cachedFileSystems);
|
||||
|
||||
if (caches is not null)
|
||||
{
|
||||
for (int i = 0; i < caches.Length; i++)
|
||||
{
|
||||
caches[i].Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Result Initialize(int maxCacheCount)
|
||||
{
|
||||
Assert.SdkRequiresGreaterEqual(maxCacheCount, 0);
|
||||
|
||||
using ScopedLock<SdkRecursiveMutexType> scopedLock = ScopedLock.Lock(ref _mutex);
|
||||
|
||||
Assert.SdkAssert(_cachedFileSystems is null);
|
||||
|
||||
_maxCachedFileSystemCount = maxCacheCount;
|
||||
if (maxCacheCount > 0)
|
||||
{
|
||||
// Note: The original checks for overflow here
|
||||
_cachedFileSystems = new Cache[maxCacheCount];
|
||||
}
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
public bool GetCache(ref SharedRef<SaveDataFileSystemHolder> outFileSystem, SaveDataSpaceId spaceId,
|
||||
ulong saveDataId)
|
||||
{
|
||||
Assert.SdkRequiresGreaterEqual(_maxCachedFileSystemCount, 0);
|
||||
|
||||
using ScopedLock<SdkRecursiveMutexType> scopedLock = ScopedLock.Lock(ref _mutex);
|
||||
|
||||
for (int i = 0; i < _maxCachedFileSystemCount; i++)
|
||||
{
|
||||
if (_cachedFileSystems[i].IsCached(spaceId, saveDataId))
|
||||
{
|
||||
using SharedRef<SaveDataFileSystemHolder> cachedFs = _cachedFileSystems[i].Move();
|
||||
outFileSystem.SetByMove(ref cachedFs.Ref());
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Register(ref SharedRef<ApplicationTemporaryFileSystem> fileSystem)
|
||||
{
|
||||
// Don't cache temporary save data
|
||||
using ScopedLock<SdkRecursiveMutexType> scopedLock = ScopedLock.Lock(ref _mutex);
|
||||
fileSystem.Reset();
|
||||
}
|
||||
|
||||
public void Register(ref SharedRef<SaveDataFileSystemHolder> fileSystem)
|
||||
{
|
||||
if (_maxCachedFileSystemCount <= 0)
|
||||
return;
|
||||
|
||||
Assert.SdkRequiresGreaterEqual(_nextCacheIndex, 0);
|
||||
Assert.SdkRequiresGreater(_maxCachedFileSystemCount, _nextCacheIndex);
|
||||
|
||||
if (fileSystem.Get.GetSaveDataSpaceId() == SaveDataSpaceId.SdSystem)
|
||||
{
|
||||
// Don't cache system save data
|
||||
using ScopedLock<SdkRecursiveMutexType> scopedLock = ScopedLock.Lock(ref _mutex);
|
||||
fileSystem.Reset();
|
||||
}
|
||||
else
|
||||
{
|
||||
Result rc = fileSystem.Get.RollbackOnlyModified();
|
||||
if (rc.IsFailure()) return;
|
||||
|
||||
using ScopedLock<SdkRecursiveMutexType> scopedLock = ScopedLock.Lock(ref _mutex);
|
||||
|
||||
_cachedFileSystems[_nextCacheIndex].Register(ref fileSystem);
|
||||
_nextCacheIndex = (_nextCacheIndex + 1) % _maxCachedFileSystemCount;
|
||||
}
|
||||
}
|
||||
|
||||
public void Unregister(SaveDataSpaceId spaceId, ulong saveDataId)
|
||||
{
|
||||
Assert.SdkRequiresGreaterEqual(_maxCachedFileSystemCount, 0);
|
||||
|
||||
using ScopedLock<SdkRecursiveMutexType> scopedLock = ScopedLock.Lock(ref _mutex);
|
||||
|
||||
for (int i = 0; i < _maxCachedFileSystemCount; i++)
|
||||
{
|
||||
if (_cachedFileSystems[i].IsCached(spaceId, saveDataId))
|
||||
{
|
||||
_cachedFileSystems[i].Unregister();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public UniqueLockRef<SdkRecursiveMutexType> GetScopedLock()
|
||||
{
|
||||
return new UniqueLockRef<SdkRecursiveMutexType>(ref _mutex);
|
||||
}
|
||||
}
|
|
@ -1,132 +0,0 @@
|
|||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using LibHac.Common;
|
||||
using LibHac.Diag;
|
||||
using LibHac.Fs;
|
||||
using LibHac.Fs.Fsa;
|
||||
using LibHac.Tools.FsSystem.Save;
|
||||
|
||||
namespace LibHac.FsSystem;
|
||||
|
||||
/// <summary>
|
||||
/// Wraps a save data <see cref="IFileSystem"/>.
|
||||
/// Upon disposal the base file system is returned to the provided <see cref="ISaveDataFileSystemCacheManager"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the base file system. Must be one of <see cref="SaveDataFileSystem"/>,
|
||||
/// <see cref="ApplicationTemporaryFileSystem"/> or <see cref="DirectorySaveDataFileSystem"/>.</typeparam>
|
||||
/// <remarks>Based on FS 13.1.0 (nnSdk 13.4.0)</remarks>
|
||||
public class SaveDataFileSystemCacheRegisterBase<T> : IFileSystem where T : IFileSystem
|
||||
{
|
||||
private SharedRef<T> _baseFileSystem;
|
||||
private ISaveDataFileSystemCacheManager _cacheManager;
|
||||
|
||||
public SaveDataFileSystemCacheRegisterBase(ref SharedRef<T> baseFileSystem,
|
||||
ISaveDataFileSystemCacheManager cacheManager)
|
||||
{
|
||||
if (typeof(T) != typeof(SaveDataFileSystemHolder) && typeof(T) != typeof(ApplicationTemporaryFileSystem))
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
$"The file system type of a {nameof(SaveDataFileSystemCacheRegisterBase<T>)} must be {nameof(SaveDataFileSystemHolder)} or {nameof(ApplicationTemporaryFileSystem)}.");
|
||||
}
|
||||
|
||||
_baseFileSystem = SharedRef<T>.CreateMove(ref baseFileSystem);
|
||||
_cacheManager = cacheManager;
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
if (typeof(T) == typeof(SaveDataFileSystemHolder))
|
||||
{
|
||||
_cacheManager.Register(ref Unsafe.As<SharedRef<T>, SharedRef<SaveDataFileSystemHolder>>(ref _baseFileSystem));
|
||||
}
|
||||
else if (typeof(T) == typeof(ApplicationTemporaryFileSystem))
|
||||
{
|
||||
_cacheManager.Register(ref Unsafe.As<SharedRef<T>, SharedRef<ApplicationTemporaryFileSystem>>(ref _baseFileSystem));
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.SdkAssert(false, "Invalid save data file system type.");
|
||||
}
|
||||
}
|
||||
|
||||
protected override Result DoOpenFile(ref UniqueRef<IFile> outFile, in Path path, OpenMode mode)
|
||||
{
|
||||
return _baseFileSystem.Get.OpenFile(ref outFile, path, mode);
|
||||
}
|
||||
|
||||
protected override Result DoOpenDirectory(ref UniqueRef<IDirectory> outDirectory, in Path path,
|
||||
OpenDirectoryMode mode)
|
||||
{
|
||||
return _baseFileSystem.Get.OpenDirectory(ref outDirectory, path, mode);
|
||||
}
|
||||
|
||||
protected override Result DoGetEntryType(out DirectoryEntryType entryType, in Path path)
|
||||
{
|
||||
return _baseFileSystem.Get.GetEntryType(out entryType, path);
|
||||
}
|
||||
|
||||
protected override Result DoCreateFile(in Path path, long size, CreateFileOptions option)
|
||||
{
|
||||
return _baseFileSystem.Get.CreateFile(path, size, option);
|
||||
}
|
||||
|
||||
protected override Result DoDeleteFile(in Path path)
|
||||
{
|
||||
return _baseFileSystem.Get.DeleteFile(path);
|
||||
}
|
||||
|
||||
protected override Result DoCreateDirectory(in Path path)
|
||||
{
|
||||
return _baseFileSystem.Get.CreateDirectory(path);
|
||||
}
|
||||
|
||||
protected override Result DoDeleteDirectory(in Path path)
|
||||
{
|
||||
return _baseFileSystem.Get.DeleteDirectory(path);
|
||||
}
|
||||
|
||||
protected override Result DoDeleteDirectoryRecursively(in Path path)
|
||||
{
|
||||
return _baseFileSystem.Get.DeleteDirectoryRecursively(path);
|
||||
}
|
||||
|
||||
protected override Result DoCleanDirectoryRecursively(in Path path)
|
||||
{
|
||||
return _baseFileSystem.Get.CleanDirectoryRecursively(path);
|
||||
}
|
||||
|
||||
protected override Result DoRenameFile(in Path currentPath, in Path newPath)
|
||||
{
|
||||
return _baseFileSystem.Get.RenameFile(currentPath, newPath);
|
||||
}
|
||||
|
||||
protected override Result DoRenameDirectory(in Path currentPath, in Path newPath)
|
||||
{
|
||||
return _baseFileSystem.Get.RenameDirectory(currentPath, newPath);
|
||||
}
|
||||
|
||||
protected override Result DoCommit()
|
||||
{
|
||||
return _baseFileSystem.Get.Commit();
|
||||
}
|
||||
|
||||
protected override Result DoCommitProvisionally(long counter)
|
||||
{
|
||||
return _baseFileSystem.Get.CommitProvisionally(counter);
|
||||
}
|
||||
|
||||
protected override Result DoRollback()
|
||||
{
|
||||
return _baseFileSystem.Get.Rollback();
|
||||
}
|
||||
|
||||
protected override Result DoGetFreeSpaceSize(out long freeSpace, in Path path)
|
||||
{
|
||||
return _baseFileSystem.Get.GetFreeSpaceSize(out freeSpace, path);
|
||||
}
|
||||
|
||||
protected override Result DoGetTotalSpaceSize(out long totalSpace, in Path path)
|
||||
{
|
||||
return _baseFileSystem.Get.GetTotalSpaceSize(out totalSpace, path);
|
||||
}
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
using System;
|
||||
using LibHac.Common;
|
||||
using LibHac.Diag;
|
||||
using LibHac.Fs;
|
||||
using LibHac.Fs.Fsa;
|
||||
|
||||
namespace LibHac.FsSystem;
|
||||
|
||||
/// <summary>
|
||||
/// Holds a file system for adding to the save data file system cache.
|
||||
/// </summary>
|
||||
/// <remarks> Nintendo uses concrete types in <see cref="ISaveDataFileSystemCacheManager"/> instead of an interface.
|
||||
/// This class allows <see cref="DirectorySaveDataFileSystem"/> to be cached in a way that changes the original
|
||||
/// design as little as possible.
|
||||
/// </remarks>
|
||||
public class SaveDataFileSystemHolder : ForwardingFileSystem
|
||||
{
|
||||
public SaveDataFileSystemHolder(ref SharedRef<IFileSystem> baseFileSystem) : base(ref baseFileSystem)
|
||||
{
|
||||
Assert.SdkRequires(BaseFileSystem.Get.GetType() == typeof(SaveDataFileSystemHolder) ||
|
||||
BaseFileSystem.Get.GetType() == typeof(ApplicationTemporaryFileSystem));
|
||||
}
|
||||
|
||||
public SaveDataSpaceId GetSaveDataSpaceId()
|
||||
{
|
||||
IFileSystem baseFs = BaseFileSystem.Get;
|
||||
|
||||
if (baseFs.GetType() == typeof(DirectorySaveDataFileSystem))
|
||||
{
|
||||
return ((DirectorySaveDataFileSystem)baseFs).GetSaveDataSpaceId();
|
||||
}
|
||||
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public ulong GetSaveDataId()
|
||||
{
|
||||
IFileSystem baseFs = BaseFileSystem.Get;
|
||||
|
||||
if (baseFs.GetType() == typeof(DirectorySaveDataFileSystem))
|
||||
{
|
||||
return ((DirectorySaveDataFileSystem)baseFs).GetSaveDataId();
|
||||
}
|
||||
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Result RollbackOnlyModified()
|
||||
{
|
||||
IFileSystem baseFs = BaseFileSystem.Get;
|
||||
|
||||
if (baseFs.GetType() == typeof(DirectorySaveDataFileSystem))
|
||||
{
|
||||
return ((DirectorySaveDataFileSystem)baseFs).Rollback();
|
||||
}
|
||||
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue