diff --git a/src/LibHac/Fs/Shim/Bis.cs b/src/LibHac/Fs/Shim/Bis.cs new file mode 100644 index 00000000..cd152d02 --- /dev/null +++ b/src/LibHac/Fs/Shim/Bis.cs @@ -0,0 +1,178 @@ +using System; +using System.Diagnostics; +using LibHac.Common; +using LibHac.FsService; +using LibHac.FsSystem; +using static LibHac.Fs.CommonMountNames; + +namespace LibHac.Fs.Shim +{ + public static class Bis + { + private class BisCommonMountNameGenerator : ICommonMountNameGenerator + { + private BisPartitionId PartitionId { get; } + + public BisCommonMountNameGenerator(BisPartitionId partitionId) + { + PartitionId = partitionId; + } + + public Result GenerateCommonMountName(Span nameBuffer) + { + U8Span mountName = GetBisMountName(PartitionId); + + // Add 2 for the mount name separator and null terminator + // ReSharper disable once RedundantAssignment + int requiredNameBufferSize = StringUtils.GetLength(mountName, PathTools.MountNameLengthMax) + 2; + + Debug.Assert(nameBuffer.Length >= requiredNameBufferSize); + + // ReSharper disable once RedundantAssignment + int size = new U8StringBuilder(nameBuffer).Append(mountName).Append(StringTraits.DriveSeparator).Length; + Debug.Assert(size == requiredNameBufferSize - 1); + + return Result.Success; + } + } + + public static Result MountBis(this FileSystemClient fs, U8Span mountName, BisPartitionId partitionId) + { + return MountBis(fs, mountName, partitionId, default); + } + + public static Result MountBis(this FileSystemClient fs, BisPartitionId partitionId, U8Span rootPath) + { + return MountBis(fs, GetBisMountName(partitionId), partitionId, rootPath); + } + + // nn::fs::detail::MountBis + private static Result MountBis(FileSystemClient fs, U8Span mountName, BisPartitionId partitionId, U8Span rootPath) + { + Result rc; + + if (fs.IsEnabledAccessLog(AccessLogTarget.System)) + { + TimeSpan startTime = fs.Time.GetCurrent(); + rc = MountBisImpl(fs, mountName, partitionId, rootPath); + TimeSpan endTime = fs.Time.GetCurrent(); + + string logMessage = $", name: \"{mountName.ToString()}\", bispartitionid: {partitionId}, path: \"{rootPath.ToString()}\""; + + fs.OutputAccessLog(rc, startTime, endTime, logMessage); + } + else + { + rc = MountBisImpl(fs, mountName, partitionId, rootPath); + } + + if (rc.IsFailure()) return rc; + + if (fs.IsEnabledAccessLog(AccessLogTarget.System)) + { + fs.EnableFileSystemAccessorAccessLog(mountName); + } + + return Result.Success; + } + + // ReSharper disable once UnusedParameter.Local + private static Result MountBisImpl(FileSystemClient fs, U8Span mountName, BisPartitionId partitionId, U8Span rootPath) + { + Result rc = MountHelpers.CheckMountNameAcceptingReservedMountName(mountName); + if (rc.IsFailure()) return rc; + + FsPath sfPath; + unsafe { _ = &sfPath; } // workaround for CS0165 + + IFileSystemProxy fsProxy = fs.GetFileSystemProxyServiceObject(); + + // Nintendo doesn't use the provided rootPath + sfPath.Str[0] = 0; + + rc = fsProxy.OpenBisFileSystem(out IFileSystem fileSystem, ref sfPath, partitionId); + if (rc.IsFailure()) return rc; + + var nameGenerator = new BisCommonMountNameGenerator(partitionId); + + return fs.Register(mountName, fileSystem, nameGenerator); + } + + public static U8Span GetBisMountName(BisPartitionId partitionId) + { + switch (partitionId) + { + case BisPartitionId.BootPartition1Root: + case BisPartitionId.BootPartition2Root: + case BisPartitionId.UserDataRoot: + case BisPartitionId.BootConfigAndPackage2Part1: + case BisPartitionId.BootConfigAndPackage2Part2: + case BisPartitionId.BootConfigAndPackage2Part3: + case BisPartitionId.BootConfigAndPackage2Part4: + case BisPartitionId.BootConfigAndPackage2Part5: + case BisPartitionId.BootConfigAndPackage2Part6: + case BisPartitionId.CalibrationBinary: + throw new HorizonResultException(default, "The partition specified is not mountable."); + + case BisPartitionId.CalibrationFile: + return BisCalibrationFilePartitionMountName; + case BisPartitionId.SafeMode: + return BisSafeModePartitionMountName; + case BisPartitionId.User: + return BisUserPartitionMountName; + case BisPartitionId.System: + return BisSystemPartitionMountName; + + default: + throw new ArgumentOutOfRangeException(nameof(partitionId), partitionId, null); + } + } + + // todo: Decide how to handle SetBisRootForHost since it allows mounting any directory on the user's computer + public static Result SetBisRootForHost(this FileSystemClient fs, BisPartitionId partitionId, U8Span rootPath) + { + FsPath sfPath; + unsafe { _ = &sfPath; } // workaround for CS0165 + + int pathLen = StringUtils.GetLength(rootPath, PathTools.MaxPathLength + 1); + if (pathLen > PathTools.MaxPathLength) + return ResultFs.TooLongPath.Log(); + + if (pathLen > 0) + { + byte endingSeparator = PathTool.IsSeparator(rootPath[pathLen - 1]) + ? StringTraits.NullTerminator + : StringTraits.DirectorySeparator; + + Result rc = new U8StringBuilder(sfPath.Str).Append(rootPath).Append(endingSeparator).ToSfPath(); + if (rc.IsFailure()) return rc; + } + else + { + sfPath.Str[0] = StringTraits.NullTerminator; + } + + IFileSystemProxy fsProxy = fs.GetFileSystemProxyServiceObject(); + + return fsProxy.SetBisRootForHost(partitionId, ref sfPath); + } + + public static Result OpenBisPartition(this FileSystemClient fs, out IStorage partitionStorage, BisPartitionId partitionId) + { + partitionStorage = default; + + IFileSystemProxy fsProxy = fs.GetFileSystemProxyServiceObject(); + Result rc = fsProxy.OpenBisStorage(out IStorage storage, partitionId); + if (rc.IsFailure()) return rc; + + partitionStorage = storage; + return Result.Success; + } + + public static Result InvalidateBisCache(this FileSystemClient fs) + { + IFileSystemProxy fsProxy = fs.GetFileSystemProxyServiceObject(); + return fsProxy.InvalidateBisCache(); + } + } +} diff --git a/tests/LibHac.Tests/Fs/FileSystemClientTests/FileSystemServerFactory.cs b/tests/LibHac.Tests/Fs/FileSystemClientTests/FileSystemServerFactory.cs index aeb285b1..4d95b7ea 100644 --- a/tests/LibHac.Tests/Fs/FileSystemClientTests/FileSystemServerFactory.cs +++ b/tests/LibHac.Tests/Fs/FileSystemClientTests/FileSystemServerFactory.cs @@ -5,9 +5,9 @@ namespace LibHac.Tests.Fs.FileSystemClientTests { public static class FileSystemServerFactory { - public static FileSystemServer CreateServer(bool sdCardInserted) + public static FileSystemServer CreateServer(bool sdCardInserted, out IFileSystem rootFs) { - var rootFs = new InMemoryFileSystem(); + rootFs = new InMemoryFileSystem(); var defaultObjects = DefaultFsServerObjects.GetDefaultEmulatedCreators(rootFs, new Keyset()); @@ -25,7 +25,14 @@ namespace LibHac.Tests.Fs.FileSystemClientTests public static FileSystemClient CreateClient(bool sdCardInserted) { - FileSystemServer fsServer = CreateServer(sdCardInserted); + FileSystemServer fsServer = CreateServer(sdCardInserted, out _); + + return fsServer.CreateFileSystemClient(); + } + + public static FileSystemClient CreateClient(out IFileSystem rootFs) + { + FileSystemServer fsServer = CreateServer(false, out rootFs); return fsServer.CreateFileSystemClient(); } diff --git a/tests/LibHac.Tests/Fs/FileSystemClientTests/ShimTests/Bis.cs b/tests/LibHac.Tests/Fs/FileSystemClientTests/ShimTests/Bis.cs new file mode 100644 index 00000000..9c41f8e2 --- /dev/null +++ b/tests/LibHac.Tests/Fs/FileSystemClientTests/ShimTests/Bis.cs @@ -0,0 +1,95 @@ +using LibHac.Common; +using LibHac.Fs; +using LibHac.Fs.Shim; +using Xunit; + +namespace LibHac.Tests.Fs.FileSystemClientTests.ShimTests +{ + public class Bis + { + [Fact] + public void MountBis_MountCalibrationPartition_OpensCorrectDirectory() + { + FileSystemClient fs = FileSystemServerFactory.CreateClient(out IFileSystem rootFs); + + Assert.Success(fs.MountBis("calib".ToU8Span(), BisPartitionId.CalibrationFile)); + + // Create a file in the opened file system + Assert.Success(fs.CreateFile("calib:/file".ToU8Span(), 0)); + + // Make sure the file exists on the root file system + Assert.Success(rootFs.GetEntryType(out DirectoryEntryType type, "/bis/cal/file".ToU8Span())); + Assert.Equal(DirectoryEntryType.File, type); + } + [Fact] + public void MountBis_MountSafePartition_OpensCorrectDirectory() + { + FileSystemClient fs = FileSystemServerFactory.CreateClient(out IFileSystem rootFs); + + Assert.Success(fs.MountBis("safe".ToU8Span(), BisPartitionId.SafeMode)); + + // Create a file in the opened file system + Assert.Success(fs.CreateFile("safe:/file".ToU8Span(), 0)); + + // Make sure the file exists on the root file system + Assert.Success(rootFs.GetEntryType(out DirectoryEntryType type, "/bis/safe/file".ToU8Span())); + Assert.Equal(DirectoryEntryType.File, type); + } + + [Fact] + public void MountBis_MountSystemPartition_OpensCorrectDirectory() + { + FileSystemClient fs = FileSystemServerFactory.CreateClient(out IFileSystem rootFs); + + Assert.Success(fs.MountBis("system".ToU8Span(), BisPartitionId.System)); + + // Create a file in the opened file system + Assert.Success(fs.CreateFile("system:/file".ToU8Span(), 0)); + + // Make sure the file exists on the root file system + Assert.Success(rootFs.GetEntryType(out DirectoryEntryType type, "/bis/system/file".ToU8Span())); + Assert.Equal(DirectoryEntryType.File, type); + } + + [Fact] + public void MountBis_MountUserPartition_OpensCorrectDirectory() + { + FileSystemClient fs = FileSystemServerFactory.CreateClient(out IFileSystem rootFs); + + Assert.Success(fs.MountBis("user".ToU8Span(), BisPartitionId.User)); + + // Create a file in the opened file system + Assert.Success(fs.CreateFile("user:/file".ToU8Span(), 0)); + + // Make sure the file exists on the root file system + Assert.Success(rootFs.GetEntryType(out DirectoryEntryType type, "/bis/user/file".ToU8Span())); + Assert.Equal(DirectoryEntryType.File, type); + } + + [Fact] + public void MountBis_WithRootPath_IgnoresRootPath() + { + FileSystemClient fs = FileSystemServerFactory.CreateClient(out IFileSystem rootFs); + + Assert.Success(fs.MountBis(BisPartitionId.User, "/sub".ToU8Span())); + + // Create a file in the opened file system + Assert.Success(fs.CreateFile("@User:/file".ToU8Span(), 0)); + + // Make sure the file wasn't created in the sub path + Assert.Result(ResultFs.PathNotFound, rootFs.GetEntryType(out _, "/bis/user/sub/file".ToU8Span())); + + // Make sure the file was created in the main path + Assert.Success(rootFs.GetEntryType(out DirectoryEntryType type, "/bis/user/file".ToU8Span())); + Assert.Equal(DirectoryEntryType.File, type); + } + + [Fact] + public void MountBis_InvalidPartition_ReturnsInvalidArgument() + { + FileSystemClient fs = FileSystemServerFactory.CreateClient(out IFileSystem rootFs); + + Assert.Result(ResultFs.InvalidArgument, fs.MountBis("boot1".ToU8Span(), BisPartitionId.BootPartition1Root)); + } + } +}