Add some fssystem utility functions and use them in DirectorySaveDataFileSystem (#133)

* Accurately implement DirectorySaveDataFileSystem

* DirectorySaveDataFileSystem updates from FS 10.0
This commit is contained in:
Alex Barney 2020-05-12 13:17:06 -07:00 committed by GitHub
parent 7f0afbe9db
commit 44e4c7a311
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 724 additions and 25 deletions

View file

@ -139,10 +139,10 @@ namespace LibHac.Common
/// no null terminating byte will be written to the end of the string.</remarks>
public static int Concat(Span<byte> dest, ReadOnlySpan<byte> source)
{
return Concat(dest, GetLength(dest), source);
return Concat(dest, source, GetLength(dest));
}
public static int Concat(Span<byte> dest, int destLength, ReadOnlySpan<byte> source)
public static int Concat(Span<byte> dest, ReadOnlySpan<byte> source, int destLength)
{
int iDest = destLength;

26
src/LibHac/Diag/Abort.cs Normal file
View file

@ -0,0 +1,26 @@
using System.Diagnostics.CodeAnalysis;
namespace LibHac.Diag
{
public static class Abort
{
[DoesNotReturn]
public static void DoAbort(string message = null)
{
if (string.IsNullOrWhiteSpace(message))
{
throw new LibHacException("Abort.");
}
throw new LibHacException($"Abort: {message}");
}
public static void DoAbortUnless([DoesNotReturnIf(false)] bool condition, string message = null)
{
if (condition)
return;
DoAbort(message);
}
}
}

22
src/LibHac/Diag/Assert.cs Normal file
View file

@ -0,0 +1,22 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
namespace LibHac.Diag
{
public static class Assert
{
[Conditional("DEBUG")]
public static void AssertTrue([DoesNotReturnIf(false)] bool condition, string message = null)
{
if (condition)
return;
if (string.IsNullOrWhiteSpace(message))
{
throw new LibHacException("Assertion failed.");
}
throw new LibHacException($"Assertion failed: {message}");
}
}
}

View file

@ -9,6 +9,10 @@ namespace LibHac.Fs
{
public static class PathTool
{
// These are kept in nn::fs, but C# requires them to be inside a class
internal const int EntryNameLengthMax = 0x300;
internal const int MountNameLengthMax = 15;
public static bool IsSeparator(byte c)
{
return c == StringTraits.DirectorySeparator;

View file

@ -4,8 +4,18 @@ using LibHac.Fs;
namespace LibHac.FsSystem
{
/// <summary>
/// An <see cref="IFileSystem"/> that provides transactional commits for savedata on top of another base IFileSystem.
/// </summary>
/// <remarks>
/// Transactional commits should be atomic as long as the <see cref="IFileSystem.RenameDirectory"/> function of the
/// underlying <see cref="IFileSystem"/> is atomic.
/// This class is based on nn::fssystem::DirectorySaveDataFileSystem in SDK 10.4.0 used in FS 10.0.0
/// </remarks>
public class DirectorySaveDataFileSystem : FileSystemBase
{
private const int IdealWorkBufferSize = 0x100000; // 1 MiB
private ReadOnlySpan<byte> CommittedDirectoryBytes => new[] { (byte)'/', (byte)'0', (byte)'/' };
private ReadOnlySpan<byte> WorkingDirectoryBytes => new[] { (byte)'/', (byte)'1', (byte)'/' };
private ReadOnlySpan<byte> SynchronizingDirectoryBytes => new[] { (byte)'/', (byte)'_', (byte)'/' };
@ -18,15 +28,13 @@ namespace LibHac.FsSystem
private object Locker { get; } = new object();
private int OpenWritableFileCount { get; set; }
private bool IsPersistentSaveData { get; set; }
// ReSharper disable once UnusedAutoPropertyAccessor.Local
private bool IsUserSaveData { get; set; }
private bool CanCommitProvisionally { get; set; }
public static Result CreateNew(out DirectorySaveDataFileSystem created, IFileSystem baseFileSystem,
bool isPersistentSaveData, bool isUserSaveData)
bool isPersistentSaveData, bool canCommitProvisionally)
{
var obj = new DirectorySaveDataFileSystem(baseFileSystem);
Result rc = obj.Initialize(isPersistentSaveData, isUserSaveData);
Result rc = obj.Initialize(isPersistentSaveData, canCommitProvisionally);
if (rc.IsSuccess())
{
@ -44,10 +52,10 @@ namespace LibHac.FsSystem
BaseFs = baseFileSystem;
}
private Result Initialize(bool isPersistentSaveData, bool isUserSaveData)
private Result Initialize(bool isPersistentSaveData, bool canCommitProvisionally)
{
IsPersistentSaveData = isPersistentSaveData;
IsUserSaveData = isUserSaveData;
CanCommitProvisionally = canCommitProvisionally;
// Ensure the working directory exists
Result rc = BaseFs.GetEntryType(out _, WorkingDirectoryPath);
@ -277,7 +285,8 @@ namespace LibHac.FsSystem
{
lock (Locker)
{
if (!IsPersistentSaveData) return Result.Success;
if (!IsPersistentSaveData)
return Result.Success;
if (OpenWritableFileCount > 0)
{
@ -285,24 +294,31 @@ namespace LibHac.FsSystem
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 = BaseFs.RenameDirectory(CommittedDirectoryPath, SynchronizingDirectoryPath);
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 = SynchronizeDirectory(SynchronizingDirectoryPath, WorkingDirectoryPath);
rc = Utility.RetryFinitelyForTargetLocked(SynchronizeWorkingDir);
if (rc.IsFailure()) return rc;
return BaseFs.RenameDirectory(SynchronizingDirectoryPath, CommittedDirectoryPath);
rc = Utility.RetryFinitelyForTargetLocked(RenameSynchronizingDir);
if (rc.IsFailure()) return rc;
return Result.Success;
}
}
protected override Result CommitProvisionallyImpl(long commitCount)
{
if (!IsUserSaveData)
return ResultFs.UnsupportedOperationIdInPartitionFileSystem.Log();
if (!CanCommitProvisionally)
return ResultFs.UnsupportedOperationInDirectorySaveDataFileSystem.Log();
return Result.Success;
}
@ -313,7 +329,39 @@ namespace LibHac.FsSystem
if (!IsPersistentSaveData)
return Result.Success;
return Initialize(IsPersistentSaveData, IsUserSaveData);
return Initialize(IsPersistentSaveData, CanCommitProvisionally);
}
protected override Result GetFreeSpaceSizeImpl(out long freeSpace, U8Span path)
{
freeSpace = default;
FsPath fullPath;
unsafe { _ = &fullPath; } // workaround for CS0165
Result rc = ResolveFullPath(fullPath.Str, path);
if (rc.IsFailure()) return rc;
lock (Locker)
{
return BaseFs.GetFreeSpaceSize(out freeSpace, fullPath);
}
}
protected override Result GetTotalSpaceSizeImpl(out long totalSpace, U8Span path)
{
totalSpace = default;
FsPath fullPath;
unsafe { _ = &fullPath; } // workaround for CS0165
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)
@ -322,20 +370,36 @@ namespace LibHac.FsSystem
return ResultFs.TooLongPath.Log();
StringUtils.Copy(outPath, WorkingDirectoryBytes);
outPath[^1] = StringTraits.NullTerminator;
outPath[outPath.Length - 1] = StringTraits.NullTerminator;
return PathTool.Normalize(outPath.Slice(2), out _, relativePath, false, false);
}
private Result SynchronizeDirectory(U8Span dest, U8Span src)
/// <summary>
/// Creates the destination directory if needed and copies the source directory to it.
/// </summary>
/// <param name="destPath">The path of the destination directory.</param>
/// <param name="sourcePath">The path of the source directory.</param>
/// <returns>The <see cref="Result"/> of the operation.</returns>
private Result SynchronizeDirectory(U8Span destPath, U8Span sourcePath)
{
Result rc = BaseFs.DeleteDirectoryRecursively(dest);
// Delete destination dir and recreate it.
Result rc = BaseFs.DeleteDirectoryRecursively(destPath);
// 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.
// We'll ignore PathNotFound errors to be more user-friendly to users who might accidentally
// put the save directory in an invalid state.
if (rc.IsFailure() && !ResultFs.PathNotFound.Includes(rc)) return rc;
rc = BaseFs.CreateDirectory(dest);
rc = BaseFs.CreateDirectory(destPath);
if (rc.IsFailure()) return rc;
return BaseFs.CopyDirectory(BaseFs, src.ToString(), dest.ToString());
// Get a work buffer to work with.
using (var buffer = new RentedArray<byte>(IdealWorkBufferSize))
{
return Utility.CopyDirectoryRecursively(BaseFs, destPath, sourcePath, buffer.Span);
}
}
internal void NotifyCloseWritableFile()

View file

@ -43,7 +43,7 @@ namespace LibHac.FsSystem
FsPath dstPath = default;
int dstPathLen = StringUtils.Concat(dstPath.Str, destParentPath);
dstPathLen = StringUtils.Concat(dstPath.Str, dstPathLen, dirEntry.Name);
dstPathLen = StringUtils.Concat(dstPath.Str, dirEntry.Name, dstPathLen);
if (dstPathLen > FsPath.MaxLength)
{

View file

@ -236,7 +236,7 @@ namespace LibHac.FsSystem
{
Debug.Assert(IsNormalized(path));
int i = path.Length - 1;
int i = StringUtils.GetLength(path) - 1;
// A trailing separator should be ignored
if (path[i] == '/') i--;
@ -267,10 +267,12 @@ namespace LibHac.FsSystem
{
Debug.Assert(IsNormalized(path));
if (path.Length == 0)
int pathLength = StringUtils.GetLength(path);
if (pathLength == 0)
return path;
int endIndex = path[path.Length - 1] == DirectorySeparator ? path.Length - 1 : path.Length;
int endIndex = path[pathLength - 1] == DirectorySeparator ? pathLength - 1 : pathLength;
int i = endIndex;
while (i >= 1 && path[i - 1] != '/') i--;

View file

@ -0,0 +1,252 @@
using System;
using System.Runtime.CompilerServices;
using System.Threading;
using LibHac.Common;
using LibHac.Diag;
using LibHac.Fs;
namespace LibHac.FsSystem
{
internal static class Utility
{
public delegate Result FsIterationTask(U8Span path, ref DirectoryEntry entry);
private static U8Span RootPath => new U8Span(new[] { (byte)'/' });
private static U8Span DirectorySeparator => RootPath;
public static Result IterateDirectoryRecursively(IFileSystem fs, U8Span rootPath, Span<byte> workPath,
ref DirectoryEntry dirEntry, FsIterationTask onEnterDir, FsIterationTask onExitDir, FsIterationTask onFile)
{
Abort.DoAbortUnless(workPath.Length >= PathTool.EntryNameLengthMax + 1);
// Get size of the root path.
int rootPathLen = StringUtils.GetLength(rootPath, PathTool.EntryNameLengthMax + 1);
if (rootPathLen > PathTool.EntryNameLengthMax)
return ResultFs.TooLongPath.Log();
// Copy root path in, add a / if necessary.
rootPath.Value.Slice(0, rootPathLen).CopyTo(workPath);
if (!PathTool.IsSeparator(workPath[rootPathLen - 1]))
{
workPath[rootPathLen++] = StringTraits.DirectorySeparator;
}
// Make sure the result path is still valid.
if (rootPathLen > PathTool.EntryNameLengthMax)
return ResultFs.TooLongPath.Log();
workPath[rootPathLen] = StringTraits.NullTerminator;
return IterateDirectoryRecursivelyImpl(fs, workPath, ref dirEntry, onEnterDir, onExitDir, onFile);
}
public static Result IterateDirectoryRecursively(IFileSystem fs, U8Span rootPath, FsIterationTask onEnterDir,
FsIterationTask onExitDir, FsIterationTask onFile)
{
DirectoryEntry entry = default;
Span<byte> workPath = stackalloc byte[PathTools.MaxPathLength + 1];
return IterateDirectoryRecursively(fs, rootPath, workPath, ref entry, onEnterDir, onExitDir,
onFile);
}
public static Result IterateDirectoryRecursively(IFileSystem fs, FsIterationTask onEnterDir,
FsIterationTask onExitDir, FsIterationTask onFile)
{
return IterateDirectoryRecursively(fs, RootPath, onEnterDir, onExitDir, onFile);
}
private static Result IterateDirectoryRecursivelyImpl(IFileSystem fs, Span<byte> workPath,
ref DirectoryEntry dirEntry, FsIterationTask onEnterDir, FsIterationTask onExitDir, FsIterationTask onFile)
{
Result rc = fs.OpenDirectory(out IDirectory dir, new U8Span(workPath), OpenDirectoryMode.All);
if (rc.IsFailure()) return rc;
int parentLen = StringUtils.GetLength(workPath);
// Read and handle entries.
while (true)
{
// Read a single entry.
rc = dir.Read(out long readCount, SpanHelpers.AsSpan(ref dirEntry));
if (rc.IsFailure()) return rc;
// If we're out of entries, we're done.
if (readCount == 0)
break;
// Validate child path size.
int childNameLen = StringUtils.GetLength(dirEntry.Name);
bool isDir = dirEntry.Type == DirectoryEntryType.Directory;
int separatorSize = isDir ? 1 : 0;
if (parentLen + childNameLen + separatorSize >= workPath.Length)
return ResultFs.TooLongPath.Log();
// Set child path.
StringUtils.Concat(workPath, dirEntry.Name);
{
if (isDir)
{
// Enter directory.
rc = onEnterDir(new U8Span(workPath), ref dirEntry);
if (rc.IsFailure()) return rc;
// Append separator, recurse.
StringUtils.Concat(workPath, DirectorySeparator);
rc = IterateDirectoryRecursivelyImpl(fs, workPath, ref dirEntry, onEnterDir, onExitDir, onFile);
if (rc.IsFailure()) return rc;
// Exit directory.
rc = onExitDir(new U8Span(workPath), ref dirEntry);
if (rc.IsFailure()) return rc;
}
else
{
// Call file handler.
rc = onFile(new U8Span(workPath), ref dirEntry);
if (rc.IsFailure()) return rc;
}
}
// Restore parent path.
workPath[parentLen] = StringTraits.NullTerminator;
}
return Result.Success;
}
public static Result CopyDirectoryRecursively(IFileSystem fileSystem, U8Span destPath, U8Span sourcePath,
Span<byte> workBuffer)
{
return CopyDirectoryRecursively(fileSystem, fileSystem, destPath, sourcePath, workBuffer);
}
public static unsafe Result CopyDirectoryRecursively(IFileSystem destFileSystem, IFileSystem sourceFileSystem,
U8Span destPath, U8Span sourcePath, Span<byte> workBuffer)
{
var destPathBuf = new FsPath();
int originalSize = StringUtils.Copy(destPathBuf.Str, destPath);
Abort.DoAbortUnless(originalSize < Unsafe.SizeOf<FsPath>());
// Pin and recreate the span because C# can't use byref-like types in a closure
int workBufferSize = workBuffer.Length;
fixed (byte* pWorkBuffer = workBuffer)
{
// Copy the pointer to workaround CS1764.
// IterateDirectoryRecursively won't store the delegate anywhere, so it should be safe
byte* pWorkBuffer2 = pWorkBuffer;
Result OnEnterDir(U8Span path, ref DirectoryEntry entry)
{
// Update path, create new dir.
StringUtils.Concat(SpanHelpers.AsByteSpan(ref destPathBuf), entry.Name);
StringUtils.Concat(SpanHelpers.AsByteSpan(ref destPathBuf), DirectorySeparator);
return destFileSystem.CreateDirectory(destPathBuf);
}
Result OnExitDir(U8Span path, ref DirectoryEntry entry)
{
// Check we have a parent directory.
int len = StringUtils.GetLength(SpanHelpers.AsByteSpan(ref destPathBuf));
if (len < 2)
return ResultFs.InvalidPathFormat.Log();
// Find previous separator, add null terminator
int cur = len - 2;
while (!PathTool.IsSeparator(SpanHelpers.AsByteSpan(ref destPathBuf)[cur]) && cur > 0)
{
cur--;
}
SpanHelpers.AsByteSpan(ref destPathBuf)[cur + 1] = StringTraits.NullTerminator;
return Result.Success;
}
Result OnFile(U8Span path, ref DirectoryEntry entry)
{
var buffer = new Span<byte>(pWorkBuffer2, workBufferSize);
return CopyFile(destFileSystem, sourceFileSystem, destPathBuf, path, ref entry, buffer);
}
return IterateDirectoryRecursively(sourceFileSystem, sourcePath, OnEnterDir, OnExitDir, OnFile);
}
}
public static Result CopyFile(IFileSystem destFileSystem, IFileSystem sourceFileSystem, U8Span destParentPath,
U8Span sourcePath, ref DirectoryEntry entry, Span<byte> workBuffer)
{
// Open source file.
Result rc = sourceFileSystem.OpenFile(out IFile sourceFile, sourcePath, OpenMode.Read);
if (rc.IsFailure()) return rc;
using (sourceFile)
{
// Open dest file.
FsPath destPath;
unsafe { _ = &destPath; } // workaround for CS0165
var sb = new U8StringBuilder(destPath.Str);
sb.Append(destParentPath).Append(entry.Name);
Abort.DoAbortUnless(sb.Length < Unsafe.SizeOf<FsPath>());
rc = destFileSystem.CreateFile(new U8Span(destPath.Str), entry.Size, CreateFileOptions.None);
if (rc.IsFailure()) return rc;
rc = destFileSystem.OpenFile(out IFile destFile, new U8Span(destPath.Str), OpenMode.Write);
if (rc.IsFailure()) return rc;
using (destFile)
{
// Read/Write file in work buffer sized chunks.
long remaining = entry.Size;
long offset = 0;
while (remaining > 0)
{
rc = sourceFile.Read(out long bytesRead, offset, workBuffer, ReadOption.None);
if (rc.IsFailure()) return rc;
rc = destFile.Write(offset, workBuffer.Slice(0, (int)bytesRead), WriteOption.None);
if (rc.IsFailure()) return rc;
remaining -= bytesRead;
offset += bytesRead;
}
}
}
return Result.Success;
}
public static Result RetryFinitelyForTargetLocked(Func<Result> function)
{
const int maxRetryCount = 10;
const int retryWaitTimeMs = 100;
int remainingRetries = maxRetryCount;
while (true)
{
Result rc = function();
if (rc.IsSuccess())
return rc;
if (!ResultFs.TargetLocked.Includes(rc))
return rc;
if (remainingRetries <= 0)
return rc;
remainingRetries--;
Thread.Sleep(retryWaitTimeMs);
}
}
}
}

View file

@ -0,0 +1,172 @@
using LibHac.Common;
using LibHac.Fs;
using LibHac.FsSystem;
using LibHac.Tests.Fs.IFileSystemTestBase;
using Xunit;
namespace LibHac.Tests.Fs
{
public class DirectorySaveDataFileSystemTests : CommittableIFileSystemTests
{
protected override IFileSystem CreateFileSystem()
{
return CreateFileSystemInternal().saveFs;
}
protected override IReopenableFileSystemCreator GetFileSystemCreator()
{
return new DirectorySaveDataFileSystemCreator();
}
private class DirectorySaveDataFileSystemCreator : IReopenableFileSystemCreator
{
private IFileSystem _baseFileSystem { get; }
public DirectorySaveDataFileSystemCreator()
{
_baseFileSystem = new InMemoryFileSystem();
}
public IFileSystem Create()
{
DirectorySaveDataFileSystem.CreateNew(out DirectorySaveDataFileSystem saveFs, _baseFileSystem, true, true)
.ThrowIfFailure();
return saveFs;
}
}
private (IFileSystem baseFs, IFileSystem saveFs) CreateFileSystemInternal()
{
var baseFs = new InMemoryFileSystem();
DirectorySaveDataFileSystem.CreateNew(out DirectorySaveDataFileSystem saveFs, baseFs, true, true)
.ThrowIfFailure();
return (baseFs, saveFs);
}
[Fact]
public void CreateFile_CreatedInWorkingDirectory()
{
(IFileSystem baseFs, IFileSystem saveFs) = CreateFileSystemInternal();
saveFs.CreateFile("/file".ToU8Span(), 0, CreateFileOptions.None);
Assert.Success(baseFs.GetEntryType(out DirectoryEntryType type, "/1/file".ToU8Span()));
Assert.Equal(DirectoryEntryType.File, type);
}
[Fact]
public void CreateFile_NotCreatedInCommittedDirectory()
{
(IFileSystem baseFs, IFileSystem saveFs) = CreateFileSystemInternal();
saveFs.CreateFile("/file".ToU8Span(), 0, CreateFileOptions.None);
Assert.Result(ResultFs.PathNotFound, baseFs.GetEntryType(out _, "/0/file".ToU8Span()));
}
[Fact]
public void Commit_FileExistsInCommittedDirectory()
{
(IFileSystem baseFs, IFileSystem saveFs) = CreateFileSystemInternal();
saveFs.CreateFile("/file".ToU8Span(), 0, CreateFileOptions.None);
Assert.Success(saveFs.Commit());
Assert.Success(baseFs.GetEntryType(out DirectoryEntryType type, "/0/file".ToU8Span()));
Assert.Equal(DirectoryEntryType.File, type);
}
[Fact]
public void Rollback_FileDoesNotExistInBaseAfterRollback()
{
(IFileSystem baseFs, IFileSystem saveFs) = CreateFileSystemInternal();
saveFs.CreateFile("/file".ToU8Span(), 0, CreateFileOptions.None);
// Rollback should succeed
Assert.Success(saveFs.Rollback());
// Make sure all the files are gone
Assert.Result(ResultFs.PathNotFound, saveFs.GetEntryType(out _, "/file".ToU8Span()));
Assert.Result(ResultFs.PathNotFound, baseFs.GetEntryType(out _, "/0/file".ToU8Span()));
Assert.Result(ResultFs.PathNotFound, baseFs.GetEntryType(out _, "/1/file".ToU8Span()));
}
[Fact]
public void Rollback_DeletedFileIsRestoredInBaseAfterRollback()
{
(IFileSystem baseFs, IFileSystem saveFs) = CreateFileSystemInternal();
saveFs.CreateFile("/file".ToU8Span(), 0, CreateFileOptions.None);
saveFs.Commit();
saveFs.DeleteFile("/file".ToU8Span());
// Rollback should succeed
Assert.Success(saveFs.Rollback());
// Make sure all the files are restored
Assert.Success(saveFs.GetEntryType(out _, "/file".ToU8Span()));
Assert.Success(baseFs.GetEntryType(out _, "/0/file".ToU8Span()));
Assert.Success(baseFs.GetEntryType(out _, "/1/file".ToU8Span()));
}
[Fact]
public void Initialize_NormalState_UsesCommittedData()
{
var baseFs = new InMemoryFileSystem();
baseFs.CreateDirectory("/0".ToU8Span()).ThrowIfFailure();
baseFs.CreateDirectory("/1".ToU8Span()).ThrowIfFailure();
// Set the existing files before initializing the save FS
baseFs.CreateFile("/0/file1".ToU8Span(), 0, CreateFileOptions.None).ThrowIfFailure();
baseFs.CreateFile("/1/file2".ToU8Span(), 0, CreateFileOptions.None).ThrowIfFailure();
DirectorySaveDataFileSystem.CreateNew(out DirectorySaveDataFileSystem saveFs, baseFs, true, true)
.ThrowIfFailure();
Assert.Success(saveFs.GetEntryType(out _, "/file1".ToU8Span()));
Assert.Result(ResultFs.PathNotFound, saveFs.GetEntryType(out _, "/file2".ToU8Span()));
}
[Fact]
public void Initialize_InterruptedAfterCommitPart1_UsesWorkingData()
{
var baseFs = new InMemoryFileSystem();
baseFs.CreateDirectory("/_".ToU8Span()).ThrowIfFailure();
baseFs.CreateDirectory("/1".ToU8Span()).ThrowIfFailure();
// Set the existing files before initializing the save FS
baseFs.CreateFile("/_/file1".ToU8Span(), 0, CreateFileOptions.None).ThrowIfFailure();
baseFs.CreateFile("/1/file2".ToU8Span(), 0, CreateFileOptions.None).ThrowIfFailure();
DirectorySaveDataFileSystem.CreateNew(out DirectorySaveDataFileSystem saveFs, baseFs, true, true)
.ThrowIfFailure();
Assert.Result(ResultFs.PathNotFound, saveFs.GetEntryType(out _, "/file1".ToU8Span()));
Assert.Success(saveFs.GetEntryType(out _, "/file2".ToU8Span()));
}
[Fact]
public void Initialize_InterruptedDuringCommitPart2_UsesWorkingData()
{
var baseFs = new InMemoryFileSystem();
baseFs.CreateDirectory("/1".ToU8Span()).ThrowIfFailure();
// Set the existing files before initializing the save FS
baseFs.CreateFile("/1/file2".ToU8Span(), 0, CreateFileOptions.None).ThrowIfFailure();
DirectorySaveDataFileSystem.CreateNew(out DirectorySaveDataFileSystem saveFs, baseFs, true, true)
.ThrowIfFailure();
Assert.Result(ResultFs.PathNotFound, saveFs.GetEntryType(out _, "/file1".ToU8Span()));
Assert.Success(saveFs.GetEntryType(out _, "/file2".ToU8Span()));
}
}
}

View file

@ -0,0 +1,143 @@
using LibHac.Common;
using LibHac.Fs;
using Xunit;
namespace LibHac.Tests.Fs.IFileSystemTestBase
{
public abstract partial class CommittableIFileSystemTests
{
[Fact]
public void Commit_AfterSuccessfulCommit_CanReadCommittedData()
{
// "Random" test data
var data1 = new byte[] { 7, 4, 1, 0, 8, 5, 2, 9, 6, 3 };
var data2 = new byte[] { 6, 1, 6, 8, 0, 3, 9, 7, 5, 1 };
IReopenableFileSystemCreator fsCreator = GetFileSystemCreator();
IFileSystem fs = fsCreator.Create();
// Make sure to test both directories and files
fs.CreateDirectory("/dir1".ToU8Span()).ThrowIfFailure();
fs.CreateDirectory("/dir2".ToU8Span()).ThrowIfFailure();
fs.CreateFile("/dir1/file".ToU8Span(), data1.Length, CreateFileOptions.None).ThrowIfFailure();
fs.CreateFile("/dir2/file".ToU8Span(), data2.Length, CreateFileOptions.None).ThrowIfFailure();
fs.OpenFile(out IFile file1, "/dir1/file".ToU8Span(), OpenMode.Write).ThrowIfFailure();
fs.OpenFile(out IFile file2, "/dir2/file".ToU8Span(), OpenMode.Write).ThrowIfFailure();
file1.Write(0, data1, WriteOption.Flush).ThrowIfFailure();
file2.Write(0, data2, WriteOption.Flush).ThrowIfFailure();
file1.Dispose();
file2.Dispose();
fs.Commit().ThrowIfFailure();
fs.Dispose();
// Reopen after committing
fs = fsCreator.Create();
var readData1 = new byte[data1.Length];
var readData2 = new byte[data2.Length];
Assert.Success(fs.OpenFile(out file1, "/dir1/file".ToU8Span(), OpenMode.Read));
using (file1)
{
Assert.Success(file1.Read(out long bytesRead, 0, readData1, ReadOption.None));
Assert.Equal(data1.Length, bytesRead);
}
Assert.Equal(data1, readData1);
Assert.Success(fs.OpenFile(out file2, "/dir2/file".ToU8Span(), OpenMode.Read));
using (file2)
{
Assert.Success(file2.Read(out long bytesRead, 0, readData2, ReadOption.None));
Assert.Equal(data2.Length, bytesRead);
}
Assert.Equal(data2, readData2);
}
[Fact]
public void Rollback_CreateFileThenRollback_FileDoesNotExist()
{
IFileSystem fs = CreateFileSystem();
fs.CreateDirectory("/dir".ToU8Span()).ThrowIfFailure();
fs.CreateFile("/dir/file".ToU8Span(), 0, CreateFileOptions.None).ThrowIfFailure();
// Rollback should succeed
Assert.Success(fs.Rollback());
// Make sure the file and directory are gone
Assert.Result(ResultFs.PathNotFound, fs.GetEntryType(out _, "/dir".ToU8Span()));
Assert.Result(ResultFs.PathNotFound, fs.GetEntryType(out _, "/dir/file".ToU8Span()));
}
[Fact]
public void Rollback_CreateFileThenCloseFs_FileDoesNotExist()
{
IReopenableFileSystemCreator fsCreator = GetFileSystemCreator();
IFileSystem fs = fsCreator.Create();
fs.CreateDirectory("/dir".ToU8Span()).ThrowIfFailure();
fs.CreateFile("/dir/file".ToU8Span(), 0, CreateFileOptions.None).ThrowIfFailure();
// Close without committing and reopen the file system
fs.Dispose();
fs = fsCreator.Create();
// Make sure the file and directory are gone
Assert.Result(ResultFs.PathNotFound, fs.GetEntryType(out _, "/dir".ToU8Span()));
Assert.Result(ResultFs.PathNotFound, fs.GetEntryType(out _, "/dir/file".ToU8Span()));
}
[Fact]
public void Rollback_AfterChangingExistingFiles_GoesBackToOriginalData()
{
// "Random" test data
var data1 = new byte[] { 7, 4, 1, 0, 8, 5, 2, 9, 6, 3 };
var data2 = new byte[] { 6, 1, 6, 8, 0, 3, 9, 7, 5, 1 };
IReopenableFileSystemCreator fsCreator = GetFileSystemCreator();
IFileSystem fs = fsCreator.Create();
fs.CreateDirectory("/dir".ToU8Span()).ThrowIfFailure();
fs.CreateFile("/dir/file".ToU8Span(), data1.Length, CreateFileOptions.None).ThrowIfFailure();
fs.OpenFile(out IFile file, "/dir/file".ToU8Span(), OpenMode.Write).ThrowIfFailure();
file.Write(0, data1, WriteOption.Flush).ThrowIfFailure();
file.Dispose();
// Commit and reopen the file system
fs.Commit().ThrowIfFailure();
fs.Dispose();
fs = fsCreator.Create();
// Make changes to the file
fs.OpenFile(out file, "/dir/file".ToU8Span(), OpenMode.Write).ThrowIfFailure();
file.Write(0, data2, WriteOption.Flush).ThrowIfFailure();
file.Dispose();
Assert.Success(fs.Rollback());
// The file should contain the original data after the rollback
var readData = new byte[data1.Length];
Assert.Success(fs.OpenFile(out file, "/dir/file".ToU8Span(), OpenMode.Read));
using (file)
{
Assert.Success(file.Read(out long bytesRead, 0, readData, ReadOption.None));
Assert.Equal(data1.Length, bytesRead);
}
Assert.Equal(data1, readData);
}
}
}

View file

@ -0,0 +1,14 @@
using LibHac.Fs;
namespace LibHac.Tests.Fs.IFileSystemTestBase
{
public abstract partial class CommittableIFileSystemTests : IFileSystemTests
{
protected interface IReopenableFileSystemCreator
{
IFileSystem Create();
}
protected abstract IReopenableFileSystemCreator GetFileSystemCreator();
}
}