Add InMemoryFileSystem

This commit is contained in:
Alex Barney 2020-01-19 01:40:29 -07:00
parent da467a10b0
commit 1ae973a346
4 changed files with 1485 additions and 0 deletions

View File

@ -0,0 +1,791 @@
using System;
using System.Diagnostics;
using System.IO;
using LibHac.Common;
using LibHac.FsSystem;
namespace LibHac.Fs
{
/// <summary>
/// A filesystem stored in-memory. Mainly used for testing.
/// </summary>
public class InMemoryFileSystem : AttributeFileSystemBase
{
private FileTable FsTable { get; }
public InMemoryFileSystem()
{
FsTable = new FileTable();
}
protected override Result CreateDirectoryImpl(string path)
{
return FsTable.AddDirectory(new U8Span(path));
}
protected override Result CreateDirectoryImpl(string path, NxFileAttributes archiveAttribute)
{
var u8Path = new U8Span(path);
Result rc = FsTable.AddDirectory(u8Path);
if (rc.IsFailure()) return rc;
rc = FsTable.GetDirectory(u8Path, out DirectoryNode dir);
if (rc.IsFailure()) return rc;
dir.Attributes = archiveAttribute;
return Result.Success;
}
protected override Result CreateFileImpl(string path, long size, CreateFileOptions options)
{
var u8Path = new U8Span(path);
Result rc = FsTable.AddFile(u8Path);
if (rc.IsFailure()) return rc;
rc = FsTable.GetFile(u8Path, out FileNode file);
if (rc.IsFailure()) return rc;
return file.File.SetSize(size);
}
protected override Result DeleteDirectoryImpl(string path)
{
return FsTable.DeleteDirectory(new U8Span(path), false);
}
protected override Result DeleteDirectoryRecursivelyImpl(string path)
{
return FsTable.DeleteDirectory(new U8Span(path), true);
}
protected override Result CleanDirectoryRecursivelyImpl(string path)
{
return FsTable.CleanDirectory(new U8Span(path));
}
protected override Result DeleteFileImpl(string path)
{
return FsTable.DeleteFile(new U8Span(path));
}
protected override Result OpenDirectoryImpl(out IDirectory directory, string path, OpenDirectoryMode mode)
{
directory = default;
Result rs = FsTable.GetDirectory(new U8Span(path), out DirectoryNode dirNode);
if (rs.IsFailure()) return rs;
directory = new MemoryDirectory(dirNode, mode);
return Result.Success;
}
protected override Result OpenFileImpl(out IFile file, string path, OpenMode mode)
{
file = default;
var u8Path = new U8Span(path);
Result rc = FsTable.GetFile(u8Path, out FileNode fileNode);
if (rc.IsFailure()) return rc;
file = new MemoryFile(mode, fileNode.File);
return Result.Success;
}
protected override Result RenameDirectoryImpl(string oldPath, string newPath)
{
return FsTable.RenameDirectory(new U8Span(oldPath), new U8Span(newPath));
}
protected override Result RenameFileImpl(string oldPath, string newPath)
{
return FsTable.RenameFile(new U8Span(oldPath), new U8Span(newPath));
}
protected override Result GetEntryTypeImpl(out DirectoryEntryType entryType, string path)
{
var u8Path = new U8Span(path);
if (FsTable.GetFile(u8Path, out _).IsSuccess())
{
entryType = DirectoryEntryType.File;
return Result.Success;
}
if (FsTable.GetDirectory(u8Path, out _).IsSuccess())
{
entryType = DirectoryEntryType.Directory;
return Result.Success;
}
entryType = default;
return ResultFs.PathNotFound.Log();
}
protected override Result CommitImpl()
{
return Result.Success;
}
protected override Result GetFileAttributesImpl(string path, out NxFileAttributes attributes)
{
var u8Path = new U8Span(path);
if (FsTable.GetFile(u8Path, out FileNode file).IsSuccess())
{
attributes = file.Attributes;
return Result.Success;
}
if (FsTable.GetDirectory(u8Path, out DirectoryNode dir).IsSuccess())
{
attributes = dir.Attributes;
return Result.Success;
}
attributes = default;
return ResultFs.PathNotFound.Log();
}
protected override Result SetFileAttributesImpl(string path, NxFileAttributes attributes)
{
var u8Path = new U8Span(path);
if (FsTable.GetFile(u8Path, out FileNode file).IsSuccess())
{
file.Attributes = attributes;
return Result.Success;
}
if (FsTable.GetDirectory(u8Path, out DirectoryNode dir).IsSuccess())
{
dir.Attributes = attributes;
return Result.Success;
}
return ResultFs.PathNotFound.Log();
}
protected override Result GetFileSizeImpl(out long fileSize, string path)
{
var u8Path = new U8Span(path);
if (FsTable.GetFile(u8Path, out FileNode file).IsSuccess())
{
return file.File.GetSize(out fileSize);
}
fileSize = default;
return ResultFs.PathNotFound.Log();
}
// todo: Make a more generic MemoryFile-type class
private class MemoryFile : FileBase
{
private OpenMode Mode { get; }
private MemoryStreamAccessor BaseStream { get; }
public MemoryFile(OpenMode mode, MemoryStreamAccessor buffer)
{
BaseStream = buffer;
Mode = mode;
}
protected override Result ReadImpl(out long bytesRead, long offset, Span<byte> destination, ReadOption options)
{
if (!Mode.HasFlag(OpenMode.Read))
{
bytesRead = 0;
return ResultFs.InvalidOpenModeForRead.Log();
}
return BaseStream.Read(out bytesRead, offset, destination);
}
protected override Result WriteImpl(long offset, ReadOnlySpan<byte> source, WriteOption options)
{
if (!Mode.HasFlag(OpenMode.Write))
{
return ResultFs.InvalidOpenModeForWrite.Log();
}
return BaseStream.Write(offset, source, Mode.HasFlag(OpenMode.AllowAppend));
}
protected override Result FlushImpl()
{
return BaseStream.Flush();
}
protected override Result SetSizeImpl(long size)
{
return BaseStream.SetSize(size);
}
protected override Result GetSizeImpl(out long size)
{
return BaseStream.GetSize(out size);
}
}
private class MemoryDirectory : IDirectory
{
private OpenDirectoryMode Mode { get; }
private DirectoryNode Directory { get; }
private DirectoryNode CurrentDir { get; set; }
private FileNode CurrentFile { get; set; }
public MemoryDirectory(DirectoryNode directory, OpenDirectoryMode mode)
{
Mode = mode;
Directory = directory;
CurrentDir = directory.ChildDirectory;
CurrentFile = directory.ChildFile;
}
public Result Read(out long entriesRead, Span<DirectoryEntry> entryBuffer)
{
int i = 0;
if (Mode.HasFlag(OpenDirectoryMode.Directory))
{
while (CurrentDir != null && i < entryBuffer.Length)
{
ref DirectoryEntry entry = ref entryBuffer[i];
StringUtils.Copy(entry.Name, CurrentDir.Name);
entry.Name[PathTools.MaxPathLength] = 0;
entry.Type = DirectoryEntryType.Directory;
entry.Attributes = CurrentDir.Attributes;
entry.Size = 0;
i++;
CurrentDir = CurrentDir.Next;
}
}
if (Mode.HasFlag(OpenDirectoryMode.File))
{
while (CurrentFile != null && i < entryBuffer.Length)
{
ref DirectoryEntry entry = ref entryBuffer[i];
StringUtils.Copy(entry.Name, CurrentFile.Name);
entry.Name[PathTools.MaxPathLength] = 0;
entry.Type = DirectoryEntryType.File;
entry.Attributes = CurrentFile.Attributes;
CurrentFile.File.GetSize(out entry.Size);
i++;
CurrentFile = CurrentFile.Next;
}
}
entriesRead = i;
return Result.Success;
}
public Result GetEntryCount(out long entryCount)
{
long count = 0;
if (Mode.HasFlag(OpenDirectoryMode.Directory))
{
DirectoryNode current = Directory.ChildDirectory;
while (current != null)
{
count++;
current = current.Next;
}
}
if (Mode.HasFlag(OpenDirectoryMode.File))
{
FileNode current = Directory.ChildFile;
while (current != null)
{
count++;
current = current.Next;
}
}
entryCount = count;
return Result.Success;
}
}
// todo: Replace with a class that uses multiple byte arrays as backing memory
// so resizing doesn't involve copies
/// <summary>
/// Provides exclusive access to a <see cref="MemoryStream"/> object.
/// Used by <see cref="MemoryFile"/> to enable opening a file multiple times with differing permissions.
/// </summary>
private class MemoryStreamAccessor
{
private const int MemStreamMaxLength = int.MaxValue;
private MemoryStream BaseStream { get; }
private object Locker { get; } = new object();
public MemoryStreamAccessor(MemoryStream stream)
{
BaseStream = stream;
}
public Result Read(out long bytesRead, long offset, Span<byte> destination)
{
lock (Locker)
{
if (offset > BaseStream.Length)
{
bytesRead = default;
return ResultFs.ValueOutOfRange.Log();
}
BaseStream.Position = offset;
bytesRead = BaseStream.Read(destination);
return Result.Success;
}
}
public Result Write(long offset, ReadOnlySpan<byte> source, bool allowAppend)
{
lock (Locker)
{
if (offset + source.Length > BaseStream.Length)
{
if (!allowAppend)
{
return ResultFs.FileExtensionWithoutOpenModeAllowAppend.Log();
}
if (offset + source.Length > MemStreamMaxLength)
return ResultFs.ValueOutOfRange.Log();
}
BaseStream.Position = offset;
BaseStream.Write(source);
return Result.Success;
}
}
public Result Flush()
{
return Result.Success;
}
public Result SetSize(long size)
{
lock (Locker)
{
if (size > MemStreamMaxLength)
return ResultFs.ValueOutOfRange.Log();
BaseStream.SetLength(size);
return Result.Success;
}
}
public Result GetSize(out long size)
{
size = BaseStream.Length;
return Result.Success;
}
}
private class FileNode
{
public MemoryStreamAccessor File { get; set; }
public DirectoryNode Parent { get; set; }
public U8String Name { get; set; }
public NxFileAttributes Attributes { get; set; }
public FileNode Next { get; set; }
}
private class DirectoryNode
{
private NxFileAttributes _attributes = NxFileAttributes.Directory;
public NxFileAttributes Attributes
{
get => _attributes;
set => _attributes = value | NxFileAttributes.Directory;
}
public DirectoryNode Parent { get; set; }
public U8String Name { get; set; }
public DirectoryNode Next { get; set; }
public DirectoryNode ChildDirectory { get; set; }
public FileNode ChildFile { get; set; }
}
private class FileTable
{
private DirectoryNode Root;
public FileTable()
{
Root = new DirectoryNode();
Root.Name = new U8String("");
}
public Result AddFile(U8Span path)
{
var parentPath = new U8Span(PathTools.GetParentDirectory(path));
Result rc = FindDirectory(parentPath, out DirectoryNode parent);
if (rc.IsFailure()) return rc;
var fileName = new U8Span(PathTools.GetLastSegment(path));
return AddFile(fileName, parent);
}
public Result AddDirectory(U8Span path)
{
var parentPath = new U8Span(PathTools.GetParentDirectory(path));
Result rc = FindDirectory(parentPath, out DirectoryNode parent);
if (rc.IsFailure()) return rc;
var dirName = new U8Span(PathTools.GetLastSegment(path));
return AddDirectory(dirName, parent);
}
public Result GetFile(U8Span path, out FileNode file)
{
return FindFile(path, out file);
}
public Result GetDirectory(U8Span path, out DirectoryNode dir)
{
return FindDirectory(path, out dir);
}
public Result RenameDirectory(U8Span oldPath, U8Span newPath)
{
Result rc = FindDirectory(oldPath, out DirectoryNode directory);
if (rc.IsFailure()) return rc;
var newParentPath = new U8Span(PathTools.GetParentDirectory(newPath));
rc = FindDirectory(newParentPath, out DirectoryNode newParent);
if (rc.IsFailure()) return rc;
var newName = new U8Span(PathTools.GetLastSegment(newPath));
if (TryFindChildDirectory(newName, newParent, out _) || TryFindChildFile(newName, newParent, out _))
{
return ResultFs.PathAlreadyExists.Log();
}
if (directory.Parent != newParent)
{
if (!UnlinkDirectory(directory))
{
return ResultFs.PreconditionViolation.Log();
}
LinkDirectory(directory, newParent);
}
if (StringUtils.Compare(directory.Name, newName) != 0)
{
directory.Name = newName.ToU8String();
}
return Result.Success;
}
public Result RenameFile(U8Span oldPath, U8Span newPath)
{
Result rc = FindFile(oldPath, out FileNode file);
if (rc.IsFailure()) return rc;
var newParentPath = new U8Span(PathTools.GetParentDirectory(newPath));
rc = FindDirectory(newParentPath, out DirectoryNode newParent);
if (rc.IsFailure()) return rc;
var newName = new U8Span(PathTools.GetLastSegment(newPath));
if (TryFindChildDirectory(newName, newParent, out _) || TryFindChildFile(newName, newParent, out _))
{
return ResultFs.PathAlreadyExists.Log();
}
if (file.Parent != newParent)
{
if (!UnlinkFile(file))
{
return ResultFs.PreconditionViolation.Log();
}
LinkFile(file, newParent);
}
if (StringUtils.Compare(file.Name, newName) != 0)
{
file.Name = newName.ToU8String();
}
return Result.Success;
}
public Result DeleteDirectory(U8Span path, bool recursive)
{
Result rc = FindDirectory(path, out DirectoryNode directory);
if (rc.IsFailure()) return rc;
if (!recursive && (directory.ChildDirectory != null || directory.ChildFile != null))
{
return ResultFs.DirectoryNotEmpty.Log();
}
UnlinkDirectory(directory);
return Result.Success;
}
public Result DeleteFile(U8Span path)
{
Result rc = FindFile(path, out FileNode file);
if (rc.IsFailure()) return rc;
UnlinkFile(file);
return Result.Success;
}
public Result CleanDirectory(U8Span path)
{
Result rc = FindDirectory(path, out DirectoryNode directory);
if (rc.IsFailure()) return rc;
directory.ChildDirectory = null;
directory.ChildFile = null;
return Result.Success;
}
private Result AddFile(U8Span name, DirectoryNode parent)
{
if (TryFindChildDirectory(name, parent, out _))
{
return ResultFs.PathAlreadyExists.Log();
}
if (TryFindChildFile(name, parent, out _))
{
return ResultFs.PathAlreadyExists.Log();
}
var newFileNode = new FileNode
{
Name = name.ToU8String(),
File = new MemoryStreamAccessor(new MemoryStream())
};
LinkFile(newFileNode, parent);
return Result.Success;
}
private Result AddDirectory(U8Span name, DirectoryNode parent)
{
if (TryFindChildDirectory(name, parent, out _))
{
return ResultFs.PathAlreadyExists.Log();
}
if (TryFindChildFile(name, parent, out _))
{
return ResultFs.PathAlreadyExists.Log();
}
var newDirNode = new DirectoryNode { Name = name.ToU8String() };
LinkDirectory(newDirNode, parent);
return Result.Success;
}
private Result FindFile(U8Span path, out FileNode file)
{
var parentPath = new U8Span(PathTools.GetParentDirectory(path));
Result rc = FindDirectory(parentPath, out DirectoryNode parentNode);
if (rc.IsFailure())
{
file = default;
return rc;
}
var fileName = new U8Span(PathTools.GetLastSegment(path));
if (TryFindChildFile(fileName, parentNode, out file))
{
return Result.Success;
}
return ResultFs.PathNotFound.Log();
}
private Result FindDirectory(U8Span path, out DirectoryNode directory)
{
var parser = new PathParser(path);
DirectoryNode current = Root;
while (parser.MoveNext())
{
if (!TryFindChildDirectory(new U8Span(parser.GetCurrent()), current, out DirectoryNode child))
{
directory = default;
return ResultFs.PathNotFound.Log();
}
current = child;
}
directory = current;
return Result.Success;
}
private bool TryFindChildDirectory(U8Span name, DirectoryNode parent, out DirectoryNode child)
{
DirectoryNode currentChild = parent.ChildDirectory;
while (currentChild != null)
{
if (StringUtils.Compare(name, currentChild.Name) == 0)
{
child = currentChild;
return true;
}
currentChild = currentChild.Next;
}
child = default;
return false;
}
private bool TryFindChildFile(U8Span name, DirectoryNode parent, out FileNode child)
{
FileNode currentChild = parent.ChildFile;
while (currentChild != null)
{
if (StringUtils.Compare(name, currentChild.Name) == 0)
{
child = currentChild;
return true;
}
currentChild = currentChild.Next;
}
child = default;
return false;
}
private void LinkDirectory(DirectoryNode dir, DirectoryNode parentDir)
{
Debug.Assert(dir.Parent == null);
Debug.Assert(dir.Next == null);
dir.Next = parentDir.ChildDirectory;
dir.Parent = parentDir;
parentDir.ChildDirectory = dir;
}
private bool UnlinkDirectory(DirectoryNode dir)
{
Debug.Assert(dir.Parent != null);
DirectoryNode parent = dir.Parent;
if (parent.ChildDirectory == null)
return false;
if (parent.ChildDirectory == dir)
{
parent.ChildDirectory = dir.Next;
dir.Parent = null;
return true;
}
DirectoryNode current = parent.ChildDirectory;
while (current != null)
{
if (current.Next == dir)
{
current.Next = dir.Next;
dir.Parent = null;
return true;
}
current = current.Next;
}
return false;
}
private void LinkFile(FileNode file, DirectoryNode parentDir)
{
Debug.Assert(file.Parent == null);
Debug.Assert(file.Next == null);
file.Next = parentDir.ChildFile;
file.Parent = parentDir;
parentDir.ChildFile = file;
}
private bool UnlinkFile(FileNode file)
{
Debug.Assert(file.Parent != null);
DirectoryNode parent = file.Parent;
if (parent.ChildFile == null)
return false;
if (parent.ChildFile == file)
{
parent.ChildFile = file.Next;
file.Parent = null;
return true;
}
FileNode current = parent.ChildFile;
while (current != null)
{
if (current.Next == file)
{
current.Next = file.Next;
file.Parent = null;
return true;
}
current = current.Next;
}
return false;
}
}
}
}

View File

@ -262,6 +262,22 @@ namespace LibHac.FsSystem
return path.Slice(i, path.Length - i);
}
public static ReadOnlySpan<byte> GetLastSegment(ReadOnlySpan<byte> path)
{
Debug.Assert(IsNormalized(path));
if (path.Length == 0)
return path;
int endIndex = path[path.Length - 1] == DirectorySeparator ? path.Length - 1 : path.Length;
int i = endIndex;
while (i >= 1 && path[i - 1] != '/') i--;
i = Math.Max(i, 0);
return path.Slice(i, endIndex - i);
}
public static bool IsNormalized(ReadOnlySpan<char> path)
{
var state = NormalizeState.Initial;

View File

@ -0,0 +1,654 @@
using System;
using LibHac.Common;
using LibHac.Fs;
using Xunit;
namespace LibHac.Tests
{
public class InMemoryFileSystemTests
{
private IAttributeFileSystem GetFileSystem()
{
return new InMemoryFileSystem();
}
[Fact]
public void CreateFileWithNoParentDirectory()
{
IAttributeFileSystem fs = GetFileSystem();
Result rc = fs.CreateFile("/dir/file", 0, CreateFileOptions.None);
Assert.Equal(ResultFs.PathNotFound, rc);
}
[Fact]
public void RootDirectoryHasCorrectEntryType()
{
IAttributeFileSystem fs = GetFileSystem();
Result rc = fs.GetEntryType(out DirectoryEntryType type, "/");
Assert.True(rc.IsSuccess());
Assert.Equal(DirectoryEntryType.Directory, type);
}
[Fact]
public void CreatedFileHasCorrectSize()
{
const long expectedSize = 12345;
IAttributeFileSystem fs = GetFileSystem();
fs.CreateFile("/file", expectedSize, CreateFileOptions.None);
fs.OpenFile(out IFile file, "/file", OpenMode.Read);
Result rc = file.GetSize(out long fileSize);
Assert.True(rc.IsSuccess());
Assert.Equal(expectedSize, fileSize);
}
[Fact]
public void ReadDataWrittenToFileAfterReopening()
{
var data = new byte[] { 7, 4, 1, 0, 8, 5, 2, 9, 6, 3 };
IAttributeFileSystem fs = GetFileSystem();
fs.CreateFile("/file", data.Length, CreateFileOptions.None);
fs.OpenFile(out IFile file, "/file", OpenMode.Write);
file.Write(0, data, WriteOption.None);
file.Dispose();
var readData = new byte[data.Length];
fs.OpenFile(out file, "/file", OpenMode.Read);
Result rc = file.Read(out long bytesRead, 0, readData, ReadOption.None);
file.Dispose();
Assert.True(rc.IsSuccess());
Assert.Equal(data.Length, bytesRead);
Assert.Equal(data, readData);
}
[Fact]
public void ReadDataWrittenToFileAfterRenaming()
{
var data = new byte[] { 7, 4, 1, 0, 8, 5, 2, 9, 6, 3 };
IAttributeFileSystem fs = GetFileSystem();
fs.CreateFile("/file", data.Length, CreateFileOptions.None);
fs.OpenFile(out IFile file, "/file", OpenMode.Write);
file.Write(0, data, WriteOption.None);
file.Dispose();
fs.RenameFile("/file", "/renamed");
var readData = new byte[data.Length];
fs.OpenFile(out file, "/renamed", OpenMode.Read);
Result rc = file.Read(out long bytesRead, 0, readData, ReadOption.None);
file.Dispose();
Assert.True(rc.IsSuccess());
Assert.Equal(data.Length, bytesRead);
Assert.Equal(data, readData);
}
[Fact]
public void OpenFileAsDirectory()
{
IAttributeFileSystem fs = GetFileSystem();
fs.CreateFile("/file", 0, CreateFileOptions.None);
Result rc = fs.OpenDirectory(out _, "/file", OpenDirectoryMode.All);
Assert.Equal(ResultFs.PathNotFound, rc);
}
[Fact]
public void OpenDirectoryAsFile()
{
IAttributeFileSystem fs = GetFileSystem();
fs.CreateDirectory("/dir");
Result rc = fs.OpenFile(out _, "/dir", OpenMode.All);
Assert.Equal(ResultFs.PathNotFound, rc);
}
[Fact]
public void DeleteNonexistentFile()
{
IAttributeFileSystem fs = GetFileSystem();
Result rc = fs.DeleteFile("/file");
Assert.Equal(ResultFs.PathNotFound, rc);
}
[Fact]
public void DeleteNonexistentDirectory()
{
IAttributeFileSystem fs = GetFileSystem();
Result rc = fs.DeleteDirectory("/dir");
Assert.Equal(ResultFs.PathNotFound, rc);
}
[Fact]
public void DeleteFile()
{
IAttributeFileSystem fs = GetFileSystem();
fs.CreateFile("/file", 0, CreateFileOptions.None);
Result rcDelete = fs.DeleteFile("/file");
Result rcEntry = fs.GetEntryType(out _, "/file");
Assert.True(rcDelete.IsSuccess());
Assert.Equal(ResultFs.PathNotFound, rcEntry);
}
[Fact]
public void DeleteFileWithSiblingA()
{
IAttributeFileSystem fs = GetFileSystem();
fs.CreateFile("/file1", 0, CreateFileOptions.None);
fs.CreateFile("/file2", 0, CreateFileOptions.None);
Result rcDelete = fs.DeleteFile("/file2");
Result rcEntry1 = fs.GetEntryType(out DirectoryEntryType dir1Type, "/file1");
Result rcEntry2 = fs.GetEntryType(out _, "/file2");
Assert.True(rcDelete.IsSuccess());
Assert.True(rcEntry1.IsSuccess());
Assert.Equal(ResultFs.PathNotFound, rcEntry2);
Assert.Equal(DirectoryEntryType.File, dir1Type);
}
[Fact]
public void DeleteFileWithSiblingB()
{
IAttributeFileSystem fs = GetFileSystem();
fs.CreateFile("/file2", 0, CreateFileOptions.None);
fs.CreateFile("/file1", 0, CreateFileOptions.None);
Result rcDelete = fs.DeleteFile("/file2");
Result rcEntry1 = fs.GetEntryType(out DirectoryEntryType dir1Type, "/file1");
Result rcEntry2 = fs.GetEntryType(out _, "/file2");
Assert.True(rcDelete.IsSuccess());
Assert.True(rcEntry1.IsSuccess());
Assert.Equal(ResultFs.PathNotFound, rcEntry2);
Assert.Equal(DirectoryEntryType.File, dir1Type);
}
[Fact]
public void DeleteDirectory()
{
IAttributeFileSystem fs = GetFileSystem();
fs.CreateDirectory("/dir");
Result rcDelete = fs.DeleteDirectory("/dir");
Result rcEntry = fs.GetEntryType(out _, "/dir");
Assert.True(rcDelete.IsSuccess());
Assert.Equal(ResultFs.PathNotFound, rcEntry);
}
[Fact]
public void DeleteDirectoryWithSiblingA()
{
IAttributeFileSystem fs = GetFileSystem();
fs.CreateDirectory("/dir1");
fs.CreateDirectory("/dir2");
Result rcDelete = fs.DeleteDirectory("/dir2");
Result rcEntry1 = fs.GetEntryType(out DirectoryEntryType dir1Type, "/dir1");
Result rcEntry2 = fs.GetEntryType(out _, "/dir2");
Assert.True(rcDelete.IsSuccess());
Assert.True(rcEntry1.IsSuccess());
Assert.Equal(ResultFs.PathNotFound, rcEntry2);
Assert.Equal(DirectoryEntryType.Directory, dir1Type);
}
[Fact]
public void DeleteDirectoryWithSiblingB()
{
IAttributeFileSystem fs = GetFileSystem();
fs.CreateDirectory("/dir2");
fs.CreateDirectory("/dir1");
Result rcDelete = fs.DeleteDirectory("/dir2");
Result rcEntry1 = fs.GetEntryType(out DirectoryEntryType dir1Type, "/dir1");
Result rcEntry2 = fs.GetEntryType(out _, "/dir2");
Assert.True(rcDelete.IsSuccess());
Assert.True(rcEntry1.IsSuccess());
Assert.Equal(ResultFs.PathNotFound, rcEntry2);
Assert.Equal(DirectoryEntryType.Directory, dir1Type);
}
[Fact]
public void DeleteDirectoryWithChildren()
{
IAttributeFileSystem fs = GetFileSystem();
fs.CreateDirectory("/dir");
fs.CreateFile("/dir/file", 0, CreateFileOptions.None);
Result rc = fs.DeleteDirectory("/dir");
Assert.Equal(ResultFs.DirectoryNotEmpty, rc);
}
[Fact]
public void DeleteDirectoryRecursively()
{
IAttributeFileSystem fs = GetFileSystem();
fs.CreateDirectory("/dir");
fs.CreateDirectory("/dir/dir2");
fs.CreateFile("/dir/file1", 0, CreateFileOptions.None);
Result rcDelete = fs.DeleteDirectoryRecursively("/dir");
Result rcDir1Type = fs.GetEntryType(out _, "/dir");
Result rcDir2Type = fs.GetEntryType(out _, "/dir/dir2");
Result rcFileType = fs.GetEntryType(out _, "/dir/file1");
Assert.True(rcDelete.IsSuccess());
Assert.Equal(ResultFs.PathNotFound, rcDir1Type);
Assert.Equal(ResultFs.PathNotFound, rcDir2Type);
Assert.Equal(ResultFs.PathNotFound, rcFileType);
}
[Fact]
public void CleanDirectoryRecursively()
{
IAttributeFileSystem fs = GetFileSystem();
fs.CreateDirectory("/dir");
fs.CreateDirectory("/dir/dir2");
fs.CreateFile("/dir/file1", 0, CreateFileOptions.None);
Result rcDelete = fs.CleanDirectoryRecursively("/dir");
Result rcDir1Type = fs.GetEntryType(out DirectoryEntryType dir1Type, "/dir");
Result rcDir2Type = fs.GetEntryType(out _, "/dir/dir2");
Result rcFileType = fs.GetEntryType(out _, "/dir/file1");
Assert.True(rcDelete.IsSuccess());
Assert.True(rcDir1Type.IsSuccess());
Assert.Equal(DirectoryEntryType.Directory, dir1Type);
Assert.Equal(ResultFs.PathNotFound, rcDir2Type);
Assert.Equal(ResultFs.PathNotFound, rcFileType);
}
[Fact]
public void CreateFile()
{
IAttributeFileSystem fs = GetFileSystem();
fs.CreateFile("/file", 0, CreateFileOptions.None);
Result rc = fs.GetEntryType(out DirectoryEntryType type, "/file");
Assert.True(rc.IsSuccess());
Assert.Equal(DirectoryEntryType.File, type);
}
[Fact]
public void CreateFileWithTrailingSlash()
{
IAttributeFileSystem fs = GetFileSystem();
fs.CreateFile("/file/", 0, CreateFileOptions.None);
Result rc = fs.GetEntryType(out DirectoryEntryType type, "/file/");
Assert.True(rc.IsSuccess());
Assert.Equal(DirectoryEntryType.File, type);
}
[Fact]
public void CreateDirectory()
{
IAttributeFileSystem fs = GetFileSystem();
fs.CreateDirectory("/dir");
Result rc = fs.GetEntryType(out DirectoryEntryType type, "/dir");
Assert.True(rc.IsSuccess());
Assert.Equal(DirectoryEntryType.Directory, type);
}
[Fact]
public void CreateDirectoryWithTrailingSlash()
{
IAttributeFileSystem fs = GetFileSystem();
fs.CreateDirectory("/dir/");
Result rc = fs.GetEntryType(out DirectoryEntryType type, "/dir/");
Assert.True(rc.IsSuccess());
Assert.Equal(DirectoryEntryType.Directory, type);
}
[Fact]
public void CreateMultipleDirectories()
{
IAttributeFileSystem fs = GetFileSystem();
fs.CreateDirectory("/dir1");
fs.CreateDirectory("/dir2");
Result rc1 = fs.GetEntryType(out DirectoryEntryType type1, "/dir1");
Result rc2 = fs.GetEntryType(out DirectoryEntryType type2, "/dir2");
Assert.True(rc1.IsSuccess());
Assert.True(rc2.IsSuccess());
Assert.Equal(DirectoryEntryType.Directory, type1);
Assert.Equal(DirectoryEntryType.Directory, type2);
}
[Fact]
public void CreateMultipleNestedDirectories()
{
IAttributeFileSystem fs = GetFileSystem();
fs.CreateDirectory("/dir1");
fs.CreateDirectory("/dir2");
fs.CreateDirectory("/dir1/dir1a");
fs.CreateDirectory("/dir2/dir2a");
Result rc1 = fs.GetEntryType(out DirectoryEntryType type1, "/dir1/dir1a");
Result rc2 = fs.GetEntryType(out DirectoryEntryType type2, "/dir2/dir2a");
Assert.True(rc1.IsSuccess());
Assert.True(rc2.IsSuccess());
Assert.Equal(DirectoryEntryType.Directory, type1);
Assert.Equal(DirectoryEntryType.Directory, type2);
}
[Fact]
public void CreateDirectoryWithAttribute()
{
IAttributeFileSystem fs = GetFileSystem();
fs.CreateDirectory("/dir1", NxFileAttributes.None);
fs.CreateDirectory("/dir2", NxFileAttributes.Archive);
Result rc1 = fs.GetFileAttributes("/dir1", out NxFileAttributes type1);
Result rc2 = fs.GetFileAttributes("/dir2", out NxFileAttributes type2);
Assert.True(rc1.IsSuccess());
Assert.True(rc2.IsSuccess());
Assert.Equal(NxFileAttributes.Directory, type1);
Assert.Equal(NxFileAttributes.Directory | NxFileAttributes.Archive, type2);
}
[Fact]
public void RenameFile()
{
IAttributeFileSystem fs = GetFileSystem();
fs.CreateFile("/file1", 12345, CreateFileOptions.None);
Result rcRename = fs.RenameFile("/file1", "/file2");
Result rcOpen = fs.OpenFile(out IFile file, "/file2", OpenMode.All);
Result rcSize = file.GetSize(out long fileSize);
Result rcOldType = fs.GetEntryType(out _, "/file1");
Assert.True(rcRename.IsSuccess());
Assert.True(rcOpen.IsSuccess());
Assert.True(rcSize.IsSuccess());
Assert.Equal(12345, fileSize);
Assert.Equal(ResultFs.PathNotFound, rcOldType);
}
[Fact]
public void RenameFileWhenDestExists()
{
IAttributeFileSystem fs = GetFileSystem();
fs.CreateFile("/file1", 12345, CreateFileOptions.None);
fs.CreateFile("/file2", 54321, CreateFileOptions.None);
Result rcRename = fs.RenameFile("/file1", "/file2");
Result rcFile1 = fs.GetEntryType(out DirectoryEntryType file1Type, "/file1");
Result rcFile2 = fs.GetEntryType(out DirectoryEntryType file2Type, "/file2");
Assert.Equal(ResultFs.PathAlreadyExists, rcRename);
Assert.True(rcFile1.IsSuccess());
Assert.True(rcFile2.IsSuccess());
Assert.Equal(DirectoryEntryType.File, file1Type);
Assert.Equal(DirectoryEntryType.File, file2Type);
}
[Fact]
public void RenameDirectory()
{
IAttributeFileSystem fs = GetFileSystem();
fs.CreateDirectory("/dir1");
Result rcRename = fs.RenameDirectory("/dir1", "/dir2");
Result rcDir2 = fs.GetEntryType(out DirectoryEntryType dir2Type, "/dir2");
Result rcDir1 = fs.GetEntryType(out _, "/dir1");
Assert.True(rcRename.IsSuccess());
Assert.True(rcDir2.IsSuccess());
Assert.Equal(DirectoryEntryType.Directory, dir2Type);
Assert.Equal(ResultFs.PathNotFound, rcDir1);
}
[Fact]
public void RenameDirectoryWithChildren()
{
IAttributeFileSystem fs = GetFileSystem();
fs.CreateDirectory("/dir1");
fs.CreateDirectory("/dir1/dirC");
fs.CreateFile("/dir1/file1", 0, CreateFileOptions.None);
Result rcRename = fs.RenameDirectory("/dir1", "/dir2");
// Check that renamed structure exists
Result rcDir2 = fs.GetEntryType(out DirectoryEntryType dir2Type, "/dir2");
Result rcDirC = fs.GetEntryType(out DirectoryEntryType dir1CType, "/dir2/dirC");
Result rcFile1 = fs.GetEntryType(out DirectoryEntryType file1Type, "/dir2/file1");
// Check that old structure doesn't exist
Result rcDir1 = fs.GetEntryType(out _, "/dir1");
Result rcDirCOld = fs.GetEntryType(out _, "/dir1/dirC");
Result rcFile1Old = fs.GetEntryType(out _, "/dir1/file1");
Assert.True(rcRename.IsSuccess());
Assert.True(rcDir2.IsSuccess());
Assert.True(rcDirC.IsSuccess());
Assert.True(rcFile1.IsSuccess());
Assert.Equal(DirectoryEntryType.Directory, dir2Type);
Assert.Equal(DirectoryEntryType.Directory, dir1CType);
Assert.Equal(DirectoryEntryType.File, file1Type);
Assert.Equal(ResultFs.PathNotFound, rcDir1);
Assert.Equal(ResultFs.PathNotFound, rcDirCOld);
Assert.Equal(ResultFs.PathNotFound, rcFile1Old);
}
[Fact]
public void RenameDirectoryToDifferentParent()
{
IAttributeFileSystem fs = GetFileSystem();
fs.CreateDirectory("/parent1");
fs.CreateDirectory("/parent2");
fs.CreateDirectory("/parent1/dir1");
Result rcRename = fs.RenameDirectory("/parent1/dir1", "/parent2/dir2");
Result rcDir2 = fs.GetEntryType(out DirectoryEntryType dir2Type, "/parent2/dir2");
Result rcDir1 = fs.GetEntryType(out _, "/parent1/dir1");
Assert.True(rcRename.IsSuccess());
Assert.Equal(Result.Success, rcDir2);
Assert.True(rcDir2.IsSuccess());
Assert.Equal(DirectoryEntryType.Directory, dir2Type);
Assert.Equal(ResultFs.PathNotFound, rcDir1);
}
[Fact]
public void RenameDirectoryWhenDestExists()
{
IAttributeFileSystem fs = GetFileSystem();
fs.CreateDirectory("/dir1");
fs.CreateDirectory("/dir2");
Result rcRename = fs.RenameDirectory("/dir1", "/dir2");
Result rcDir1 = fs.GetEntryType(out DirectoryEntryType dir1Type, "/dir1");
Result rcDir2 = fs.GetEntryType(out DirectoryEntryType dir2Type, "/dir2");
Assert.Equal(ResultFs.PathAlreadyExists, rcRename);
Assert.True(rcDir1.IsSuccess());
Assert.True(rcDir2.IsSuccess());
Assert.Equal(DirectoryEntryType.Directory, dir1Type);
Assert.Equal(DirectoryEntryType.Directory, dir2Type);
}
[Fact]
public void SetFileSize()
{
IAttributeFileSystem fs = GetFileSystem();
fs.CreateFile("/file", 0, CreateFileOptions.None);
fs.OpenFile(out IFile file, "/file", OpenMode.All);
Result rc = file.SetSize(54321);
file.Dispose();
fs.OpenFile(out file, "/file", OpenMode.All);
file.GetSize(out long fileSize);
file.Dispose();
Assert.True(rc.IsSuccess());
Assert.Equal(54321, fileSize);
}
[Fact]
public void SetFileAttributes()
{
IAttributeFileSystem fs = GetFileSystem();
fs.CreateFile("/file", 0, CreateFileOptions.None);
Result rcSet = fs.SetFileAttributes("/file", NxFileAttributes.Archive);
Result rcGet = fs.GetFileAttributes("/file", out NxFileAttributes attributes);
Assert.True(rcSet.IsSuccess());
Assert.True(rcGet.IsSuccess());
Assert.Equal(NxFileAttributes.Archive, attributes);
}
[Fact]
public void IterateDirectory()
{
IAttributeFileSystem fs = GetFileSystem();
fs.CreateDirectory("/dir");
fs.CreateDirectory("/dir/dir1");
fs.CreateFile("/dir/dir1/file1", 0, CreateFileOptions.None);
fs.CreateFile("/dir/file1", 0, CreateFileOptions.None);
fs.CreateFile("/dir/file2", 0, CreateFileOptions.None);
Result rc = fs.OpenDirectory(out IDirectory dir, "/dir", OpenDirectoryMode.All);
Assert.True(rc.IsSuccess());
var entry1 = new DirectoryEntry();
var entry2 = new DirectoryEntry();
var entry3 = new DirectoryEntry();
var entry4 = new DirectoryEntry();
Assert.True(dir.Read(out long entriesRead1, SpanHelpers.AsSpan(ref entry1)).IsSuccess());
Assert.True(dir.Read(out long entriesRead2, SpanHelpers.AsSpan(ref entry2)).IsSuccess());
Assert.True(dir.Read(out long entriesRead3, SpanHelpers.AsSpan(ref entry3)).IsSuccess());
Assert.True(dir.Read(out long entriesRead4, SpanHelpers.AsSpan(ref entry4)).IsSuccess());
Assert.Equal(1, entriesRead1);
Assert.Equal(1, entriesRead2);
Assert.Equal(1, entriesRead3);
Assert.Equal(0, entriesRead4);
bool dir1Read = false;
bool file1Read = false;
bool file2Read = false;
// Entries are not guaranteed to be in any particular order
CheckEntry(ref entry1);
CheckEntry(ref entry2);
CheckEntry(ref entry3);
Assert.True(dir1Read);
Assert.True(file1Read);
Assert.True(file2Read);
void CheckEntry(ref DirectoryEntry entry)
{
switch (StringUtils.Utf8ZToString(entry.Name))
{
case "dir1":
Assert.False(dir1Read);
Assert.Equal(DirectoryEntryType.Directory, entry.Type);
dir1Read = true;
break;
case "file1":
Assert.False(file1Read);
Assert.Equal(DirectoryEntryType.File, entry.Type);
file1Read = true;
break;
case "file2":
Assert.False(file2Read);
Assert.Equal(DirectoryEntryType.File, entry.Type);
file2Read = true;
break;
default:
throw new ArgumentOutOfRangeException();
}
}
}
}
}

View File

@ -299,5 +299,29 @@ namespace LibHac.Tests
Assert.Equal(expected, actual);
}
public static object[][] GetLastSegmentTestItems =
{
new object[] {"/a/bb/ccc", "ccc"},
new object[] {"/a/bb/ccc/", "ccc"},
new object[] {"/a/bb", "bb"},
new object[] {"/a/bb/", "bb"},
new object[] {"/a", "a"},
new object[] {"/a/", "a"},
new object[] {"/", ""},
};
[Theory]
[MemberData(nameof(GetLastSegmentTestItems))]
public static void GetLastSegmentTest(string path, string expected)
{
var u8Path = path.ToU8String();
ReadOnlySpan<byte> fileName = PathTools.GetLastSegment(u8Path);
string actual = StringUtils.Utf8ZToString(fileName);
Assert.Equal(expected, actual);
}
}
}