mirror of
https://github.com/Thealexbarney/LibHac.git
synced 2024-11-14 10:49:41 +01:00
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:
parent
7f0afbe9db
commit
44e4c7a311
11 changed files with 724 additions and 25 deletions
|
@ -139,10 +139,10 @@ namespace LibHac.Common
|
||||||
/// no null terminating byte will be written to the end of the string.</remarks>
|
/// no null terminating byte will be written to the end of the string.</remarks>
|
||||||
public static int Concat(Span<byte> dest, ReadOnlySpan<byte> source)
|
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;
|
int iDest = destLength;
|
||||||
|
|
||||||
|
|
26
src/LibHac/Diag/Abort.cs
Normal file
26
src/LibHac/Diag/Abort.cs
Normal 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
22
src/LibHac/Diag/Assert.cs
Normal 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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,6 +9,10 @@ namespace LibHac.Fs
|
||||||
{
|
{
|
||||||
public static class PathTool
|
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)
|
public static bool IsSeparator(byte c)
|
||||||
{
|
{
|
||||||
return c == StringTraits.DirectorySeparator;
|
return c == StringTraits.DirectorySeparator;
|
||||||
|
|
|
@ -4,8 +4,18 @@ using LibHac.Fs;
|
||||||
|
|
||||||
namespace LibHac.FsSystem
|
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
|
public class DirectorySaveDataFileSystem : FileSystemBase
|
||||||
{
|
{
|
||||||
|
private const int IdealWorkBufferSize = 0x100000; // 1 MiB
|
||||||
|
|
||||||
private ReadOnlySpan<byte> CommittedDirectoryBytes => new[] { (byte)'/', (byte)'0', (byte)'/' };
|
private ReadOnlySpan<byte> CommittedDirectoryBytes => new[] { (byte)'/', (byte)'0', (byte)'/' };
|
||||||
private ReadOnlySpan<byte> WorkingDirectoryBytes => new[] { (byte)'/', (byte)'1', (byte)'/' };
|
private ReadOnlySpan<byte> WorkingDirectoryBytes => new[] { (byte)'/', (byte)'1', (byte)'/' };
|
||||||
private ReadOnlySpan<byte> SynchronizingDirectoryBytes => new[] { (byte)'/', (byte)'_', (byte)'/' };
|
private ReadOnlySpan<byte> SynchronizingDirectoryBytes => new[] { (byte)'/', (byte)'_', (byte)'/' };
|
||||||
|
@ -18,15 +28,13 @@ namespace LibHac.FsSystem
|
||||||
private object Locker { get; } = new object();
|
private object Locker { get; } = new object();
|
||||||
private int OpenWritableFileCount { get; set; }
|
private int OpenWritableFileCount { get; set; }
|
||||||
private bool IsPersistentSaveData { get; set; }
|
private bool IsPersistentSaveData { get; set; }
|
||||||
|
private bool CanCommitProvisionally { get; set; }
|
||||||
// ReSharper disable once UnusedAutoPropertyAccessor.Local
|
|
||||||
private bool IsUserSaveData { get; set; }
|
|
||||||
|
|
||||||
public static Result CreateNew(out DirectorySaveDataFileSystem created, IFileSystem baseFileSystem,
|
public static Result CreateNew(out DirectorySaveDataFileSystem created, IFileSystem baseFileSystem,
|
||||||
bool isPersistentSaveData, bool isUserSaveData)
|
bool isPersistentSaveData, bool canCommitProvisionally)
|
||||||
{
|
{
|
||||||
var obj = new DirectorySaveDataFileSystem(baseFileSystem);
|
var obj = new DirectorySaveDataFileSystem(baseFileSystem);
|
||||||
Result rc = obj.Initialize(isPersistentSaveData, isUserSaveData);
|
Result rc = obj.Initialize(isPersistentSaveData, canCommitProvisionally);
|
||||||
|
|
||||||
if (rc.IsSuccess())
|
if (rc.IsSuccess())
|
||||||
{
|
{
|
||||||
|
@ -44,10 +52,10 @@ namespace LibHac.FsSystem
|
||||||
BaseFs = baseFileSystem;
|
BaseFs = baseFileSystem;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Result Initialize(bool isPersistentSaveData, bool isUserSaveData)
|
private Result Initialize(bool isPersistentSaveData, bool canCommitProvisionally)
|
||||||
{
|
{
|
||||||
IsPersistentSaveData = isPersistentSaveData;
|
IsPersistentSaveData = isPersistentSaveData;
|
||||||
IsUserSaveData = isUserSaveData;
|
CanCommitProvisionally = canCommitProvisionally;
|
||||||
|
|
||||||
// Ensure the working directory exists
|
// Ensure the working directory exists
|
||||||
Result rc = BaseFs.GetEntryType(out _, WorkingDirectoryPath);
|
Result rc = BaseFs.GetEntryType(out _, WorkingDirectoryPath);
|
||||||
|
@ -277,7 +285,8 @@ namespace LibHac.FsSystem
|
||||||
{
|
{
|
||||||
lock (Locker)
|
lock (Locker)
|
||||||
{
|
{
|
||||||
if (!IsPersistentSaveData) return Result.Success;
|
if (!IsPersistentSaveData)
|
||||||
|
return Result.Success;
|
||||||
|
|
||||||
if (OpenWritableFileCount > 0)
|
if (OpenWritableFileCount > 0)
|
||||||
{
|
{
|
||||||
|
@ -285,24 +294,31 @@ namespace LibHac.FsSystem
|
||||||
return ResultFs.WriteModeFileNotClosed.Log();
|
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
|
// 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 (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
// If something goes wrong beyond this point, the commit will be
|
// If something goes wrong beyond this point, the commit will be
|
||||||
// completed the next time the savedata is opened
|
// completed the next time the savedata is opened
|
||||||
|
|
||||||
rc = SynchronizeDirectory(SynchronizingDirectoryPath, WorkingDirectoryPath);
|
rc = Utility.RetryFinitelyForTargetLocked(SynchronizeWorkingDir);
|
||||||
if (rc.IsFailure()) return rc;
|
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)
|
protected override Result CommitProvisionallyImpl(long commitCount)
|
||||||
{
|
{
|
||||||
if (!IsUserSaveData)
|
if (!CanCommitProvisionally)
|
||||||
return ResultFs.UnsupportedOperationIdInPartitionFileSystem.Log();
|
return ResultFs.UnsupportedOperationInDirectorySaveDataFileSystem.Log();
|
||||||
|
|
||||||
return Result.Success;
|
return Result.Success;
|
||||||
}
|
}
|
||||||
|
@ -313,7 +329,39 @@ namespace LibHac.FsSystem
|
||||||
if (!IsPersistentSaveData)
|
if (!IsPersistentSaveData)
|
||||||
return Result.Success;
|
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)
|
private Result ResolveFullPath(Span<byte> outPath, U8Span relativePath)
|
||||||
|
@ -322,20 +370,36 @@ namespace LibHac.FsSystem
|
||||||
return ResultFs.TooLongPath.Log();
|
return ResultFs.TooLongPath.Log();
|
||||||
|
|
||||||
StringUtils.Copy(outPath, WorkingDirectoryBytes);
|
StringUtils.Copy(outPath, WorkingDirectoryBytes);
|
||||||
outPath[^1] = StringTraits.NullTerminator;
|
outPath[outPath.Length - 1] = StringTraits.NullTerminator;
|
||||||
|
|
||||||
return PathTool.Normalize(outPath.Slice(2), out _, relativePath, false, false);
|
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;
|
if (rc.IsFailure() && !ResultFs.PathNotFound.Includes(rc)) return rc;
|
||||||
|
|
||||||
rc = BaseFs.CreateDirectory(dest);
|
rc = BaseFs.CreateDirectory(destPath);
|
||||||
if (rc.IsFailure()) return rc;
|
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()
|
internal void NotifyCloseWritableFile()
|
||||||
|
|
|
@ -43,7 +43,7 @@ namespace LibHac.FsSystem
|
||||||
|
|
||||||
FsPath dstPath = default;
|
FsPath dstPath = default;
|
||||||
int dstPathLen = StringUtils.Concat(dstPath.Str, destParentPath);
|
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)
|
if (dstPathLen > FsPath.MaxLength)
|
||||||
{
|
{
|
||||||
|
|
|
@ -236,7 +236,7 @@ namespace LibHac.FsSystem
|
||||||
{
|
{
|
||||||
Debug.Assert(IsNormalized(path));
|
Debug.Assert(IsNormalized(path));
|
||||||
|
|
||||||
int i = path.Length - 1;
|
int i = StringUtils.GetLength(path) - 1;
|
||||||
|
|
||||||
// A trailing separator should be ignored
|
// A trailing separator should be ignored
|
||||||
if (path[i] == '/') i--;
|
if (path[i] == '/') i--;
|
||||||
|
@ -267,10 +267,12 @@ namespace LibHac.FsSystem
|
||||||
{
|
{
|
||||||
Debug.Assert(IsNormalized(path));
|
Debug.Assert(IsNormalized(path));
|
||||||
|
|
||||||
if (path.Length == 0)
|
int pathLength = StringUtils.GetLength(path);
|
||||||
|
|
||||||
|
if (pathLength == 0)
|
||||||
return path;
|
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;
|
int i = endIndex;
|
||||||
|
|
||||||
while (i >= 1 && path[i - 1] != '/') i--;
|
while (i >= 1 && path[i - 1] != '/') i--;
|
||||||
|
|
252
src/LibHac/FsSystem/Utility.cs
Normal file
252
src/LibHac/FsSystem/Utility.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
172
tests/LibHac.Tests/Fs/DirectorySaveDataFileSystemTests.cs
Normal file
172
tests/LibHac.Tests/Fs/DirectorySaveDataFileSystemTests.cs
Normal 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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue