diff --git a/src/LibHac/FsSystem/LayeredFileSystem.cs b/src/LibHac/FsSystem/LayeredFileSystem.cs
index 4798ba8a..4a7f24f4 100644
--- a/src/LibHac/FsSystem/LayeredFileSystem.cs
+++ b/src/LibHac/FsSystem/LayeredFileSystem.cs
@@ -1,13 +1,35 @@
using System;
using System.Collections.Generic;
+using LibHac.Common;
using LibHac.Fs;
namespace LibHac.FsSystem
{
public class LayeredFileSystem : FileSystemBase
{
+ ///
+ /// List of source s.
+ /// Filesystems at the beginning of the list will take precedence over those later in the list.
+ ///
private List Sources { get; } = new List();
+ ///
+ /// Creates a new from the input objects.
+ ///
+ /// The base .
+ /// The to be layered on top of the .
+ public LayeredFileSystem(IFileSystem lowerFileSystem, IFileSystem upperFileSystem)
+ {
+ Sources.Add(upperFileSystem);
+ Sources.Add(lowerFileSystem);
+ }
+
+ ///
+ /// Creates a new from the input objects.
+ ///
+ /// An containing the s
+ /// used to create the . Filesystems at the beginning of the list will take
+ /// precedence over those later in the list.
public LayeredFileSystem(IList sourceFileSystems)
{
Sources.AddRange(sourceFileSystems);
@@ -18,29 +40,71 @@ namespace LibHac.FsSystem
directory = default;
path = PathTools.Normalize(path);
- var dirs = new List();
+ // Open directories from all layers so they can be merged
+ // Only allocate the list for multiple sources if needed
+ List multipleSources = null;
+ IFileSystem singleSource = null;
foreach (IFileSystem fs in Sources)
{
Result rc = fs.GetEntryType(out DirectoryEntryType entryType, path);
- if (rc.IsFailure()) return rc;
- if (entryType == DirectoryEntryType.File && dirs.Count == 0)
+ if (rc.IsSuccess())
{
- ThrowHelper.ThrowResult(ResultFs.PathNotFound.Value);
+ // There were no directories with this path in higher levels, so the entry is a file
+ if (entryType == DirectoryEntryType.File && singleSource is null)
+ {
+ return ResultFs.PathNotFound.Log();
+ }
+
+ if (entryType == DirectoryEntryType.Directory)
+ {
+ if (singleSource is null)
+ {
+ singleSource = fs;
+ }
+ else if (multipleSources is null)
+ {
+ multipleSources = new List { singleSource, fs };
+ }
+ else
+ {
+ multipleSources.Add(fs);
+ }
+ }
}
-
- if (entryType == DirectoryEntryType.Directory)
+ else if (!ResultFs.PathNotFound.Includes(rc))
{
- rc = fs.OpenDirectory(out IDirectory subDirectory, path, mode);
- if (rc.IsFailure()) return rc;
-
- dirs.Add(subDirectory);
+ return rc;
}
}
- directory = new LayeredFileSystemDirectory(dirs);
- return Result.Success;
+ if (!(multipleSources is null))
+ {
+ var dir = new MergedDirectory(multipleSources, path, mode);
+ Result rc = dir.Initialize();
+
+ if (rc.IsSuccess())
+ {
+ directory = dir;
+ }
+
+ return rc;
+ }
+
+ if (!(singleSource is null))
+ {
+ Result rc = singleSource.OpenDirectory(out IDirectory dir, path, mode);
+
+ if (rc.IsSuccess())
+ {
+ directory = dir;
+ }
+
+ return rc;
+ }
+
+ return ResultFs.PathNotFound.Log();
}
protected override Result OpenFileImpl(out IFile file, string path, OpenMode mode)
@@ -51,16 +115,22 @@ namespace LibHac.FsSystem
foreach (IFileSystem fs in Sources)
{
Result rc = fs.GetEntryType(out DirectoryEntryType type, path);
- if (rc.IsFailure()) return rc;
- if (type == DirectoryEntryType.File)
+ if (rc.IsSuccess())
{
- return fs.OpenFile(out file, path, mode);
+ if (type == DirectoryEntryType.File)
+ {
+ return fs.OpenFile(out file, path, mode);
+ }
+
+ if (type == DirectoryEntryType.Directory)
+ {
+ return ResultFs.PathNotFound.Log();
+ }
}
-
- if (type == DirectoryEntryType.Directory)
+ else if (!ResultFs.PathNotFound.Includes(rc))
{
- return ResultFs.PathNotFound.Log();
+ return rc;
}
}
@@ -92,7 +162,7 @@ namespace LibHac.FsSystem
foreach (IFileSystem fs in Sources)
{
- Result getEntryResult = fs.GetEntryType(out DirectoryEntryType type, path);
+ Result getEntryResult = fs.GetEntryType(out _, path);
if (getEntryResult.IsSuccess())
{
@@ -110,7 +180,7 @@ namespace LibHac.FsSystem
foreach (IFileSystem fs in Sources)
{
- Result getEntryResult = fs.GetEntryType(out DirectoryEntryType type, path);
+ Result getEntryResult = fs.GetEntryType(out _, path);
if (getEntryResult.IsSuccess())
{
@@ -134,5 +204,95 @@ namespace LibHac.FsSystem
protected override Result DeleteFileImpl(string path) => ResultFs.UnsupportedOperation.Log();
protected override Result RenameDirectoryImpl(string oldPath, string newPath) => ResultFs.UnsupportedOperation.Log();
protected override Result RenameFileImpl(string oldPath, string newPath) => ResultFs.UnsupportedOperation.Log();
+
+ private class MergedDirectory : IDirectory
+ {
+ // Needed to open new directories for GetEntryCount
+ private List SourceFileSystems { get; }
+ private List SourceDirs { get; }
+ private string Path { get; }
+ private OpenDirectoryMode Mode { get; }
+
+ // todo: Efficient way to remove duplicates
+ private HashSet Names { get; } = new HashSet();
+
+ public MergedDirectory(List sourceFileSystems, string path, OpenDirectoryMode mode)
+ {
+ SourceFileSystems = sourceFileSystems;
+ SourceDirs = new List(sourceFileSystems.Count);
+ Path = path;
+ Mode = mode;
+ }
+
+ public Result Initialize()
+ {
+ foreach (IFileSystem fs in SourceFileSystems)
+ {
+ Result rc = fs.OpenDirectory(out IDirectory dir, Path, Mode);
+ if (rc.IsFailure()) return rc;
+
+ SourceDirs.Add(dir);
+ }
+
+ return Result.Success;
+ }
+
+ public Result Read(out long entriesRead, Span entryBuffer)
+ {
+ entriesRead = 0;
+ int entryIndex = 0;
+
+ for (int i = 0; i < SourceDirs.Count && entryIndex < entryBuffer.Length; i++)
+ {
+ long subEntriesRead;
+
+ do
+ {
+ Result rs = SourceDirs[i].Read(out subEntriesRead, entryBuffer.Slice(entryIndex, 1));
+ if (rs.IsFailure()) return rs;
+
+ if (subEntriesRead == 1 && Names.Add(StringUtils.Utf8ZToString(entryBuffer[entryIndex].Name)))
+ {
+ entryIndex++;
+ }
+ } while (subEntriesRead != 0 && entryIndex < entryBuffer.Length);
+ }
+
+ entriesRead = entryIndex;
+ return Result.Success;
+ }
+
+ public Result GetEntryCount(out long entryCount)
+ {
+ entryCount = 0;
+ long totalEntryCount = 0;
+ var entry = new DirectoryEntry();
+
+ // todo: Efficient way to remove duplicates
+ var names = new HashSet();
+
+ // Open new directories for each source because we need to remove duplicate entries
+ foreach (IFileSystem fs in SourceFileSystems)
+ {
+ Result rc = fs.OpenDirectory(out IDirectory dir, Path, Mode);
+ if (rc.IsFailure()) return rc;
+
+ long entriesRead;
+ do
+ {
+ dir.Read(out entriesRead, SpanHelpers.AsSpan(ref entry));
+ if (rc.IsFailure()) return rc;
+
+ if (entriesRead == 1 && names.Add(StringUtils.Utf8ZToString(entry.Name)))
+ {
+ totalEntryCount++;
+ }
+ } while (entriesRead != 0);
+ }
+
+ entryCount = totalEntryCount;
+ return Result.Success;
+ }
+ }
}
}
diff --git a/src/LibHac/FsSystem/LayeredFileSystemDirectory.cs b/src/LibHac/FsSystem/LayeredFileSystemDirectory.cs
deleted file mode 100644
index 244beeed..00000000
--- a/src/LibHac/FsSystem/LayeredFileSystemDirectory.cs
+++ /dev/null
@@ -1,52 +0,0 @@
-using System;
-using System.Collections.Generic;
-using LibHac.Fs;
-
-namespace LibHac.FsSystem
-{
- public class LayeredFileSystemDirectory : IDirectory
- {
- private List Sources { get; }
-
- public LayeredFileSystemDirectory(List sources)
- {
- Sources = sources;
- }
-
- // Todo: Don't return duplicate entries
- public Result Read(out long entriesRead, Span entryBuffer)
- {
- entriesRead = 0;
- int entryIndex = 0;
-
- for (int i = 0; i < Sources.Count && entryIndex < entryBuffer.Length; i++)
- {
- Result rs = Sources[i].Read(out long subEntriesRead, entryBuffer.Slice(entryIndex));
- if (rs.IsFailure()) return rs;
-
- entryIndex += (int)subEntriesRead;
- }
-
- entriesRead = entryIndex;
- return Result.Success;
- }
-
- // Todo: Don't count duplicate entries
- public Result GetEntryCount(out long entryCount)
- {
- entryCount = 0;
- long totalEntryCount = 0;
-
- foreach (IDirectory dir in Sources)
- {
- Result rc = dir.GetEntryCount(out long subEntryCount);
- if (rc.IsFailure()) return rc;
-
- totalEntryCount += subEntryCount;
- }
-
- entryCount = totalEntryCount;
- return Result.Success;
- }
- }
-}
diff --git a/tests/LibHac.Tests/Fs/IFileSystemTestBase/IFileSystemTests.IDirectory.cs b/tests/LibHac.Tests/Fs/IFileSystemTestBase/IFileSystemTests.IDirectory.cs
index 50caf866..ce7a9aa1 100644
--- a/tests/LibHac.Tests/Fs/IFileSystemTestBase/IFileSystemTests.IDirectory.cs
+++ b/tests/LibHac.Tests/Fs/IFileSystemTestBase/IFileSystemTests.IDirectory.cs
@@ -7,6 +7,29 @@ namespace LibHac.Tests.Fs.IFileSystemTestBase
{
public abstract partial class IFileSystemTests
{
+ [Fact]
+ public void IDirectoryRead_EmptyFs_NoEntriesAreRead()
+ {
+ IFileSystem fs = CreateFileSystem();
+ Span entries = stackalloc DirectoryEntry[1];
+
+ Assert.Success(fs.OpenDirectory(out IDirectory directory, "/", OpenDirectoryMode.All));
+
+ Assert.Success(directory.Read(out long entriesRead, entries));
+ Assert.Equal(0, entriesRead);
+ }
+
+ [Fact]
+ public void IDirectoryGetEntryCount_EmptyFs_EntryCountIsZero()
+ {
+ IFileSystem fs = CreateFileSystem();
+
+ Assert.Success(fs.OpenDirectory(out IDirectory directory, "/", OpenDirectoryMode.All));
+
+ Assert.Success(directory.GetEntryCount(out long entryCount));
+ Assert.Equal(0, entryCount);
+ }
+
[Fact]
public void IDirectoryRead_AllEntriesAreReturned()
{
diff --git a/tests/LibHac.Tests/Fs/LayeredFileSystemTests.cs b/tests/LibHac.Tests/Fs/LayeredFileSystemTests.cs
new file mode 100644
index 00000000..ff361aec
--- /dev/null
+++ b/tests/LibHac.Tests/Fs/LayeredFileSystemTests.cs
@@ -0,0 +1,195 @@
+using System;
+using LibHac.Common;
+using LibHac.Fs;
+using LibHac.FsSystem;
+using Xunit;
+
+namespace LibHac.Tests.Fs
+{
+ public class LayeredFileSystemTests
+ {
+ private IFileSystem CreateFileSystem()
+ {
+ var lowerLayerFs = new InMemoryFileSystem();
+ var upperLayerFs = new InMemoryFileSystem();
+
+ var layeredFs = new LayeredFileSystem(lowerLayerFs, upperLayerFs);
+
+ lowerLayerFs.CreateDirectory("/dir").ThrowIfFailure();
+ upperLayerFs.CreateDirectory("/dir").ThrowIfFailure();
+ lowerLayerFs.CreateDirectory("/dir2").ThrowIfFailure();
+ upperLayerFs.CreateDirectory("/dir2").ThrowIfFailure();
+ lowerLayerFs.CreateDirectory("/dir3").ThrowIfFailure();
+ upperLayerFs.CreateDirectory("/dir3").ThrowIfFailure();
+
+ lowerLayerFs.CreateDirectory("/lowerDir").ThrowIfFailure();
+ upperLayerFs.CreateDirectory("/upperDir").ThrowIfFailure();
+
+ lowerLayerFs.CreateFile("/dir/replacedFile", 1, CreateFileOptions.None).ThrowIfFailure();
+ upperLayerFs.CreateFile("/dir/replacedFile", 2, CreateFileOptions.None).ThrowIfFailure();
+
+ lowerLayerFs.CreateFile("/dir2/lowerFile", 0, CreateFileOptions.None).ThrowIfFailure();
+ upperLayerFs.CreateFile("/dir2/upperFile", 0, CreateFileOptions.None).ThrowIfFailure();
+
+ lowerLayerFs.CreateFile("/dir3/lowerFile", 0, CreateFileOptions.None).ThrowIfFailure();
+ upperLayerFs.CreateFile("/dir3/upperFile", 2, CreateFileOptions.None).ThrowIfFailure();
+ lowerLayerFs.CreateFile("/dir3/replacedFile", 1, CreateFileOptions.None).ThrowIfFailure();
+ upperLayerFs.CreateFile("/dir3/replacedFile", 2, CreateFileOptions.None).ThrowIfFailure();
+
+ lowerLayerFs.CreateFile("/replacedWithDir", 0, CreateFileOptions.None).ThrowIfFailure();
+ upperLayerFs.CreateDirectory("/replacedWithDir").ThrowIfFailure();
+ upperLayerFs.CreateFile("/replacedWithDir/subFile", 0, CreateFileOptions.None).ThrowIfFailure();
+
+ return layeredFs;
+ }
+
+ private IFileSystem CreateEmptyFileSystem()
+ {
+ var baseLayerFs = new InMemoryFileSystem();
+ var topLayerFs = new InMemoryFileSystem();
+
+ return new LayeredFileSystem(baseLayerFs, topLayerFs);
+ }
+
+ [Fact]
+ public void OpenFile_FileDoesNotExist_ReturnsPathNotFound()
+ {
+ IFileSystem fs = CreateFileSystem();
+
+ Assert.Result(ResultFs.PathNotFound, fs.OpenFile(out _, "/fakefile", OpenMode.All));
+ }
+
+ [Fact]
+ public void OpenFile_FileIsInBothSources_ReturnsFileFromTopSource()
+ {
+ IFileSystem fs = CreateFileSystem();
+
+ Assert.Success(fs.OpenFile(out IFile file, "/dir/replacedFile", OpenMode.All));
+ Assert.Success(file.GetSize(out long fileSize));
+
+ Assert.Equal(2, fileSize);
+ }
+
+ [Fact]
+ public void OpenFile_InsideMergedDirectory_CanOpenFilesFromBothSources()
+ {
+ IFileSystem fs = CreateFileSystem();
+
+ Assert.Success(fs.OpenFile(out _, "/dir2/lowerFile", OpenMode.All));
+ Assert.Success(fs.OpenFile(out _, "/dir2/upperFile", OpenMode.All));
+ }
+
+ [Fact]
+ public void OpenDirectory_DirDoesNotExist_ReturnsPathNotFound()
+ {
+ IFileSystem fs = CreateFileSystem();
+
+ Assert.Result(ResultFs.PathNotFound, fs.OpenDirectory(out _, "/fakedir", OpenDirectoryMode.All));
+ }
+
+ [Fact]
+ public void OpenDirectory_ExistsInSingleLayer_ReturnsNonMergedDirectory()
+ {
+ IFileSystem fs = CreateFileSystem();
+
+ Assert.Success(fs.OpenDirectory(out IDirectory dir, "/lowerDir", OpenDirectoryMode.All));
+ Assert.Equal(typeof(InMemoryFileSystem), dir.GetType().DeclaringType);
+ }
+
+ [Fact]
+ public void OpenDirectory_ExistsInMultipleLayers_ReturnsMergedDirectory()
+ {
+ IFileSystem fs = CreateFileSystem();
+
+ Assert.Success(fs.OpenDirectory(out IDirectory dir, "/dir", OpenDirectoryMode.All));
+ Assert.Equal(typeof(LayeredFileSystem), dir.GetType().DeclaringType);
+ }
+
+ [Fact]
+ public void GetEntryType_InsideMergedDirectory_CanGetEntryTypesFromBothSources()
+ {
+ IFileSystem fs = CreateFileSystem();
+
+ Assert.Success(fs.GetEntryType(out _, "/dir2/lowerFile"));
+ Assert.Success(fs.GetEntryType(out _, "/dir2/upperFile"));
+ }
+
+ [Fact]
+ public void IDirectoryRead_DuplicatedEntriesAreReturnedOnlyOnce()
+ {
+ IFileSystem fs = CreateFileSystem();
+ Span entries = stackalloc DirectoryEntry[4];
+
+ Assert.Success(fs.OpenDirectory(out IDirectory directory, "/dir3", OpenDirectoryMode.All));
+
+ Assert.Success(directory.Read(out long entriesRead, entries));
+ Assert.Equal(3, entriesRead);
+ }
+
+ [Fact]
+ public void IDirectoryRead_DuplicatedEntryReturnsFromTopLayer()
+ {
+ IFileSystem fs = CreateFileSystem();
+ var entry = new DirectoryEntry();
+
+ Assert.Success(fs.OpenDirectory(out IDirectory directory, "/dir", OpenDirectoryMode.All));
+
+ Assert.Success(directory.Read(out _, SpanHelpers.AsSpan(ref entry)));
+ Assert.Equal("replacedFile", StringUtils.Utf8ZToString(entry.Name));
+ Assert.Equal(2, entry.Size);
+ }
+
+ [Fact]
+ public void IDirectoryRead_EmptyFs_NoEntriesAreRead()
+ {
+ IFileSystem fs = CreateEmptyFileSystem();
+ var entry = new DirectoryEntry();
+
+ Assert.Success(fs.OpenDirectory(out IDirectory directory, "/", OpenDirectoryMode.All));
+
+ Assert.Success(directory.Read(out long entriesRead, SpanHelpers.AsSpan(ref entry)));
+ Assert.Equal(0, entriesRead);
+ }
+
+ [Fact]
+ public void IDirectoryGetEntryCount_DuplicatedEntriesAreCountedOnlyOnce()
+ {
+ IFileSystem fs = CreateFileSystem();
+
+ Assert.Success(fs.OpenDirectory(out IDirectory directory, "/dir3", OpenDirectoryMode.All));
+
+ Assert.Success(directory.GetEntryCount(out long entryCount));
+ Assert.Equal(3, entryCount);
+ }
+
+ [Fact]
+ public void IDirectoryGetEntryCount_MergedDirectoryAfterRead_AllEntriesAreCounted()
+ {
+ IFileSystem fs = CreateFileSystem();
+ var entry = new DirectoryEntry();
+
+ Assert.Success(fs.OpenDirectory(out IDirectory directory, "/dir3", OpenDirectoryMode.All));
+
+ // Read all entries
+ long entriesRead;
+ do
+ {
+ Assert.Success(directory.Read(out entriesRead, SpanHelpers.AsSpan(ref entry)));
+ } while (entriesRead != 0);
+
+ Assert.Success(directory.GetEntryCount(out long entryCount));
+ Assert.Equal(3, entryCount);
+ }
+
+ [Fact]
+ public void IDirectoryGetEntryCount_EmptyFs_EntryCountIsZero()
+ {
+ IFileSystem fs = CreateEmptyFileSystem();
+
+ Assert.Success(fs.OpenDirectory(out IDirectory directory, "/", OpenDirectoryMode.All));
+
+ Assert.Success(directory.GetEntryCount(out long entryCount));
+ Assert.Equal(0, entryCount);
+ }
+ }
+}