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>
|
||||
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
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
|
||||
{
|
||||
// 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;
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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--;
|
||||
|
|
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