Merge pull request #127 from Thealexbarney/fs-shims

Add BIS and more BCAT FS client shims
- MountBis
- SetBisRootForHost
- OpenBisPartition
- MountBcatSaveData
This commit is contained in:
Alex Barney 2020-04-04 14:29:10 -07:00 committed by GitHub
commit 1b1e8e6780
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 461 additions and 15 deletions

View file

@ -12,8 +12,9 @@ namespace LibHac.Common
private int _length;
public bool Overflowed { get; private set; }
public int Length => _length;
public int Capacity => _buffer.Length - NullTerminatorLength;
public readonly int Length => _length;
public readonly int Capacity => _buffer.Length - NullTerminatorLength;
public readonly Span<byte> Buffer => _buffer;
public U8StringBuilder(Span<byte> buffer)
{
@ -62,12 +63,12 @@ namespace LibHac.Common
return this;
}
private bool HasCapacity(int requiredCapacity)
private readonly bool HasCapacity(int requiredCapacity)
{
return requiredCapacity <= Capacity;
}
private bool HasAdditionalCapacity(int requiredAdditionalCapacity)
private readonly bool HasAdditionalCapacity(int requiredAdditionalCapacity)
{
return HasCapacity(_length + requiredAdditionalCapacity);
}
@ -77,11 +78,11 @@ namespace LibHac.Common
_buffer[_length] = 0;
}
private void ThrowIfBufferLengthIsZero()
private readonly void ThrowIfBufferLengthIsZero()
{
if (_buffer.Length == 0) throw new ArgumentException("Buffer length must be greater than 0.");
}
public override string ToString() => StringUtils.Utf8ZToString(_buffer);
public override readonly string ToString() => StringUtils.Utf8ZToString(_buffer);
}
}

View file

@ -6,7 +6,7 @@ namespace LibHac.Fs
{
public class FileStorage2 : StorageBase
{
protected const long InvalidSize = -1;
protected const long SizeNotInitialized = -1;
private IFile BaseFile { get; set; }
protected long FileSize { get; set; }
@ -14,7 +14,7 @@ namespace LibHac.Fs
public FileStorage2(IFile baseFile)
{
BaseFile = baseFile;
FileSize = InvalidSize;
FileSize = SizeNotInitialized;
}
protected FileStorage2() { }
@ -29,7 +29,7 @@ namespace LibHac.Fs
private Result UpdateSize()
{
if (FileSize != InvalidSize)
if (FileSize != SizeNotInitialized)
return Result.Success;
Result rc = BaseFile.GetSize(out long fileSize);
@ -87,7 +87,7 @@ namespace LibHac.Fs
protected override Result SetSizeImpl(long size)
{
FileSize = InvalidSize;
FileSize = SizeNotInitialized;
return BaseFile.SetSize(size);
}

View file

@ -11,7 +11,7 @@ namespace LibHac.Fs
private FileStorageBasedFileSystem()
{
FileSize = InvalidSize;
FileSize = SizeNotInitialized;
}
public static Result CreateNew(out FileStorageBasedFileSystem created, IFileSystem baseFileSystem, U8Span path,

View file

@ -1,4 +1,5 @@
using System.Diagnostics;
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using LibHac.Common;
using static LibHac.Fs.PathTool;
@ -77,5 +78,33 @@ namespace LibHac.Fs
return ResultFs.TooLongPath.Log();
}
public static void Replace(Span<byte> buffer, byte oldChar, byte newChar)
{
for (int i = 0; i < buffer.Length; i++)
{
if (buffer[i] == oldChar)
{
buffer[i] = newChar;
}
}
}
/// <summary>
/// Performs the extra functions that nn::fs::FspPathPrintf does on the string buffer.
/// </summary>
/// <param name="builder">The string builder to process.</param>
/// <returns>The <see cref="Result"/> of the operation.</returns>
public static Result ToSfPath(in this U8StringBuilder builder)
{
if (builder.Overflowed)
return ResultFs.TooLongPath.Log();
Replace(builder.Buffer.Slice(builder.Capacity),
StringTraits.AltDirectorySeparator,
StringTraits.DirectorySeparator);
return Result.Success;
}
}
}

View file

@ -0,0 +1,56 @@
using System;
using LibHac.Common;
using LibHac.FsService;
using LibHac.Ncm;
namespace LibHac.Fs.Shim
{
public static class BcatSaveData
{
public static Result MountBcatSaveData(this FileSystemClient fs, U8Span mountName, TitleId applicationId)
{
Result rc;
if (fs.IsEnabledAccessLog(AccessLogTarget.System))
{
TimeSpan startTime = fs.Time.GetCurrent();
rc = MountBcatSaveDataImpl(fs, mountName, applicationId);
TimeSpan endTime = fs.Time.GetCurrent();
string logMessage = $", name: \"{mountName.ToString()}\", applicationid: 0x{applicationId}\"";
fs.OutputAccessLog(rc, startTime, endTime, logMessage);
}
else
{
rc = MountBcatSaveDataImpl(fs, mountName, applicationId);
}
if (rc.IsFailure()) return rc;
if (fs.IsEnabledAccessLog(AccessLogTarget.System))
{
fs.EnableFileSystemAccessorAccessLog(mountName);
}
return Result.Success;
}
private static Result MountBcatSaveDataImpl(FileSystemClient fs, U8Span mountName, TitleId applicationId)
{
Result rc = MountHelpers.CheckMountName(mountName);
if (rc.IsFailure()) return rc;
IFileSystemProxy fsProxy = fs.GetFileSystemProxyServiceObject();
SaveDataAttribute attribute = default;
attribute.TitleId = applicationId;
attribute.Type = SaveDataType.Bcat;
rc = fsProxy.OpenSaveDataFileSystem(out IFileSystem fileSystem, SaveDataSpaceId.User, ref attribute);
if (rc.IsFailure()) return rc;
return fs.Register(mountName, fileSystem);
}
}
}

178
src/LibHac/Fs/Shim/Bis.cs Normal file
View file

@ -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<byte> 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();
}
}
}

View file

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

View file

@ -0,0 +1,51 @@
using LibHac.Common;
using LibHac.Fs;
using LibHac.Fs.Shim;
using LibHac.Ncm;
using Xunit;
namespace LibHac.Tests.Fs.FileSystemClientTests.ShimTests
{
public class BcatSaveData
{
[Fact]
public void MountBcatSaveData_SaveDoesNotExist_ReturnsTargetNotFound()
{
var applicationId = new TitleId(1);
FileSystemClient fs = FileSystemServerFactory.CreateClient(true);
Assert.Result(ResultFs.TargetNotFound, fs.MountBcatSaveData("bcat_test".ToU8Span(), applicationId));
}
[Fact]
public void MountBcatSaveData_SaveExists_ReturnsSuccess()
{
var applicationId = new TitleId(1);
FileSystemClient fs = FileSystemServerFactory.CreateClient(true);
Assert.Success(fs.CreateBcatSaveData(applicationId, 0x400000));
Assert.Success(fs.MountBcatSaveData("bcat_test".ToU8Span(), applicationId));
}
[Fact]
public void MountBcatSaveData_WrittenDataPersists()
{
var applicationId = new TitleId(1);
FileSystemClient fs = FileSystemServerFactory.CreateClient(true);
Assert.Success(fs.CreateBcatSaveData(applicationId, 0x400000));
Assert.Success(fs.MountBcatSaveData("bcat_test".ToU8Span(), applicationId));
// Check that the path doesn't exist
Assert.Result(ResultFs.PathNotFound, fs.GetEntryType(out _, "bcat_test:/file".ToU8Span()));
fs.CreateFile("bcat_test:/file".ToU8Span(), 0);
fs.Commit("bcat_test".ToU8Span());
fs.Unmount("bcat_test".ToU8Span());
Assert.Success(fs.MountBcatSaveData("bcat_test".ToU8Span(), applicationId));
Assert.Success(fs.GetEntryType(out DirectoryEntryType type, "bcat_test:/file".ToU8Span()));
Assert.Equal(DirectoryEntryType.File, type);
}
}
}

View file

@ -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));
}
}
}

View file

@ -36,6 +36,7 @@ namespace LibHac.Tests.Fs.FileSystemClientTests.ShimTests
Assert.Success(fs.GetEntryType(out DirectoryEntryType type, "cache:/file".ToU8Span()));
Assert.Equal(DirectoryEntryType.File, type);
}
[Fact]
public void MountCacheStorage_SdCardIsPreferredOverBis()
{

View file

@ -84,5 +84,33 @@ namespace LibHac.Tests.Fs.FileSystemClientTests.ShimTests
Assert.Equal(expectedIndexes, actualIndexes);
}
[Fact]
public void CreateBcatSaveData_DoesNotExist_SaveIsCreated()
{
var applicationId = new TitleId(1);
FileSystemClient fs = FileSystemServerFactory.CreateClient(true);
Assert.Success(fs.CreateBcatSaveData(applicationId, 0x400000));
fs.OpenSaveDataIterator(out SaveDataIterator iterator, SaveDataSpaceId.User);
var info = new SaveDataInfo[2];
iterator.ReadSaveDataInfo(out long entriesRead, info);
Assert.Equal(1, entriesRead);
Assert.Equal(applicationId, info[0].TitleId);
Assert.Equal(SaveDataType.Bcat, info[0].Type);
}
[Fact]
public void CreateBcatSaveData_AlreadyExists_ReturnsPathAlreadyExists()
{
var applicationId = new TitleId(1);
FileSystemClient fs = FileSystemServerFactory.CreateClient(true);
Assert.Success(fs.CreateBcatSaveData(applicationId, 0x400000));
Assert.Result(ResultFs.PathAlreadyExists, fs.CreateBcatSaveData(applicationId, 0x400000));
}
}
}