From a006816a2ebdee25b2e84be4b99c640225bad8ab Mon Sep 17 00:00:00 2001 From: Alex Barney Date: Fri, 13 Mar 2020 00:09:44 -0700 Subject: [PATCH] Allow opening directories with OpenFileSystem --- src/LibHac/Common/U8Span.cs | 2 + src/LibHac/Fs/CommonMountNames.cs | 4 + src/LibHac/Fs/PathUtility.cs | 51 ++++- .../ISubDirectoryFileSystemCreator.cs | 1 + .../ITargetManagerFileSystemCreator.cs | 5 +- .../FsService/Creators/StorageOnNcaCreator.cs | 3 +- .../Creators/SubDirectoryFileSystemCreator.cs | 7 +- .../TargetManagerFileSystemCreator.cs | 2 +- src/LibHac/FsService/FileSystemProxyCore.cs | 208 +++++++++++++++++- src/LibHac/FsService/Util.cs | 29 ++- 10 files changed, 292 insertions(+), 20 deletions(-) diff --git a/src/LibHac/Common/U8Span.cs b/src/LibHac/Common/U8Span.cs index b14f32cd..92cd9978 100644 --- a/src/LibHac/Common/U8Span.cs +++ b/src/LibHac/Common/U8Span.cs @@ -14,6 +14,8 @@ namespace LibHac.Common public ReadOnlySpan Value => _buffer; public int Length => _buffer.Length; + public static U8Span Empty => default; + public byte this[int i] { get => _buffer[i]; diff --git a/src/LibHac/Fs/CommonMountNames.cs b/src/LibHac/Fs/CommonMountNames.cs index a3cdc16e..ff6cf258 100644 --- a/src/LibHac/Fs/CommonMountNames.cs +++ b/src/LibHac/Fs/CommonMountNames.cs @@ -15,5 +15,9 @@ namespace LibHac.Fs public static readonly U8String SdCardFileSystemMountName = new U8String("@Sdcard"); public static readonly U8String HostRootFileSystemMountName = new U8String("@Host"); public static readonly U8String RegisteredUpdatePartitionMountName = new U8String("@RegUpdate"); + + public const char GameCardFileSystemMountNameUpdateSuffix = 'U'; + public const char GameCardFileSystemMountNameNormalSuffix = 'N'; + public const char GameCardFileSystemMountNameSecureSuffix = 'S'; } } diff --git a/src/LibHac/Fs/PathUtility.cs b/src/LibHac/Fs/PathUtility.cs index b142d309..949d343a 100644 --- a/src/LibHac/Fs/PathUtility.cs +++ b/src/LibHac/Fs/PathUtility.cs @@ -1,4 +1,5 @@ -using System.Runtime.CompilerServices; +using System.Diagnostics; +using System.Runtime.CompilerServices; using LibHac.Common; using static LibHac.Fs.PathTool; @@ -28,5 +29,53 @@ namespace LibHac.Fs (IsSeparator(path.GetUnsafe(0)) && IsSeparator(path.GetUnsafe(1)) || IsAltSeparator(path.GetUnsafe(0)) && IsAltSeparator(path.GetUnsafe(1))); } + + public static int GetWindowsPathSkipLength(U8Span path) + { + if (IsWindowsDrive(path)) + return 2; + + if (!IsUnc(path)) + return 0; + + for (int i = 2; i < path.Length && !IsNullTerminator(path[i]); i++) + { + byte c = path[i]; + if (c == (byte)'$' || IsDriveSeparator(c)) + { + return i + 1; + } + } + + return 0; + } + + public static Result VerifyPath(U8Span path, int maxPathLength, int maxNameLength) + { + Debug.Assert(!path.IsNull()); + + int nameLength = 0; + + for (int i = 0; i < path.Length && i <= maxPathLength && nameLength <= maxNameLength; i++) + { + byte c = path[i]; + + if (IsNullTerminator(c)) + return Result.Success; + + // todo: Compare path based on their Unicode code points + + if (c == ':' || c == '*' || c == '?' || c == '<' || c == '>' || c == '|') + return ResultFs.InvalidCharacter.Log(); + + nameLength++; + if (c == '\\' || c == '/') + { + nameLength = 0; + } + } + + return ResultFs.TooLongPath.Log(); + } } } diff --git a/src/LibHac/FsService/Creators/ISubDirectoryFileSystemCreator.cs b/src/LibHac/FsService/Creators/ISubDirectoryFileSystemCreator.cs index e8c60add..a3510a46 100644 --- a/src/LibHac/FsService/Creators/ISubDirectoryFileSystemCreator.cs +++ b/src/LibHac/FsService/Creators/ISubDirectoryFileSystemCreator.cs @@ -6,5 +6,6 @@ namespace LibHac.FsService.Creators public interface ISubDirectoryFileSystemCreator { Result Create(out IFileSystem subDirFileSystem, IFileSystem baseFileSystem, U8Span path); + Result Create(out IFileSystem subDirFileSystem, IFileSystem baseFileSystem, U8Span path, bool preserveUnc); } } \ No newline at end of file diff --git a/src/LibHac/FsService/Creators/ITargetManagerFileSystemCreator.cs b/src/LibHac/FsService/Creators/ITargetManagerFileSystemCreator.cs index e0858de4..cae86243 100644 --- a/src/LibHac/FsService/Creators/ITargetManagerFileSystemCreator.cs +++ b/src/LibHac/FsService/Creators/ITargetManagerFileSystemCreator.cs @@ -1,10 +1,11 @@ -using LibHac.Fs; +using System; +using LibHac.Fs; namespace LibHac.FsService.Creators { public interface ITargetManagerFileSystemCreator { Result Create(out IFileSystem fileSystem, bool openCaseSensitive); - Result GetCaseSensitivePath(out bool isSuccess, ref string path); + Result GetCaseSensitivePath(out bool isSuccess, Span path); } } \ No newline at end of file diff --git a/src/LibHac/FsService/Creators/StorageOnNcaCreator.cs b/src/LibHac/FsService/Creators/StorageOnNcaCreator.cs index bf90ce1e..f93c12d4 100644 --- a/src/LibHac/FsService/Creators/StorageOnNcaCreator.cs +++ b/src/LibHac/FsService/Creators/StorageOnNcaCreator.cs @@ -8,6 +8,7 @@ namespace LibHac.FsService.Creators { public class StorageOnNcaCreator : IStorageOnNcaCreator { + // ReSharper disable once UnusedMember.Local private bool IsEnabledProgramVerification { get; set; } private Keyset Keyset { get; } @@ -39,7 +40,7 @@ namespace LibHac.FsService.Creators storage = storageTemp; fsHeader = nca.Header.GetFsHeader(fsIndex); - + return Result.Success; } diff --git a/src/LibHac/FsService/Creators/SubDirectoryFileSystemCreator.cs b/src/LibHac/FsService/Creators/SubDirectoryFileSystemCreator.cs index 90e835f7..9ddbc677 100644 --- a/src/LibHac/FsService/Creators/SubDirectoryFileSystemCreator.cs +++ b/src/LibHac/FsService/Creators/SubDirectoryFileSystemCreator.cs @@ -7,13 +7,18 @@ namespace LibHac.FsService.Creators public class SubDirectoryFileSystemCreator : ISubDirectoryFileSystemCreator { public Result Create(out IFileSystem subDirFileSystem, IFileSystem baseFileSystem, U8Span path) + { + return Create(out subDirFileSystem, baseFileSystem, path, false); + } + + public Result Create(out IFileSystem subDirFileSystem, IFileSystem baseFileSystem, U8Span path, bool preserveUnc) { subDirFileSystem = default; Result rc = baseFileSystem.OpenDirectory(out IDirectory _, path, OpenDirectoryMode.Directory); if (rc.IsFailure()) return rc; - rc = SubdirectoryFileSystem.CreateNew(out SubdirectoryFileSystem fs, baseFileSystem, path.ToU8String()); + rc = SubdirectoryFileSystem.CreateNew(out SubdirectoryFileSystem fs, baseFileSystem, path.ToU8String(), preserveUnc); subDirFileSystem = fs; return rc; } diff --git a/src/LibHac/FsService/Creators/TargetManagerFileSystemCreator.cs b/src/LibHac/FsService/Creators/TargetManagerFileSystemCreator.cs index c20d5884..1ccefcc8 100644 --- a/src/LibHac/FsService/Creators/TargetManagerFileSystemCreator.cs +++ b/src/LibHac/FsService/Creators/TargetManagerFileSystemCreator.cs @@ -10,7 +10,7 @@ namespace LibHac.FsService.Creators throw new NotImplementedException(); } - public Result GetCaseSensitivePath(out bool isSuccess, ref string path) + public Result GetCaseSensitivePath(out bool isSuccess, Span path) { throw new NotImplementedException(); } diff --git a/src/LibHac/FsService/FileSystemProxyCore.cs b/src/LibHac/FsService/FileSystemProxyCore.cs index b6a0ec6b..b64999bf 100644 --- a/src/LibHac/FsService/FileSystemProxyCore.cs +++ b/src/LibHac/FsService/FileSystemProxyCore.cs @@ -1,4 +1,5 @@ using System; +using System.Buffers.Text; using System.Runtime.CompilerServices; using LibHac.Common; using LibHac.Fs; @@ -41,11 +42,13 @@ namespace LibHac.FsService // Get a reference to the path that will be advanced as each part of the path is parsed U8Span path2 = path.Slice(0, StringUtils.GetLength(path)); - Result rc = OpenFileSystemFromMountName(ref path2, out IFileSystem baseFileSystem, out bool successQQ, + // Open the root filesystem based on the path's mount name + Result rc = OpenFileSystemFromMountName(ref path2, out IFileSystem baseFileSystem, out bool shouldContinue, out MountNameInfo mountNameInfo); if (rc.IsFailure()) return rc; - if (!successQQ) + // Don't continue if the rest of the path is empty + if (!shouldContinue) return ResultFs.InvalidArgument.Log(); if (type == FileSystemProxyType.Logo && mountNameInfo.IsGameCard) @@ -65,7 +68,19 @@ namespace LibHac.FsService if (isDirectory) { - throw new NotImplementedException(); + if (!mountNameInfo.IsHostFs) + return ResultFs.PermissionDenied.Log(); + + if (type == FileSystemProxyType.Manual) + { + rc = TryOpenCaseSensitiveContentDirectory(out IFileSystem manualFileSystem, baseFileSystem, path2); + if (rc.IsFailure()) return rc; + + fileSystem = new ReadOnlyFileSystem(manualFileSystem); + return Result.Success; + } + + return TryOpenContentDirectory(path2, out fileSystem, baseFileSystem, type, true); } rc = TryOpenNsp(ref path2, out IFileSystem nspFileSystem, baseFileSystem); @@ -123,23 +138,52 @@ namespace LibHac.FsService public bool CanMountNca; } - private Result OpenFileSystemFromMountName(ref U8Span path, out IFileSystem fileSystem, out bool successQQ, + private Result OpenFileSystemFromMountName(ref U8Span path, out IFileSystem fileSystem, out bool shouldContinue, out MountNameInfo info) { fileSystem = default; info = new MountNameInfo(); - successQQ = true; + shouldContinue = true; if (StringUtils.Compare(path, CommonMountNames.GameCardFileSystemMountName, CommonMountNames.GameCardFileSystemMountName.Length) == 0) { path = path.Slice(CommonMountNames.GameCardFileSystemMountName.Length); + if (StringUtils.GetLength(path.Value, 9) < 9) + return ResultFs.InvalidPath.Log(); + + GameCardPartition partition; + switch ((char)path[0]) + { + case CommonMountNames.GameCardFileSystemMountNameUpdateSuffix: + partition = GameCardPartition.Update; + break; + case CommonMountNames.GameCardFileSystemMountNameNormalSuffix: + partition = GameCardPartition.Normal; + break; + case CommonMountNames.GameCardFileSystemMountNameSecureSuffix: + partition = GameCardPartition.Secure; + break; + default: + return ResultFs.InvalidPath.Log(); + } + + path = path.Slice(1); + bool handleParsed = Utf8Parser.TryParse(path, out int handle, out int bytesConsumed); + + if (!handleParsed || handle == -1 || bytesConsumed != 8) + return ResultFs.InvalidPath.Log(); + + path = path.Slice(8); + + Result rc = OpenGameCardFileSystem(out fileSystem, new GameCardHandle(handle), partition); + if (rc.IsFailure()) return rc; + + info.GcHandle = handle; info.IsGameCard = true; info.CanMountNca = true; - - throw new NotImplementedException(); } else if (StringUtils.Compare(path, CommonMountNames.ContentStorageSystemMountName, @@ -228,7 +272,8 @@ namespace LibHac.FsService info.IsHostFs = true; info.CanMountNca = true; - throw new NotImplementedException(); + Result rc = OpenHostFileSystem(out fileSystem, U8Span.Empty, openCaseSensitive: false); + if (rc.IsFailure()) return rc; } else if (StringUtils.Compare(path, CommonMountNames.RegisteredUpdatePartitionMountName, @@ -248,7 +293,7 @@ namespace LibHac.FsService if (StringUtils.GetLength(path, FsPath.MaxLength) == 0) { - successQQ = false; + shouldContinue = false; } return Result.Success; @@ -293,14 +338,106 @@ namespace LibHac.FsService return ResultFs.PathNotFound.Log(); } + private Result TryOpenContentDirectory(U8Span path, out IFileSystem contentFileSystem, + IFileSystem baseFileSystem, FileSystemProxyType fsType, bool preserveUnc) + { + contentFileSystem = default; + + FsPath fullPath; + unsafe { _ = &fullPath; } // workaround for CS0165 + + Result rc = FsCreators.SubDirectoryFileSystemCreator.Create(out IFileSystem subDirFs, + baseFileSystem, path, preserveUnc); + if (rc.IsFailure()) return rc; + + return OpenSubDirectoryForFsType(out contentFileSystem, subDirFs, fsType); + } + + private Result TryOpenCaseSensitiveContentDirectory(out IFileSystem contentFileSystem, + IFileSystem baseFileSystem, U8Span path) + { + contentFileSystem = default; + FsPath fullPath; + unsafe { _ = &fullPath; } // workaround for CS0165 + + var pathBuilder = new PathBuilder(fullPath.Str); + Result rc = pathBuilder.Append(path); + if (rc.IsFailure()) return rc; + + rc = pathBuilder.Append(new[] { (byte)'d', (byte)'a', (byte)'t', (byte)'a', (byte)'/' }); + if (rc.IsFailure()) return rc; + + pathBuilder.Terminate(); + + rc = FsCreators.TargetManagerFileSystemCreator.GetCaseSensitivePath(out bool success, fullPath.Str); + if (rc.IsFailure()) return rc; + + // Reopen the host filesystem as case sensitive + if (success) + { + baseFileSystem.Dispose(); + + rc = OpenHostFileSystem(out baseFileSystem, U8Span.Empty, openCaseSensitive: true); + if (rc.IsFailure()) return rc; + } + + return FsCreators.SubDirectoryFileSystemCreator.Create(out contentFileSystem, baseFileSystem, fullPath); + } + + private Result OpenSubDirectoryForFsType(out IFileSystem fileSystem, IFileSystem baseFileSystem, + FileSystemProxyType fsType) + { + fileSystem = default; + ReadOnlySpan dirName; + + // Get the name of the subdirectory for the filesystem type + switch (fsType) + { + case FileSystemProxyType.Package: + fileSystem = baseFileSystem; + return Result.Success; + + case FileSystemProxyType.Code: + dirName = new[] { (byte)'/', (byte)'c', (byte)'o', (byte)'d', (byte)'e', (byte)'/' }; + break; + case FileSystemProxyType.Rom: + case FileSystemProxyType.Control: + case FileSystemProxyType.Manual: + case FileSystemProxyType.Meta: + // Nintendo doesn't include the Data case in the switch. Maybe an oversight? + case FileSystemProxyType.Data: + case FileSystemProxyType.RegisteredUpdate: + dirName = new[] { (byte)'/', (byte)'d', (byte)'a', (byte)'t', (byte)'a', (byte)'/' }; + break; + case FileSystemProxyType.Logo: + dirName = new[] { (byte)'/', (byte)'l', (byte)'o', (byte)'g', (byte)'o', (byte)'/' }; + break; + + default: + return ResultFs.InvalidArgument.Log(); + } + + // Open the subdirectory filesystem + Result rc = FsCreators.SubDirectoryFileSystemCreator.Create(out IFileSystem subDirFs, baseFileSystem, + new U8Span(dirName)); + if (rc.IsFailure()) return rc; + + if (fsType == FileSystemProxyType.Code) + { + rc = FsCreators.StorageOnNcaCreator.VerifyAcidSignature(subDirFs, null); + if (rc.IsFailure()) return rc; + } + + fileSystem = subDirFs; + return Result.Success; + } + private Result TryOpenNsp(ref U8Span path, out IFileSystem outFileSystem, IFileSystem baseFileSystem) { outFileSystem = default; ReadOnlySpan nspExtension = new[] { (byte)'.', (byte)'n', (byte)'s', (byte)'p' }; - int searchEnd = path.Length - 4; - // Search for the end of the nsp part of the path int nspPathLen = 0; @@ -447,6 +584,7 @@ namespace LibHac.FsService if (Crypto.CryptoUtil.IsSameBytes(rightsId.AsBytes(), zero.AsBytes(), Unsafe.SizeOf())) return Result.Success; + // ReSharper disable once UnusedVariable Result rc = ExternalKeys.Get(rightsId, out AccessKey accessKey); if (rc.IsFailure()) return rc; @@ -604,7 +742,53 @@ namespace LibHac.FsService public Result OpenGameCardFileSystem(out IFileSystem fileSystem, GameCardHandle handle, GameCardPartition partitionId) { - return FsCreators.GameCardFileSystemCreator.Create(out fileSystem, handle, partitionId); + Result rc; + int tries = 0; + + do + { + rc = FsCreators.GameCardFileSystemCreator.Create(out fileSystem, handle, partitionId); + + if (!ResultFs.DataCorrupted.Includes(rc)) + break; + + tries++; + } while (tries < 2); + + return rc; + } + + public Result OpenHostFileSystem(out IFileSystem fileSystem, U8Span path, bool openCaseSensitive) + { + fileSystem = default; + Result rc; + + if (!path.IsEmpty()) + { + rc = Util.VerifyHostPath(path); + if (rc.IsFailure()) return rc; + } + + rc = FsCreators.TargetManagerFileSystemCreator.Create(out IFileSystem hostFs, openCaseSensitive); + if (rc.IsFailure()) return rc; + + if (path.IsEmpty()) + { + rc = hostFs.GetEntryType(out _, "C:/".ToU8Span()); + + // Nintendo ignores all results other than this one + if (ResultFs.TargetNotFound.Includes(rc)) + return rc; + + fileSystem = hostFs; + return Result.Success; + } + + rc = FsCreators.SubDirectoryFileSystemCreator.Create(out IFileSystem subDirFs, hostFs, path, preserveUnc: true); + if (rc.IsFailure()) return rc; + + fileSystem = subDirFs; + return Result.Success; } public Result RegisterExternalKey(ref RightsId rightsId, ref AccessKey externalKey) diff --git a/src/LibHac/FsService/Util.cs b/src/LibHac/FsService/Util.cs index b1208cf1..243717b5 100644 --- a/src/LibHac/FsService/Util.cs +++ b/src/LibHac/FsService/Util.cs @@ -10,12 +10,13 @@ namespace LibHac.FsService bool createPathIfMissing) { subFileSystem = default; + Result rc; if (!createPathIfMissing) { if (path == null) return ResultFs.NullptrArgument.Log(); - Result rc = baseFileSystem.GetEntryType(out DirectoryEntryType entryType, path.ToU8Span()); + rc = baseFileSystem.GetEntryType(out DirectoryEntryType entryType, path.ToU8Span()); if (rc.IsFailure() || entryType != DirectoryEntryType.Directory) { @@ -23,7 +24,8 @@ namespace LibHac.FsService } } - baseFileSystem.EnsureDirectoryExists(path); + rc = baseFileSystem.EnsureDirectoryExists(path); + if (rc.IsFailure()) return rc; return CreateSubFileSystemImpl(out subFileSystem, baseFileSystem, path); } @@ -42,6 +44,29 @@ namespace LibHac.FsService return rc; } + public static Result VerifyHostPath(U8Span path) + { + if(path.IsEmpty()) + return Result.Success; + + if (path[0] != StringTraits.DirectorySeparator) + return ResultFs.InvalidPathFormat.Log(); + + U8Span path2 = path.Slice(1); + + if(path2.IsEmpty()) + return Result.Success; + + int skipLength = PathUtility.GetWindowsPathSkipLength(path2); + int remainingLength = PathTools.MaxPathLength - skipLength; + + Result rc = PathUtility.VerifyPath(path2.Slice(skipLength), remainingLength, remainingLength); + if (rc.IsFailure()) return rc; + + var normalizer = new PathNormalizer(path, PathNormalizer.Option.PreserveUnc); + return normalizer.Result; + } + public static bool UseDeviceUniqueSaveMac(SaveDataSpaceId spaceId) { return spaceId == SaveDataSpaceId.System ||