diff --git a/src/LibHac/Common/U8StringBuilder.cs b/src/LibHac/Common/U8StringBuilder.cs index cbaed2f1..ba78f569 100644 --- a/src/LibHac/Common/U8StringBuilder.cs +++ b/src/LibHac/Common/U8StringBuilder.cs @@ -12,6 +12,7 @@ namespace LibHac.Common private int _length; public bool Overflowed { get; private set; } + public int Length => _length; public int Capacity => _buffer.Length - NullTerminatorLength; public U8StringBuilder(Span buffer) diff --git a/src/LibHac/Fs/CommonMountNames.cs b/src/LibHac/Fs/CommonMountNames.cs index ff6cf258..c26e0a47 100644 --- a/src/LibHac/Fs/CommonMountNames.cs +++ b/src/LibHac/Fs/CommonMountNames.cs @@ -4,6 +4,8 @@ namespace LibHac.Fs { internal static class CommonMountNames { + public const char ReservedMountNamePrefixCharacter = '@'; + public static readonly U8String GameCardFileSystemMountName = new U8String("@Gc"); public static readonly U8String ContentStorageSystemMountName = new U8String("@SystemContent"); public static readonly U8String ContentStorageUserMountName = new U8String("@UserContent"); diff --git a/src/LibHac/Fs/FileSystemClient.AccessLog.cs b/src/LibHac/Fs/FileSystemClient.AccessLog.cs index bc1b988f..8ade3eaa 100644 --- a/src/LibHac/Fs/FileSystemClient.AccessLog.cs +++ b/src/LibHac/Fs/FileSystemClient.AccessLog.cs @@ -150,6 +150,30 @@ namespace LibHac.Fs OutputAccessLogImpl(result, startTime, endTime, handle.GetId(), message, caller); } + internal void OutputAccessLogUnlessResultSuccess(Result result, TimeSpan startTime, TimeSpan endTime, string message, [CallerMemberName] string caller = "") + { + if (result.IsFailure()) + { + OutputAccessLogImpl(result, startTime, endTime, 0, message, caller); + } + } + + internal void OutputAccessLogUnlessResultSuccess(Result result, TimeSpan startTime, TimeSpan endTime, FileHandle handle, string message, [CallerMemberName] string caller = "") + { + if (result.IsFailure()) + { + OutputAccessLogImpl(result, startTime, endTime, handle.GetId(), message, caller); + } + } + + internal void OutputAccessLogUnlessResultSuccess(Result result, TimeSpan startTime, TimeSpan endTime, DirectoryHandle handle, string message, [CallerMemberName] string caller = "") + { + if (result.IsFailure()) + { + OutputAccessLogImpl(result, startTime, endTime, handle.GetId(), message, caller); + } + } + internal void OutputAccessLogImpl(Result result, TimeSpan startTime, TimeSpan endTime, int handleId, string message, [CallerMemberName] string caller = "") { @@ -208,6 +232,27 @@ namespace LibHac.Fs return rc; } + + public Result RunOperationWithAccessLogOnFailure(AccessLogTarget logTarget, Func operation, + Func textGenerator, [CallerMemberName] string caller = "") + { + Result rc; + + if (IsEnabledAccessLog(logTarget)) + { + TimeSpan startTime = Time.GetCurrent(); + rc = operation(); + TimeSpan endTime = Time.GetCurrent(); + + OutputAccessLogUnlessResultSuccess(rc, startTime, endTime, textGenerator(), caller); + } + else + { + rc = operation(); + } + + return rc; + } } [Flags] diff --git a/src/LibHac/Fs/FsEnums.cs b/src/LibHac/Fs/FsEnums.cs index 64534def..941cf84c 100644 --- a/src/LibHac/Fs/FsEnums.cs +++ b/src/LibHac/Fs/FsEnums.cs @@ -187,4 +187,10 @@ namespace LibHac.Fs Nand = 1, SdCard = 2 } + + public enum MountHostOption + { + None = 0, + PseudoCaseSensitive = 1 + } } diff --git a/src/LibHac/Fs/MountHelpers.cs b/src/LibHac/Fs/MountHelpers.cs index a7d49654..a91be5bb 100644 --- a/src/LibHac/Fs/MountHelpers.cs +++ b/src/LibHac/Fs/MountHelpers.cs @@ -1,4 +1,5 @@ using LibHac.Common; +using static LibHac.Fs.CommonMountNames; namespace LibHac.Fs { @@ -23,6 +24,11 @@ namespace LibHac.Fs return Result.Success; } + public static bool IsReservedMountName(U8Span name) + { + return (uint)name.Length > 0 && name[0] == ReservedMountNamePrefixCharacter; + } + // ReSharper disable once UnusedParameter.Local private static bool CheckMountNameImpl(U8Span name) { diff --git a/src/LibHac/Fs/Shim/Host.cs b/src/LibHac/Fs/Shim/Host.cs new file mode 100644 index 00000000..e45bb44b --- /dev/null +++ b/src/LibHac/Fs/Shim/Host.cs @@ -0,0 +1,304 @@ +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using LibHac.Common; +using LibHac.FsService; +using LibHac.FsSystem; +using static LibHac.Fs.CommonMountNames; + +namespace LibHac.Fs.Shim +{ + public static class Host + { + private static ReadOnlySpan HostRootFileSystemPath => new[] + {(byte) '@', (byte) 'H', (byte) 'o', (byte) 's', (byte) 't', (byte) ':', (byte) '/'}; + + private const int HostRootFileSystemPathLength = 8; + + private class HostCommonMountNameGenerator : ICommonMountNameGenerator + { + private FsPath _path; + + public HostCommonMountNameGenerator(U8Span path) + { + StringUtils.Copy(_path.Str, path); + + int pathLength = StringUtils.GetLength(_path.Str); + if (pathLength != 0 && _path.Str[pathLength - 1] == StringTraits.DirectorySeparator) + { + _path.Str[pathLength - 1] = StringTraits.NullTerminator; + } + } + + public Result GenerateCommonMountName(Span nameBuffer) + { + int requiredNameBufferSize = StringUtils.GetLength(_path.Str, FsPath.MaxLength) + HostRootFileSystemPathLength; + + if (nameBuffer.Length < requiredNameBufferSize) + return ResultFs.TooLongPath.Log(); + + int size = new U8StringBuilder(nameBuffer).Append(HostRootFileSystemPath).Append(_path.Str).Length; + Debug.Assert(size == requiredNameBufferSize - 1); + + return Result.Success; + } + } + + private class HostRootCommonMountNameGenerator : ICommonMountNameGenerator + { + public Result GenerateCommonMountName(Span nameBuffer) + { + const int requiredNameBufferSize = HostRootFileSystemPathLength; + + Debug.Assert(nameBuffer.Length >= requiredNameBufferSize); + + int size = StringUtils.Copy(nameBuffer, HostRootFileSystemPath); + Debug.Assert(size == requiredNameBufferSize - 1); + + return Result.Success; + } + } + + public static Result MountHostRoot(this FileSystemClient fs) + { + IFileSystem hostFileSystem = default; + var path = new FsPath(); + path.Str[0] = 0; + + static string LogMessageGenerator() => $", name: \"{HostRootFileSystemMountName.ToString()}\""; + + Result OpenHostFs() => OpenHostFileSystemImpl(fs, out hostFileSystem, ref path, MountHostOption.None); + + Result MountHostFs() => fs.Register(HostRootFileSystemMountName, hostFileSystem, + new HostRootCommonMountNameGenerator()); + + // Open the host file system + Result result = + fs.RunOperationWithAccessLogOnFailure(AccessLogTarget.Application, OpenHostFs, LogMessageGenerator); + if (result.IsFailure()) return result; + + // Mount the host file system + result = fs.RunOperationWithAccessLog(AccessLogTarget.Application, MountHostFs, LogMessageGenerator); + if (result.IsFailure()) return result; + + if (fs.IsEnabledAccessLog(AccessLogTarget.Application)) + fs.EnableFileSystemAccessorAccessLog(HostRootFileSystemMountName); + + return Result.Success; + } + + public static Result MountHostRoot(this FileSystemClient fs, MountHostOption option) + { + IFileSystem hostFileSystem = default; + var path = new FsPath(); + path.Str[0] = 0; + + string LogMessageGenerator() => + $", name: \"{HostRootFileSystemMountName.ToString()}, mount_host_option: {option}\""; + + Result OpenHostFs() => OpenHostFileSystemImpl(fs, out hostFileSystem, ref path, option); + + Result MountHostFs() => fs.Register(HostRootFileSystemMountName, hostFileSystem, + new HostRootCommonMountNameGenerator()); + + // Open the host file system + Result result = + fs.RunOperationWithAccessLogOnFailure(AccessLogTarget.Application, OpenHostFs, LogMessageGenerator); + if (result.IsFailure()) return result; + + // Mount the host file system + result = fs.RunOperationWithAccessLog(AccessLogTarget.Application, MountHostFs, LogMessageGenerator); + if (result.IsFailure()) return result; + + if (fs.IsEnabledAccessLog(AccessLogTarget.Application)) + fs.EnableFileSystemAccessorAccessLog(HostRootFileSystemMountName); + + return Result.Success; + } + + public static void UnmountHostRoot(this FileSystemClient fs) + { + fs.Unmount(HostRootFileSystemMountName); + } + + public static Result MountHost(this FileSystemClient fs, U8Span mountName, U8Span path) + { + return MountHostImpl(fs, mountName, path, null); + } + + public static Result MountHost(this FileSystemClient fs, U8Span mountName, U8Span path, MountHostOption option) + { + return MountHostImpl(fs, mountName, path, option); + } + + private static Result MountHostImpl(this FileSystemClient fs, U8Span mountName, U8Span path, + MountHostOption? optionalOption, [CallerMemberName] string caller = "") + { + Result rc; + ICommonMountNameGenerator nameGenerator; + + string logMessage = null; + var option = MountHostOption.None; + + // Set the mount option if it was specified + if (optionalOption.HasValue) + { + option = optionalOption.Value; + } + + if (fs.IsEnabledAccessLog(AccessLogTarget.Application)) + { + if (optionalOption.HasValue) + { + logMessage = $", name: \"{mountName.ToString()}\", mount_host_option: {option}"; + } + else + { + logMessage = $", name: \"{mountName.ToString()}\""; + } + + TimeSpan startTime = fs.Time.GetCurrent(); + rc = PreMountHost(out nameGenerator, mountName, path); + TimeSpan endTime = fs.Time.GetCurrent(); + + fs.OutputAccessLogUnlessResultSuccess(rc, startTime, endTime, logMessage, caller); + } + else + { + rc = PreMountHost(out nameGenerator, mountName, path); + } + + if (rc.IsFailure()) return rc; + + IFileSystem hostFileSystem; + + if (fs.IsEnabledAccessLog(AccessLogTarget.Application)) + { + TimeSpan startTime = fs.Time.GetCurrent(); + rc = OpenHostFileSystem(fs, out hostFileSystem, mountName, path, option); + TimeSpan endTime = fs.Time.GetCurrent(); + + fs.OutputAccessLogUnlessResultSuccess(rc, startTime, endTime, logMessage, caller); + } + else + { + rc = OpenHostFileSystem(fs, out hostFileSystem, mountName, path, option); + } + + if (rc.IsFailure()) return rc; + + if (fs.IsEnabledAccessLog(AccessLogTarget.Application)) + { + TimeSpan startTime = fs.Time.GetCurrent(); + rc = fs.Register(mountName, hostFileSystem, nameGenerator); + TimeSpan endTime = fs.Time.GetCurrent(); + + fs.OutputAccessLog(rc, startTime, endTime, logMessage, caller); + } + else + { + rc = fs.Register(mountName, hostFileSystem, nameGenerator); + } + + if (rc.IsFailure()) return rc; + + if (fs.IsEnabledAccessLog(AccessLogTarget.Application)) + { + fs.EnableFileSystemAccessorAccessLog(mountName); + } + + return Result.Success; + } + + private static Result PreMountHost(out ICommonMountNameGenerator nameGenerator, U8Span mountName, U8Span path) + { + nameGenerator = default; + + Result rc = MountHelpers.CheckMountName(mountName); + if (rc.IsFailure()) return rc; + + if (path.IsNull()) + return ResultFs.NullptrArgument.Log(); + + nameGenerator = new HostCommonMountNameGenerator(path); + return Result.Success; + } + + private static Result OpenHostFileSystem(FileSystemClient fs, out IFileSystem fileSystem, U8Span mountName, + U8Span path, MountHostOption option) + { + fileSystem = default; + + if (mountName.IsNull()) + return ResultFs.NullptrArgument.Log(); + + if (path.IsNull()) + return ResultFs.NullptrArgument.Log(); + + if (PathUtility.IsWindowsDrive(mountName)) + return ResultFs.InvalidMountName.Log(); + + if (MountHelpers.IsReservedMountName(mountName)) + return ResultFs.InvalidMountName.Log(); + + bool needsTrailingSeparator = false; + int pathLength = StringUtils.GetLength(path, PathTools.MaxPathLength + 1); + + if (pathLength != 0 && PathTool.IsSeparator(path[pathLength - 1])) + { + needsTrailingSeparator = true; + pathLength++; + } + + if (pathLength + 1 > PathTools.MaxPathLength) + return ResultFs.TooLongPath.Log(); + + FsPath fullPath; + unsafe { _ = &fullPath; } // workaround for CS0165 + + var sb = new U8StringBuilder(fullPath.Str); + sb.Append(StringTraits.DirectorySeparator).Append(path); + + if (needsTrailingSeparator) + { + sb.Append(StringTraits.DirectorySeparator); + } + + if (sb.Overflowed) + return ResultFs.TooLongPath.Log(); + + // If the input path begins with "//", change any leading '/' characters to '\' + if (PathTool.IsSeparator(fullPath.Str[1]) && PathTool.IsSeparator(fullPath.Str[2])) + { + for (int i = 1; PathTool.IsSeparator(fullPath.Str[i]); i++) + { + fullPath.Str[i] = StringTraits.AltDirectorySeparator; + } + } + + return OpenHostFileSystemImpl(fs, out fileSystem, ref fullPath, option); + } + + private static Result OpenHostFileSystemImpl(FileSystemClient fs, out IFileSystem fileSystem, ref FsPath path, MountHostOption option) + { + fileSystem = default; + + IFileSystemProxy fsProxy = fs.GetFileSystemProxyServiceObject(); + IFileSystem hostFs; + + if (option == MountHostOption.None) + { + Result rc = fsProxy.OpenHostFileSystem(out hostFs, ref path); + if (rc.IsFailure()) return rc; + } + else + { + Result rc = fsProxy.OpenHostFileSystemWithOption(out hostFs, ref path, option); + if (rc.IsFailure()) return rc; + } + + fileSystem = hostFs; + return Result.Success; + } + } +} diff --git a/src/LibHac/FsService/FileSystemProxy.cs b/src/LibHac/FsService/FileSystemProxy.cs index ed5e493a..f418b7f5 100644 --- a/src/LibHac/FsService/FileSystemProxy.cs +++ b/src/LibHac/FsService/FileSystemProxy.cs @@ -42,7 +42,7 @@ namespace LibHac.FsService var normalizer = new PathNormalizer(path, GetPathNormalizerOptions(path)); if (normalizer.Result.IsFailure()) return normalizer.Result; - + // ReSharper disable once ConditionIsAlwaysTrueOrFalse return FsProxyCore.OpenFileSystem(out fileSystem, normalizer.Path, type, canMountSystemDataPrivate, titleId); } @@ -682,7 +682,12 @@ namespace LibHac.FsService throw new NotImplementedException(); } - public Result OpenHostFileSystem(out IFileSystem fileSystem, ref FsPath subPath) + public Result OpenHostFileSystemWithOption(out IFileSystem fileSystem, ref FsPath path, MountHostOption option) + { + throw new NotImplementedException(); + } + + public Result OpenHostFileSystem(out IFileSystem fileSystem, ref FsPath path) { throw new NotImplementedException(); } diff --git a/src/LibHac/FsService/IFileSystemProxy.cs b/src/LibHac/FsService/IFileSystemProxy.cs index 11f79cd5..e4b1c9ce 100644 --- a/src/LibHac/FsService/IFileSystemProxy.cs +++ b/src/LibHac/FsService/IFileSystemProxy.cs @@ -17,7 +17,8 @@ namespace LibHac.FsService Result OpenBisFileSystem(out IFileSystem fileSystem, ref FsPath rootPath, BisPartitionId partitionId); Result OpenBisStorage(out IStorage storage, BisPartitionId partitionId); Result InvalidateBisCache(); - Result OpenHostFileSystem(out IFileSystem fileSystem, ref FsPath subPath); + Result OpenHostFileSystemWithOption(out IFileSystem fileSystem, ref FsPath path, MountHostOption option); + Result OpenHostFileSystem(out IFileSystem fileSystem, ref FsPath path); Result OpenSdCardFileSystem(out IFileSystem fileSystem); Result FormatSdCardFileSystem(); Result DeleteSaveDataFileSystem(ulong saveDataId);