mirror of
https://github.com/Thealexbarney/LibHac.git
synced 2024-11-14 10:49:41 +01:00
Update DirectorySaveDataFileSystem to 11.0.0
This commit is contained in:
parent
4ea2896b72
commit
bb2c870f27
6 changed files with 323 additions and 271 deletions
|
@ -1,5 +1,6 @@
|
||||||
using LibHac.Fs.Fsa;
|
using LibHac.Fs.Fsa;
|
||||||
using LibHac.Fs.Shim;
|
using LibHac.Fs.Shim;
|
||||||
|
using LibHac.FsSystem;
|
||||||
|
|
||||||
namespace LibHac.Fs
|
namespace LibHac.Fs
|
||||||
{
|
{
|
||||||
|
@ -26,6 +27,7 @@ namespace LibHac.Fs
|
||||||
public FsContextHandlerGlobals FsContextHandler;
|
public FsContextHandlerGlobals FsContextHandler;
|
||||||
public ResultHandlingUtilityGlobals ResultHandlingUtility;
|
public ResultHandlingUtilityGlobals ResultHandlingUtility;
|
||||||
public PathUtilityGlobals PathUtility;
|
public PathUtilityGlobals PathUtility;
|
||||||
|
public DirectorySaveDataFileSystemGlobals DirectorySaveDataFileSystem;
|
||||||
|
|
||||||
public void Initialize(FileSystemClient fsClient, HorizonClient horizonClient)
|
public void Initialize(FileSystemClient fsClient, HorizonClient horizonClient)
|
||||||
{
|
{
|
||||||
|
@ -35,6 +37,7 @@ namespace LibHac.Fs
|
||||||
UserMountTable.Initialize(fsClient);
|
UserMountTable.Initialize(fsClient);
|
||||||
FsContextHandler.Initialize(fsClient);
|
FsContextHandler.Initialize(fsClient);
|
||||||
PathUtility.Initialize(fsClient);
|
PathUtility.Initialize(fsClient);
|
||||||
|
DirectorySaveDataFileSystem.Initialize(fsClient);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -50,7 +50,7 @@ namespace LibHac.FsSrv.FsCreator
|
||||||
bool isUserSaveData = type == SaveDataType.Account || type == SaveDataType.Device;
|
bool isUserSaveData = type == SaveDataType.Account || type == SaveDataType.Device;
|
||||||
|
|
||||||
rc = DirectorySaveDataFileSystem.CreateNew(out DirectorySaveDataFileSystem saveFs, subDirFs,
|
rc = DirectorySaveDataFileSystem.CreateNew(out DirectorySaveDataFileSystem saveFs, subDirFs,
|
||||||
isPersistentSaveData, isUserSaveData);
|
isPersistentSaveData, isUserSaveData, true);
|
||||||
if (rc.IsFailure()) return rc;
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
fileSystem = saveFs;
|
fileSystem = saveFs;
|
||||||
|
|
|
@ -1,61 +0,0 @@
|
||||||
using System;
|
|
||||||
using LibHac.Fs;
|
|
||||||
using LibHac.Fs.Fsa;
|
|
||||||
|
|
||||||
namespace LibHac.FsSystem
|
|
||||||
{
|
|
||||||
public class DirectorySaveDataFile : IFile
|
|
||||||
{
|
|
||||||
private IFile BaseFile { get; }
|
|
||||||
private DirectorySaveDataFileSystem ParentFs { get; }
|
|
||||||
private OpenMode Mode { get; }
|
|
||||||
|
|
||||||
public DirectorySaveDataFile(DirectorySaveDataFileSystem parentFs, IFile baseFile, OpenMode mode)
|
|
||||||
{
|
|
||||||
ParentFs = parentFs;
|
|
||||||
BaseFile = baseFile;
|
|
||||||
Mode = mode;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Result DoRead(out long bytesRead, long offset, Span<byte> destination,
|
|
||||||
in ReadOption option)
|
|
||||||
{
|
|
||||||
return BaseFile.Read(out bytesRead, offset, destination, in option);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Result DoWrite(long offset, ReadOnlySpan<byte> source, in WriteOption option)
|
|
||||||
{
|
|
||||||
return BaseFile.Write(offset, source, in option);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Result DoFlush()
|
|
||||||
{
|
|
||||||
return BaseFile.Flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Result DoGetSize(out long size)
|
|
||||||
{
|
|
||||||
return BaseFile.GetSize(out size);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Result DoSetSize(long size)
|
|
||||||
{
|
|
||||||
return BaseFile.SetSize(size);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Result DoOperateRange(Span<byte> outBuffer, OperationId operationId, long offset, long size, ReadOnlySpan<byte> inBuffer)
|
|
||||||
{
|
|
||||||
return BaseFile.OperateRange(outBuffer, operationId, offset, size, inBuffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Dispose(bool disposing)
|
|
||||||
{
|
|
||||||
if (Mode.HasFlag(OpenMode.Write))
|
|
||||||
{
|
|
||||||
ParentFs.NotifyCloseWritableFile();
|
|
||||||
}
|
|
||||||
|
|
||||||
BaseFile?.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,17 +3,28 @@ using System.Runtime.CompilerServices;
|
||||||
using LibHac.Common;
|
using LibHac.Common;
|
||||||
using LibHac.Fs;
|
using LibHac.Fs;
|
||||||
using LibHac.Fs.Fsa;
|
using LibHac.Fs.Fsa;
|
||||||
|
using LibHac.Os;
|
||||||
using LibHac.Util;
|
using LibHac.Util;
|
||||||
|
|
||||||
namespace LibHac.FsSystem
|
namespace LibHac.FsSystem
|
||||||
{
|
{
|
||||||
|
internal struct DirectorySaveDataFileSystemGlobals
|
||||||
|
{
|
||||||
|
public SdkMutexType SynchronizeDirectoryMutex;
|
||||||
|
|
||||||
|
public void Initialize(FileSystemClient fsClient)
|
||||||
|
{
|
||||||
|
SynchronizeDirectoryMutex.Initialize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// An <see cref="IFileSystem"/> that provides transactional commits for savedata on top of another base IFileSystem.
|
/// An <see cref="IFileSystem"/> that provides transactional commits for savedata on top of another base IFileSystem.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Transactional commits should be atomic as long as the <see cref="IFileSystem.RenameDirectory"/> function of the
|
/// Transactional commits should be atomic as long as the <see cref="IFileSystem.RenameDirectory"/> function of the
|
||||||
/// underlying <see cref="IFileSystem"/> is atomic.
|
/// underlying <see cref="IFileSystem"/> is atomic.
|
||||||
/// <br/>Based on FS 10.0.0 (nnSdk 10.4.0)
|
/// <br/>Based on FS 11.0.0 (nnSdk 11.4.0)
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public class DirectorySaveDataFileSystem : IFileSystem
|
public class DirectorySaveDataFileSystem : IFileSystem
|
||||||
{
|
{
|
||||||
|
@ -27,17 +38,82 @@ namespace LibHac.FsSystem
|
||||||
private U8Span WorkingDirectoryPath => new U8Span(WorkingDirectoryBytes);
|
private U8Span WorkingDirectoryPath => new U8Span(WorkingDirectoryBytes);
|
||||||
private U8Span SynchronizingDirectoryPath => new U8Span(SynchronizingDirectoryBytes);
|
private U8Span SynchronizingDirectoryPath => new U8Span(SynchronizingDirectoryBytes);
|
||||||
|
|
||||||
private IFileSystem BaseFs { get; }
|
private FileSystemClient _fsClient;
|
||||||
private object Locker { get; } = new object();
|
private IFileSystem _baseFs;
|
||||||
private int OpenWritableFileCount { get; set; }
|
private SdkMutexType _mutex;
|
||||||
private bool IsPersistentSaveData { get; set; }
|
// Todo: Unique file system for disposal
|
||||||
private bool CanCommitProvisionally { get; set; }
|
private int _openWritableFileCount;
|
||||||
|
private bool _isPersistentSaveData;
|
||||||
|
private bool _canCommitProvisionally;
|
||||||
|
private bool _useTransactions;
|
||||||
|
|
||||||
|
private class DirectorySaveDataFile : IFile
|
||||||
|
{
|
||||||
|
private IFile _baseFile;
|
||||||
|
private DirectorySaveDataFileSystem _parentFs;
|
||||||
|
private OpenMode _mode;
|
||||||
|
|
||||||
|
public DirectorySaveDataFile(DirectorySaveDataFileSystem parentFs, IFile baseFile, OpenMode mode)
|
||||||
|
{
|
||||||
|
_parentFs = parentFs;
|
||||||
|
_baseFile = baseFile;
|
||||||
|
_mode = mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Result DoRead(out long bytesRead, long offset, Span<byte> destination,
|
||||||
|
in ReadOption option)
|
||||||
|
{
|
||||||
|
return _baseFile.Read(out bytesRead, offset, destination, in option);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Result DoWrite(long offset, ReadOnlySpan<byte> source, in WriteOption option)
|
||||||
|
{
|
||||||
|
return _baseFile.Write(offset, source, in option);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Result DoFlush()
|
||||||
|
{
|
||||||
|
return _baseFile.Flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Result DoGetSize(out long size)
|
||||||
|
{
|
||||||
|
return _baseFile.GetSize(out size);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Result DoSetSize(long size)
|
||||||
|
{
|
||||||
|
return _baseFile.SetSize(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Result DoOperateRange(Span<byte> outBuffer, OperationId operationId, long offset, long size, ReadOnlySpan<byte> inBuffer)
|
||||||
|
{
|
||||||
|
return _baseFile.OperateRange(outBuffer, operationId, offset, size, inBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
_baseFile?.Dispose();
|
||||||
|
|
||||||
|
if (_mode.HasFlag(OpenMode.Write))
|
||||||
|
{
|
||||||
|
_parentFs.DecrementWriteOpenFileCount();
|
||||||
|
_mode = default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
base.Dispose(disposing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static Result CreateNew(out DirectorySaveDataFileSystem created, IFileSystem baseFileSystem,
|
public static Result CreateNew(out DirectorySaveDataFileSystem created, IFileSystem baseFileSystem,
|
||||||
bool isPersistentSaveData, bool canCommitProvisionally)
|
bool isPersistentSaveData, bool canCommitProvisionally, bool useTransactions,
|
||||||
|
FileSystemClient fsClient = null)
|
||||||
{
|
{
|
||||||
var obj = new DirectorySaveDataFileSystem(baseFileSystem);
|
var obj = new DirectorySaveDataFileSystem(baseFileSystem, fsClient);
|
||||||
Result rc = obj.Initialize(isPersistentSaveData, canCommitProvisionally);
|
Result rc = obj.Initialize(isPersistentSaveData, canCommitProvisionally, useTransactions);
|
||||||
|
|
||||||
if (rc.IsSuccess())
|
if (rc.IsSuccess())
|
||||||
{
|
{
|
||||||
|
@ -50,29 +126,58 @@ namespace LibHac.FsSystem
|
||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
private DirectorySaveDataFileSystem(IFileSystem baseFileSystem)
|
/// <summary>
|
||||||
|
/// Create an uninitialized <see cref="DirectorySaveDataFileSystem"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="baseFileSystem">The base <see cref="IFileSystem"/> to use.</param>
|
||||||
|
public DirectorySaveDataFileSystem(IFileSystem baseFileSystem)
|
||||||
{
|
{
|
||||||
BaseFs = baseFileSystem;
|
_baseFs = baseFileSystem;
|
||||||
|
_mutex.Initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Result Initialize(bool isPersistentSaveData, bool canCommitProvisionally)
|
/// <summary>
|
||||||
|
/// Create an uninitialized <see cref="DirectorySaveDataFileSystem"/>.
|
||||||
|
/// If a <see cref="FileSystemClient"/> is provided a global mutex will be used when synchronizing directories.
|
||||||
|
/// Running outside of a Horizon context doesn't require this mutex,
|
||||||
|
/// and null can be passed to <paramref name="fsClient"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="baseFileSystem">The base <see cref="IFileSystem"/> to use.</param>
|
||||||
|
/// <param name="fsClient">The <see cref="FileSystemClient"/> to use. May be null.</param>
|
||||||
|
public DirectorySaveDataFileSystem(IFileSystem baseFileSystem, FileSystemClient fsClient)
|
||||||
{
|
{
|
||||||
IsPersistentSaveData = isPersistentSaveData;
|
_baseFs = baseFileSystem;
|
||||||
CanCommitProvisionally = canCommitProvisionally;
|
_mutex.Initialize();
|
||||||
|
_fsClient = fsClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
_baseFs?.Dispose();
|
||||||
|
base.Dispose(disposing);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Result Initialize(bool isPersistentSaveData, bool canCommitProvisionally, bool useTransactions)
|
||||||
|
{
|
||||||
|
_isPersistentSaveData = isPersistentSaveData;
|
||||||
|
_canCommitProvisionally = canCommitProvisionally;
|
||||||
|
_useTransactions = useTransactions;
|
||||||
|
|
||||||
// Ensure the working directory exists
|
// Ensure the working directory exists
|
||||||
Result rc = BaseFs.GetEntryType(out _, WorkingDirectoryPath);
|
Result rc = _baseFs.GetEntryType(out _, WorkingDirectoryPath);
|
||||||
|
|
||||||
if (rc.IsFailure())
|
if (rc.IsFailure())
|
||||||
{
|
{
|
||||||
if (!ResultFs.PathNotFound.Includes(rc)) return rc;
|
if (!ResultFs.PathNotFound.Includes(rc)) return rc;
|
||||||
|
|
||||||
rc = BaseFs.CreateDirectory(WorkingDirectoryPath);
|
rc = _baseFs.CreateDirectory(WorkingDirectoryPath);
|
||||||
if (rc.IsFailure()) return rc;
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
if (!IsPersistentSaveData) return Result.Success;
|
if (_isPersistentSaveData)
|
||||||
|
{
|
||||||
rc = BaseFs.CreateDirectory(CommittedDirectoryPath);
|
rc = _baseFs.CreateDirectory(CommittedDirectoryPath);
|
||||||
|
if (rc.IsFailure()) return rc;
|
||||||
|
}
|
||||||
|
|
||||||
// Nintendo returns on all failures, but we'll keep going if committed already exists
|
// Nintendo returns on all failures, but we'll keep going if committed already exists
|
||||||
// to avoid confusing people manually creating savedata in emulators
|
// to avoid confusing people manually creating savedata in emulators
|
||||||
|
@ -80,12 +185,16 @@ namespace LibHac.FsSystem
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only the working directory is needed for temporary savedata
|
// Only the working directory is needed for temporary savedata
|
||||||
if (!IsPersistentSaveData) return Result.Success;
|
if (!_isPersistentSaveData)
|
||||||
|
return Result.Success;
|
||||||
|
|
||||||
rc = BaseFs.GetEntryType(out _, CommittedDirectoryPath);
|
rc = _baseFs.GetEntryType(out _, CommittedDirectoryPath);
|
||||||
|
|
||||||
if (rc.IsSuccess())
|
if (rc.IsSuccess())
|
||||||
{
|
{
|
||||||
|
if (!_useTransactions)
|
||||||
|
return Result.Success;
|
||||||
|
|
||||||
return SynchronizeDirectory(WorkingDirectoryPath, CommittedDirectoryPath);
|
return SynchronizeDirectory(WorkingDirectoryPath, CommittedDirectoryPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,20 +206,22 @@ namespace LibHac.FsSystem
|
||||||
rc = SynchronizeDirectory(SynchronizingDirectoryPath, WorkingDirectoryPath);
|
rc = SynchronizeDirectory(SynchronizingDirectoryPath, WorkingDirectoryPath);
|
||||||
if (rc.IsFailure()) return rc;
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
return BaseFs.RenameDirectory(SynchronizingDirectoryPath, CommittedDirectoryPath);
|
return _baseFs.RenameDirectory(SynchronizingDirectoryPath, CommittedDirectoryPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Result DoCreateDirectory(U8Span path)
|
private Result ResolveFullPath(Span<byte> outPath, U8Span relativePath)
|
||||||
{
|
{
|
||||||
Unsafe.SkipInit(out FsPath fullPath);
|
if (StringUtils.GetLength(relativePath, PathTools.MaxPathLength + 1) > PathTools.MaxPathLength)
|
||||||
|
return ResultFs.TooLongPath.Log();
|
||||||
|
|
||||||
Result rc = ResolveFullPath(fullPath.Str, path);
|
U8Span workingPath = !_useTransactions && _isPersistentSaveData
|
||||||
if (rc.IsFailure()) return rc;
|
? CommittedDirectoryPath
|
||||||
|
: WorkingDirectoryPath;
|
||||||
|
|
||||||
lock (Locker)
|
StringUtils.Copy(outPath, workingPath);
|
||||||
{
|
outPath[outPath.Length - 1] = StringTraits.NullTerminator;
|
||||||
return BaseFs.CreateDirectory(fullPath);
|
|
||||||
}
|
return PathNormalizer.Normalize(outPath.Slice(2), out _, relativePath, false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Result DoCreateFile(U8Span path, long size, CreateFileOptions options)
|
protected override Result DoCreateFile(U8Span path, long size, CreateFileOptions options)
|
||||||
|
@ -120,49 +231,9 @@ namespace LibHac.FsSystem
|
||||||
Result rc = ResolveFullPath(fullPath.Str, path);
|
Result rc = ResolveFullPath(fullPath.Str, path);
|
||||||
if (rc.IsFailure()) return rc;
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
lock (Locker)
|
using ScopedLock<SdkMutexType> lk = ScopedLock.Lock(ref _mutex);
|
||||||
{
|
|
||||||
return BaseFs.CreateFile(fullPath, size, options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Result DoDeleteDirectory(U8Span path)
|
return _baseFs.CreateFile(fullPath, size, options);
|
||||||
{
|
|
||||||
Unsafe.SkipInit(out FsPath fullPath);
|
|
||||||
|
|
||||||
Result rc = ResolveFullPath(fullPath.Str, path);
|
|
||||||
if (rc.IsFailure()) return rc;
|
|
||||||
|
|
||||||
lock (Locker)
|
|
||||||
{
|
|
||||||
return BaseFs.DeleteDirectory(fullPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Result DoDeleteDirectoryRecursively(U8Span path)
|
|
||||||
{
|
|
||||||
Unsafe.SkipInit(out FsPath fullPath);
|
|
||||||
|
|
||||||
Result rc = ResolveFullPath(fullPath.Str, path);
|
|
||||||
if (rc.IsFailure()) return rc;
|
|
||||||
|
|
||||||
lock (Locker)
|
|
||||||
{
|
|
||||||
return BaseFs.DeleteDirectoryRecursively(fullPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Result DoCleanDirectoryRecursively(U8Span path)
|
|
||||||
{
|
|
||||||
Unsafe.SkipInit(out FsPath fullPath);
|
|
||||||
|
|
||||||
Result rc = ResolveFullPath(fullPath.Str, path);
|
|
||||||
if (rc.IsFailure()) return rc;
|
|
||||||
|
|
||||||
lock (Locker)
|
|
||||||
{
|
|
||||||
return BaseFs.CleanDirectoryRecursively(fullPath);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Result DoDeleteFile(U8Span path)
|
protected override Result DoDeleteFile(U8Span path)
|
||||||
|
@ -172,69 +243,57 @@ namespace LibHac.FsSystem
|
||||||
Result rc = ResolveFullPath(fullPath.Str, path);
|
Result rc = ResolveFullPath(fullPath.Str, path);
|
||||||
if (rc.IsFailure()) return rc;
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
lock (Locker)
|
using ScopedLock<SdkMutexType> lk = ScopedLock.Lock(ref _mutex);
|
||||||
{
|
|
||||||
return BaseFs.DeleteFile(fullPath);
|
return _baseFs.DeleteFile(fullPath);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Result DoOpenDirectory(out IDirectory directory, U8Span path, OpenDirectoryMode mode)
|
protected override Result DoCreateDirectory(U8Span path)
|
||||||
{
|
{
|
||||||
Unsafe.SkipInit(out FsPath fullPath);
|
Unsafe.SkipInit(out FsPath fullPath);
|
||||||
|
|
||||||
Result rc = ResolveFullPath(fullPath.Str, path);
|
Result rc = ResolveFullPath(fullPath.Str, path);
|
||||||
if (rc.IsFailure())
|
if (rc.IsFailure()) return rc;
|
||||||
{
|
|
||||||
UnsafeHelpers.SkipParamInit(out directory);
|
|
||||||
return rc;
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (Locker)
|
using ScopedLock<SdkMutexType> lk = ScopedLock.Lock(ref _mutex);
|
||||||
{
|
|
||||||
return BaseFs.OpenDirectory(out directory, fullPath, mode);
|
return _baseFs.CreateDirectory(fullPath);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Result DoOpenFile(out IFile file, U8Span path, OpenMode mode)
|
protected override Result DoDeleteDirectory(U8Span path)
|
||||||
{
|
{
|
||||||
UnsafeHelpers.SkipParamInit(out file);
|
|
||||||
|
|
||||||
Unsafe.SkipInit(out FsPath fullPath);
|
Unsafe.SkipInit(out FsPath fullPath);
|
||||||
|
|
||||||
Result rc = ResolveFullPath(fullPath.Str, path);
|
Result rc = ResolveFullPath(fullPath.Str, path);
|
||||||
if (rc.IsFailure()) return rc;
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
lock (Locker)
|
using ScopedLock<SdkMutexType> lk = ScopedLock.Lock(ref _mutex);
|
||||||
{
|
|
||||||
rc = BaseFs.OpenFile(out IFile baseFile, fullPath, mode);
|
|
||||||
if (rc.IsFailure()) return rc;
|
|
||||||
|
|
||||||
file = new DirectorySaveDataFile(this, baseFile, mode);
|
return _baseFs.DeleteDirectory(fullPath);
|
||||||
|
|
||||||
if (mode.HasFlag(OpenMode.Write))
|
|
||||||
{
|
|
||||||
OpenWritableFileCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Result.Success;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Result DoRenameDirectory(U8Span oldPath, U8Span newPath)
|
protected override Result DoDeleteDirectoryRecursively(U8Span path)
|
||||||
{
|
{
|
||||||
Unsafe.SkipInit(out FsPath fullCurrentPath);
|
Unsafe.SkipInit(out FsPath fullPath);
|
||||||
Unsafe.SkipInit(out FsPath fullNewPath);
|
|
||||||
|
|
||||||
Result rc = ResolveFullPath(fullCurrentPath.Str, oldPath);
|
Result rc = ResolveFullPath(fullPath.Str, path);
|
||||||
if (rc.IsFailure()) return rc;
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
rc = ResolveFullPath(fullNewPath.Str, newPath);
|
using ScopedLock<SdkMutexType> lk = ScopedLock.Lock(ref _mutex);
|
||||||
|
|
||||||
|
return _baseFs.DeleteDirectoryRecursively(fullPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Result DoCleanDirectoryRecursively(U8Span path)
|
||||||
|
{
|
||||||
|
Unsafe.SkipInit(out FsPath fullPath);
|
||||||
|
|
||||||
|
Result rc = ResolveFullPath(fullPath.Str, path);
|
||||||
if (rc.IsFailure()) return rc;
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
lock (Locker)
|
using ScopedLock<SdkMutexType> lk = ScopedLock.Lock(ref _mutex);
|
||||||
{
|
|
||||||
return BaseFs.RenameDirectory(fullCurrentPath, fullNewPath);
|
return _baseFs.CleanDirectoryRecursively(fullPath);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Result DoRenameFile(U8Span oldPath, U8Span newPath)
|
protected override Result DoRenameFile(U8Span oldPath, U8Span newPath)
|
||||||
|
@ -248,10 +307,25 @@ namespace LibHac.FsSystem
|
||||||
rc = ResolveFullPath(fullNewPath.Str, newPath);
|
rc = ResolveFullPath(fullNewPath.Str, newPath);
|
||||||
if (rc.IsFailure()) return rc;
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
lock (Locker)
|
using ScopedLock<SdkMutexType> lk = ScopedLock.Lock(ref _mutex);
|
||||||
{
|
|
||||||
return BaseFs.RenameFile(fullCurrentPath, fullNewPath);
|
return _baseFs.RenameFile(fullCurrentPath, fullNewPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override Result DoRenameDirectory(U8Span oldPath, U8Span newPath)
|
||||||
|
{
|
||||||
|
Unsafe.SkipInit(out FsPath fullCurrentPath);
|
||||||
|
Unsafe.SkipInit(out FsPath fullNewPath);
|
||||||
|
|
||||||
|
Result rc = ResolveFullPath(fullCurrentPath.Str, oldPath);
|
||||||
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
|
rc = ResolveFullPath(fullNewPath.Str, newPath);
|
||||||
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
|
using ScopedLock<SdkMutexType> lk = ScopedLock.Lock(ref _mutex);
|
||||||
|
|
||||||
|
return _baseFs.RenameDirectory(fullCurrentPath, fullNewPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Result DoGetEntryType(out DirectoryEntryType entryType, U8Span path)
|
protected override Result DoGetEntryType(out DirectoryEntryType entryType, U8Span path)
|
||||||
|
@ -265,102 +339,47 @@ namespace LibHac.FsSystem
|
||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (Locker)
|
using ScopedLock<SdkMutexType> lk = ScopedLock.Lock(ref _mutex);
|
||||||
{
|
|
||||||
return BaseFs.GetEntryType(out entryType, fullPath);
|
return _baseFs.GetEntryType(out entryType, fullPath);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Result DoCommit()
|
protected override Result DoOpenFile(out IFile file, U8Span path, OpenMode mode)
|
||||||
{
|
{
|
||||||
lock (Locker)
|
UnsafeHelpers.SkipParamInit(out file);
|
||||||
|
|
||||||
|
Unsafe.SkipInit(out FsPath fullPath);
|
||||||
|
|
||||||
|
Result rc = ResolveFullPath(fullPath.Str, path);
|
||||||
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
|
using ScopedLock<SdkMutexType> lk = ScopedLock.Lock(ref _mutex);
|
||||||
|
|
||||||
|
rc = _baseFs.OpenFile(out IFile baseFile, fullPath, mode);
|
||||||
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
|
file = new DirectorySaveDataFile(this, baseFile, mode);
|
||||||
|
|
||||||
|
if (mode.HasFlag(OpenMode.Write))
|
||||||
{
|
{
|
||||||
if (!IsPersistentSaveData)
|
_openWritableFileCount++;
|
||||||
return Result.Success;
|
|
||||||
|
|
||||||
if (OpenWritableFileCount > 0)
|
|
||||||
{
|
|
||||||
// All files must be closed before commiting save data.
|
|
||||||
return ResultFs.WriteModeFileNotClosed.Log();
|
|
||||||
}
|
|
||||||
|
|
||||||
Result RenameCommittedDir() => BaseFs.RenameDirectory(CommittedDirectoryPath, SynchronizingDirectoryPath);
|
|
||||||
Result SynchronizeWorkingDir() => SynchronizeDirectory(SynchronizingDirectoryPath, WorkingDirectoryPath);
|
|
||||||
Result RenameSynchronizingDir() => BaseFs.RenameDirectory(SynchronizingDirectoryPath, CommittedDirectoryPath);
|
|
||||||
|
|
||||||
// Get rid of the previous commit by renaming the folder
|
|
||||||
Result rc = Utility.RetryFinitelyForTargetLocked(RenameCommittedDir);
|
|
||||||
if (rc.IsFailure()) return rc;
|
|
||||||
|
|
||||||
// If something goes wrong beyond this point, the commit will be
|
|
||||||
// completed the next time the savedata is opened
|
|
||||||
|
|
||||||
rc = Utility.RetryFinitelyForTargetLocked(SynchronizeWorkingDir);
|
|
||||||
if (rc.IsFailure()) return rc;
|
|
||||||
|
|
||||||
rc = Utility.RetryFinitelyForTargetLocked(RenameSynchronizingDir);
|
|
||||||
if (rc.IsFailure()) return rc;
|
|
||||||
|
|
||||||
return Result.Success;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
protected override Result DoCommitProvisionally(long counter)
|
|
||||||
{
|
|
||||||
if (!CanCommitProvisionally)
|
|
||||||
return ResultFs.UnsupportedCommitProvisionallyForDirectorySaveDataFileSystem.Log();
|
|
||||||
|
|
||||||
return Result.Success;
|
return Result.Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Result DoRollback()
|
protected override Result DoOpenDirectory(out IDirectory directory, U8Span path, OpenDirectoryMode mode)
|
||||||
{
|
{
|
||||||
// No old data is kept for temporary save data, so there's nothing to rollback to
|
UnsafeHelpers.SkipParamInit(out directory);
|
||||||
if (!IsPersistentSaveData)
|
|
||||||
return Result.Success;
|
|
||||||
|
|
||||||
return Initialize(IsPersistentSaveData, CanCommitProvisionally);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Result DoGetFreeSpaceSize(out long freeSpace, U8Span path)
|
|
||||||
{
|
|
||||||
UnsafeHelpers.SkipParamInit(out freeSpace);
|
|
||||||
|
|
||||||
Unsafe.SkipInit(out FsPath fullPath);
|
Unsafe.SkipInit(out FsPath fullPath);
|
||||||
|
|
||||||
Result rc = ResolveFullPath(fullPath.Str, path);
|
Result rc = ResolveFullPath(fullPath.Str, path);
|
||||||
if (rc.IsFailure()) return rc;
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
lock (Locker)
|
using ScopedLock<SdkMutexType> lk = ScopedLock.Lock(ref _mutex);
|
||||||
{
|
|
||||||
return BaseFs.GetFreeSpaceSize(out freeSpace, fullPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Result DoGetTotalSpaceSize(out long totalSpace, U8Span path)
|
return _baseFs.OpenDirectory(out directory, fullPath, mode);
|
||||||
{
|
|
||||||
UnsafeHelpers.SkipParamInit(out totalSpace);
|
|
||||||
|
|
||||||
Unsafe.SkipInit(out FsPath fullPath);
|
|
||||||
|
|
||||||
Result rc = ResolveFullPath(fullPath.Str, path);
|
|
||||||
if (rc.IsFailure()) return rc;
|
|
||||||
|
|
||||||
lock (Locker)
|
|
||||||
{
|
|
||||||
return BaseFs.GetTotalSpaceSize(out totalSpace, fullPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Result ResolveFullPath(Span<byte> outPath, U8Span relativePath)
|
|
||||||
{
|
|
||||||
if (StringUtils.GetLength(relativePath, PathTools.MaxPathLength + 1) > PathTools.MaxPathLength)
|
|
||||||
return ResultFs.TooLongPath.Log();
|
|
||||||
|
|
||||||
StringUtils.Copy(outPath, WorkingDirectoryBytes);
|
|
||||||
outPath[outPath.Length - 1] = StringTraits.NullTerminator;
|
|
||||||
|
|
||||||
return PathNormalizer.Normalize(outPath.Slice(2), out _, relativePath, false, false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -372,7 +391,7 @@ namespace LibHac.FsSystem
|
||||||
private Result SynchronizeDirectory(U8Span destPath, U8Span sourcePath)
|
private Result SynchronizeDirectory(U8Span destPath, U8Span sourcePath)
|
||||||
{
|
{
|
||||||
// Delete destination dir and recreate it.
|
// Delete destination dir and recreate it.
|
||||||
Result rc = BaseFs.DeleteDirectoryRecursively(destPath);
|
Result rc = _baseFs.DeleteDirectoryRecursively(destPath);
|
||||||
|
|
||||||
// Nintendo returns error unconditionally because SynchronizeDirectory is always called in situations
|
// Nintendo returns error unconditionally because SynchronizeDirectory is always called in situations
|
||||||
// where a PathNotFound error would mean the save directory was in an invalid state.
|
// where a PathNotFound error would mean the save directory was in an invalid state.
|
||||||
|
@ -380,22 +399,112 @@ namespace LibHac.FsSystem
|
||||||
// put the save directory in an invalid state.
|
// put the save directory in an invalid state.
|
||||||
if (rc.IsFailure() && !ResultFs.PathNotFound.Includes(rc)) return rc;
|
if (rc.IsFailure() && !ResultFs.PathNotFound.Includes(rc)) return rc;
|
||||||
|
|
||||||
rc = BaseFs.CreateDirectory(destPath);
|
rc = _baseFs.CreateDirectory(destPath);
|
||||||
if (rc.IsFailure()) return rc;
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
// Get a work buffer to work with.
|
// Lock only if initialized with a client
|
||||||
using (var buffer = new RentedArray<byte>(IdealWorkBufferSize))
|
if(_fsClient is not null)
|
||||||
{
|
{
|
||||||
return Utility.CopyDirectoryRecursively(BaseFs, destPath, sourcePath, buffer.Span);
|
using ScopedLock<SdkMutexType> lk =
|
||||||
|
ScopedLock.Lock(ref _fsClient.Globals.DirectorySaveDataFileSystem.SynchronizeDirectoryMutex);
|
||||||
|
|
||||||
|
using (var buffer = new RentedArray<byte>(IdealWorkBufferSize))
|
||||||
|
{
|
||||||
|
return Utility.CopyDirectoryRecursively(_baseFs, destPath, sourcePath, buffer.Span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
using (var buffer = new RentedArray<byte>(IdealWorkBufferSize))
|
||||||
|
{
|
||||||
|
return Utility.CopyDirectoryRecursively(_baseFs, destPath, sourcePath, buffer.Span);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void NotifyCloseWritableFile()
|
protected override Result DoCommit()
|
||||||
{
|
{
|
||||||
lock (Locker)
|
using ScopedLock<SdkMutexType> lk = ScopedLock.Lock(ref _mutex);
|
||||||
|
|
||||||
|
if (!_useTransactions || !_isPersistentSaveData)
|
||||||
|
return Result.Success;
|
||||||
|
|
||||||
|
if (_openWritableFileCount > 0)
|
||||||
{
|
{
|
||||||
OpenWritableFileCount--;
|
// All files must be closed before commiting save data.
|
||||||
|
return ResultFs.WriteModeFileNotClosed.Log();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Result RenameCommittedDir() => _baseFs.RenameDirectory(CommittedDirectoryPath, SynchronizingDirectoryPath);
|
||||||
|
Result SynchronizeWorkingDir() => SynchronizeDirectory(SynchronizingDirectoryPath, WorkingDirectoryPath);
|
||||||
|
Result RenameSynchronizingDir() => _baseFs.RenameDirectory(SynchronizingDirectoryPath, CommittedDirectoryPath);
|
||||||
|
|
||||||
|
// Get rid of the previous commit by renaming the folder
|
||||||
|
Result rc = Utility.RetryFinitelyForTargetLocked(RenameCommittedDir);
|
||||||
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
|
// If something goes wrong beyond this point, the commit will be
|
||||||
|
// completed the next time the savedata is opened
|
||||||
|
|
||||||
|
rc = Utility.RetryFinitelyForTargetLocked(SynchronizeWorkingDir);
|
||||||
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
|
rc = Utility.RetryFinitelyForTargetLocked(RenameSynchronizingDir);
|
||||||
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
|
return Result.Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Result DoCommitProvisionally(long counter)
|
||||||
|
{
|
||||||
|
if (!_canCommitProvisionally)
|
||||||
|
return ResultFs.UnsupportedCommitProvisionallyForDirectorySaveDataFileSystem.Log();
|
||||||
|
|
||||||
|
return Result.Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Result DoRollback()
|
||||||
|
{
|
||||||
|
// No old data is kept for temporary save data, so there's nothing to rollback to
|
||||||
|
if (!_isPersistentSaveData)
|
||||||
|
return Result.Success;
|
||||||
|
|
||||||
|
return Initialize(_isPersistentSaveData, _canCommitProvisionally, _useTransactions);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Result DoGetFreeSpaceSize(out long freeSpace, U8Span path)
|
||||||
|
{
|
||||||
|
UnsafeHelpers.SkipParamInit(out freeSpace);
|
||||||
|
|
||||||
|
Unsafe.SkipInit(out FsPath fullPath);
|
||||||
|
|
||||||
|
Result rc = ResolveFullPath(fullPath.Str, path);
|
||||||
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
|
using ScopedLock<SdkMutexType> lk = ScopedLock.Lock(ref _mutex);
|
||||||
|
|
||||||
|
return _baseFs.GetFreeSpaceSize(out freeSpace, fullPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Result DoGetTotalSpaceSize(out long totalSpace, U8Span path)
|
||||||
|
{
|
||||||
|
UnsafeHelpers.SkipParamInit(out totalSpace);
|
||||||
|
|
||||||
|
Unsafe.SkipInit(out FsPath fullPath);
|
||||||
|
|
||||||
|
Result rc = ResolveFullPath(fullPath.Str, path);
|
||||||
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
|
using ScopedLock<SdkMutexType> lk = ScopedLock.Lock(ref _mutex);
|
||||||
|
|
||||||
|
return _baseFs.GetTotalSpaceSize(out totalSpace, fullPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void DecrementWriteOpenFileCount()
|
||||||
|
{
|
||||||
|
using ScopedLock<SdkMutexType> lk = ScopedLock.Lock(ref _mutex);
|
||||||
|
|
||||||
|
_openWritableFileCount--;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -194,9 +194,9 @@ namespace LibHac.FsSystem
|
||||||
var sb = new U8StringBuilder(destPath.Str);
|
var sb = new U8StringBuilder(destPath.Str);
|
||||||
sb.Append(destParentPath).Append(entry.Name);
|
sb.Append(destParentPath).Append(entry.Name);
|
||||||
|
|
||||||
Abort.DoAbortUnless(sb.Length < Unsafe.SizeOf<FsPath>());
|
Assert.SdkLess(sb.Length, Unsafe.SizeOf<FsPath>());
|
||||||
|
|
||||||
rc = destFileSystem.CreateFile(new U8Span(destPath.Str), entry.Size, CreateFileOptions.None);
|
rc = destFileSystem.CreateFile(new U8Span(destPath.Str), entry.Size);
|
||||||
if (rc.IsFailure()) return rc;
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
rc = destFileSystem.OpenFile(out IFile destFile, new U8Span(destPath.Str), OpenMode.Write);
|
rc = destFileSystem.OpenFile(out IFile destFile, new U8Span(destPath.Str), OpenMode.Write);
|
||||||
|
|
|
@ -30,7 +30,8 @@ namespace LibHac.Tests.Fs
|
||||||
|
|
||||||
public IFileSystem Create()
|
public IFileSystem Create()
|
||||||
{
|
{
|
||||||
DirectorySaveDataFileSystem.CreateNew(out DirectorySaveDataFileSystem saveFs, BaseFileSystem, true, true)
|
DirectorySaveDataFileSystem
|
||||||
|
.CreateNew(out DirectorySaveDataFileSystem saveFs, BaseFileSystem, true, true, true)
|
||||||
.ThrowIfFailure();
|
.ThrowIfFailure();
|
||||||
|
|
||||||
return saveFs;
|
return saveFs;
|
||||||
|
@ -41,7 +42,7 @@ namespace LibHac.Tests.Fs
|
||||||
{
|
{
|
||||||
var baseFs = new InMemoryFileSystem();
|
var baseFs = new InMemoryFileSystem();
|
||||||
|
|
||||||
DirectorySaveDataFileSystem.CreateNew(out DirectorySaveDataFileSystem saveFs, baseFs, true, true)
|
DirectorySaveDataFileSystem.CreateNew(out DirectorySaveDataFileSystem saveFs, baseFs, true, true, true)
|
||||||
.ThrowIfFailure();
|
.ThrowIfFailure();
|
||||||
|
|
||||||
return (baseFs, saveFs);
|
return (baseFs, saveFs);
|
||||||
|
@ -127,7 +128,7 @@ namespace LibHac.Tests.Fs
|
||||||
baseFs.CreateFile("/0/file1".ToU8Span(), 0, CreateFileOptions.None).ThrowIfFailure();
|
baseFs.CreateFile("/0/file1".ToU8Span(), 0, CreateFileOptions.None).ThrowIfFailure();
|
||||||
baseFs.CreateFile("/1/file2".ToU8Span(), 0, CreateFileOptions.None).ThrowIfFailure();
|
baseFs.CreateFile("/1/file2".ToU8Span(), 0, CreateFileOptions.None).ThrowIfFailure();
|
||||||
|
|
||||||
DirectorySaveDataFileSystem.CreateNew(out DirectorySaveDataFileSystem saveFs, baseFs, true, true)
|
DirectorySaveDataFileSystem.CreateNew(out DirectorySaveDataFileSystem saveFs, baseFs, true, true, true)
|
||||||
.ThrowIfFailure();
|
.ThrowIfFailure();
|
||||||
|
|
||||||
Assert.Success(saveFs.GetEntryType(out _, "/file1".ToU8Span()));
|
Assert.Success(saveFs.GetEntryType(out _, "/file1".ToU8Span()));
|
||||||
|
@ -146,7 +147,7 @@ namespace LibHac.Tests.Fs
|
||||||
baseFs.CreateFile("/_/file1".ToU8Span(), 0, CreateFileOptions.None).ThrowIfFailure();
|
baseFs.CreateFile("/_/file1".ToU8Span(), 0, CreateFileOptions.None).ThrowIfFailure();
|
||||||
baseFs.CreateFile("/1/file2".ToU8Span(), 0, CreateFileOptions.None).ThrowIfFailure();
|
baseFs.CreateFile("/1/file2".ToU8Span(), 0, CreateFileOptions.None).ThrowIfFailure();
|
||||||
|
|
||||||
DirectorySaveDataFileSystem.CreateNew(out DirectorySaveDataFileSystem saveFs, baseFs, true, true)
|
DirectorySaveDataFileSystem.CreateNew(out DirectorySaveDataFileSystem saveFs, baseFs, true, true, true)
|
||||||
.ThrowIfFailure();
|
.ThrowIfFailure();
|
||||||
|
|
||||||
Assert.Result(ResultFs.PathNotFound, saveFs.GetEntryType(out _, "/file1".ToU8Span()));
|
Assert.Result(ResultFs.PathNotFound, saveFs.GetEntryType(out _, "/file1".ToU8Span()));
|
||||||
|
@ -163,7 +164,7 @@ namespace LibHac.Tests.Fs
|
||||||
// Set the existing files before initializing the save FS
|
// Set the existing files before initializing the save FS
|
||||||
baseFs.CreateFile("/1/file2".ToU8Span(), 0, CreateFileOptions.None).ThrowIfFailure();
|
baseFs.CreateFile("/1/file2".ToU8Span(), 0, CreateFileOptions.None).ThrowIfFailure();
|
||||||
|
|
||||||
DirectorySaveDataFileSystem.CreateNew(out DirectorySaveDataFileSystem saveFs, baseFs, true, true)
|
DirectorySaveDataFileSystem.CreateNew(out DirectorySaveDataFileSystem saveFs, baseFs, true, true, true)
|
||||||
.ThrowIfFailure();
|
.ThrowIfFailure();
|
||||||
|
|
||||||
Assert.Result(ResultFs.PathNotFound, saveFs.GetEntryType(out _, "/file1".ToU8Span()));
|
Assert.Result(ResultFs.PathNotFound, saveFs.GetEntryType(out _, "/file1".ToU8Span()));
|
||||||
|
|
Loading…
Reference in a new issue