diff --git a/build/CodeGen/results.csv b/build/CodeGen/results.csv index 6eab7333..992e05a0 100644 --- a/build/CodeGen/results.csv +++ b/build/CodeGen/results.csv @@ -81,6 +81,8 @@ Module,DescriptionStart,DescriptionEnd,Name,Summary 2,4464,,AllocationTableIteratedRangeEntry, 2,4501,4599,NcaCorrupted, +2,4512,,InvalidNcaFsType, +2,4527,,InvalidNcaProgramId, 2,4601,4639,IntegrityVerificationStorageCorrupted, 2,4602,,InvalidIvfcMagic, 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/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 a3cdc16e..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"); @@ -15,5 +17,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/FileStorage2.cs b/src/LibHac/Fs/FileStorage2.cs index 75f665e3..1d242583 100644 --- a/src/LibHac/Fs/FileStorage2.cs +++ b/src/LibHac/Fs/FileStorage2.cs @@ -6,10 +6,10 @@ namespace LibHac.Fs { public class FileStorage2 : StorageBase { - private const long InvalidSize = -1; + protected const long InvalidSize = -1; private IFile BaseFile { get; set; } - private long FileSize { get; set; } + protected long FileSize { get; set; } public FileStorage2(IFile baseFile) { diff --git a/src/LibHac/Fs/FileStorageBasedFileSystem.cs b/src/LibHac/Fs/FileStorageBasedFileSystem.cs index cc27d1bb..ce43fa53 100644 --- a/src/LibHac/Fs/FileStorageBasedFileSystem.cs +++ b/src/LibHac/Fs/FileStorageBasedFileSystem.cs @@ -9,7 +9,10 @@ namespace LibHac.Fs private IFileSystem BaseFileSystem { get; set; } private IFile BaseFile { get; set; } - private FileStorageBasedFileSystem() { } + private FileStorageBasedFileSystem() + { + FileSize = InvalidSize; + } public static Result CreateNew(out FileStorageBasedFileSystem created, IFileSystem baseFileSystem, U8Span path, OpenMode mode) 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..4704a82d 100644 --- a/src/LibHac/Fs/FsEnums.cs +++ b/src/LibHac/Fs/FsEnums.cs @@ -187,4 +187,11 @@ namespace LibHac.Fs Nand = 1, SdCard = 2 } + + [Flags] + 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/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/Fs/ResultFs.cs b/src/LibHac/Fs/ResultFs.cs index 2b7e7a45..cb4f4bd0 100644 --- a/src/LibHac/Fs/ResultFs.cs +++ b/src/LibHac/Fs/ResultFs.cs @@ -171,6 +171,10 @@ namespace LibHac.Fs /// Error code: 2002-4501; Range: 4501-4599; Inner value: 0x232a02 public static Result.Base NcaCorrupted { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new Result.Base(ModuleFs, 4501, 4599); } + /// Error code: 2002-4512; Inner value: 0x234002 + public static Result.Base InvalidNcaFsType => new Result.Base(ModuleFs, 4512); + /// Error code: 2002-4527; Inner value: 0x235e02 + public static Result.Base InvalidNcaProgramId => new Result.Base(ModuleFs, 4527); /// Error code: 2002-4601; Range: 4601-4639; Inner value: 0x23f202 public static Result.Base IntegrityVerificationStorageCorrupted { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new Result.Base(ModuleFs, 4601, 4639); } diff --git a/src/LibHac/Fs/Shim/Host.cs b/src/LibHac/Fs/Shim/Host.cs new file mode 100644 index 00000000..1164dbbb --- /dev/null +++ b/src/LibHac/Fs/Shim/Host.cs @@ -0,0 +1,374 @@ +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 +{ + /// + /// Contains functions for mounting file systems from a host computer. + /// + /// + /// All functions in this file are based on SDK 9.3 + /// + 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; + } + } + + /// + /// Mounts the C:\ drive of a host Windows computer at @Host:/ + /// + /// The to use. + /// The of the operation. + 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; + } + + /// + /// Mounts the C:\ drive of a host Windows computer at @Host:/ + /// + /// The to use. + /// Options for mounting the host file system. + /// The of the operation. + 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; + } + + /// + /// Unmounts the file system at @Host:/ + /// + /// The to use. + public static void UnmountHostRoot(this FileSystemClient fs) + { + fs.Unmount(HostRootFileSystemMountName); + } + + /// + /// Mounts a directory on a host Windows computer at the specified mount point. + /// + /// The to use. + /// The mount name at which the file system will be mounted. + /// The path on the host computer to mount. e.g. C:\Windows\System32 + /// The of the operation. + public static Result MountHost(this FileSystemClient fs, U8Span mountName, U8Span path) + { + return MountHostImpl(fs, mountName, path, null); + } + + /// + /// Mounts a directory on a host Windows computer at the specified mount point. + /// + /// The to use. + /// The mount name at which the file system will be mounted. + /// The path on the host computer to mount. e.g. C:\Windows\System32 + /// Options for mounting the host file system. + /// The of the operation. + public static Result MountHost(this FileSystemClient fs, U8Span mountName, U8Span path, MountHostOption option) + { + return MountHostImpl(fs, mountName, path, option); + } + + /// + /// Mounts a directory on a host Windows computer at the specified mount point. + /// + /// The to use. + /// The mount name at which the file system will be mounted. + /// The path on the host computer to mount. e.g. C:\Windows\System32 + /// Options for mounting the host file system. Specifying this parameter is optional. + /// The caller of this function. + /// The of the operation. + 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; + } + + /// + /// Creates an based on the and + /// , and verifies the . + /// + /// If successful, the created . + /// The mount name at which the file system will be mounted. + /// The path that will be opened on the host computer. e.g. C:\Windows\System32 + /// The of the operation. + 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; + } + + /// + /// Verifies parameters and opens a host file system. + /// + /// The to use. + /// If successful, the opened host file system. + /// The mount name to be verified. + /// The path on the host computer to open. e.g. C:\Windows\System32 + /// Options for opening the host file system. + /// The of the operation. + 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); + } + + /// + /// Opens a host file system via . + /// + /// The to use. + /// If successful, the opened host file system. + /// The path on the host computer to open. e.g. /C:\Windows\System32/ + /// Options for opening the host file system. + /// The of the operation. + 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/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/RomFileSystemCreator.cs b/src/LibHac/FsService/Creators/RomFileSystemCreator.cs index 8994e8e4..a1309b51 100644 --- a/src/LibHac/FsService/Creators/RomFileSystemCreator.cs +++ b/src/LibHac/FsService/Creators/RomFileSystemCreator.cs @@ -1,13 +1,15 @@ -using System; -using LibHac.Fs; +using LibHac.Fs; +using LibHac.FsSystem.RomFs; namespace LibHac.FsService.Creators { public class RomFileSystemCreator : IRomFileSystemCreator { + // todo: Implement properly public Result Create(out IFileSystem fileSystem, IStorage romFsStorage) { - throw new NotImplementedException(); + fileSystem = new RomFsFileSystem(romFsStorage); + return Result.Success; } } } diff --git a/src/LibHac/FsService/Creators/StorageOnNcaCreator.cs b/src/LibHac/FsService/Creators/StorageOnNcaCreator.cs index e651289b..f93c12d4 100644 --- a/src/LibHac/FsService/Creators/StorageOnNcaCreator.cs +++ b/src/LibHac/FsService/Creators/StorageOnNcaCreator.cs @@ -1,14 +1,47 @@ using System; using LibHac.Fs; +using LibHac.FsSystem; +using LibHac.FsSystem.Detail; using LibHac.FsSystem.NcaUtils; namespace LibHac.FsService.Creators { public class StorageOnNcaCreator : IStorageOnNcaCreator { + // ReSharper disable once UnusedMember.Local + private bool IsEnabledProgramVerification { get; set; } + private Keyset Keyset { get; } + + public StorageOnNcaCreator(Keyset keyset) + { + Keyset = keyset; + } + + // todo: Implement NcaReader and other Nca classes public Result Create(out IStorage storage, out NcaFsHeader fsHeader, Nca nca, int fsIndex, bool isCodeFs) { - throw new NotImplementedException(); + storage = default; + fsHeader = default; + + Result rc = OpenStorage(out IStorage storageTemp, nca, fsIndex); + if (rc.IsFailure()) return rc; + + if (isCodeFs) + { + using (var codeFs = new PartitionFileSystemCore()) + { + rc = codeFs.Initialize(storageTemp); + if (rc.IsFailure()) return rc; + + rc = VerifyAcidSignature(codeFs, nca); + if (rc.IsFailure()) return rc; + } + } + + storage = storageTemp; + fsHeader = nca.Header.GetFsHeader(fsIndex); + + return Result.Success; } public Result CreateWithPatch(out IStorage storage, out NcaFsHeader fsHeader, Nca baseNca, Nca patchNca, int fsIndex, bool isCodeFs) @@ -18,12 +51,25 @@ namespace LibHac.FsService.Creators public Result OpenNca(out Nca nca, IStorage ncaStorage) { - throw new NotImplementedException(); + nca = new Nca(Keyset, ncaStorage); + return Result.Success; } public Result VerifyAcidSignature(IFileSystem codeFileSystem, Nca nca) { - throw new NotImplementedException(); + // todo + return Result.Success; + } + + private Result OpenStorage(out IStorage storage, Nca nca, int fsIndex) + { + storage = default; + + if (!nca.SectionExists(fsIndex)) + return ResultFs.PartitionNotFound.Log(); + + storage = nca.OpenStorage(fsIndex, IntegrityCheckLevel.ErrorOnInvalid); + 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/DefaultFsServerObjects.cs b/src/LibHac/FsService/DefaultFsServerObjects.cs index 86732891..c71c406a 100644 --- a/src/LibHac/FsService/DefaultFsServerObjects.cs +++ b/src/LibHac/FsService/DefaultFsServerObjects.cs @@ -20,7 +20,7 @@ namespace LibHac.FsService creators.RomFileSystemCreator = new RomFileSystemCreator(); creators.PartitionFileSystemCreator = new PartitionFileSystemCreator(); - creators.StorageOnNcaCreator = new StorageOnNcaCreator(); + creators.StorageOnNcaCreator = new StorageOnNcaCreator(keyset); creators.TargetManagerFileSystemCreator = new TargetManagerFileSystemCreator(); creators.SubDirectoryFileSystemCreator = new SubDirectoryFileSystemCreator(); creators.SaveDataFileSystemCreator = new SaveDataFileSystemCreator(keyset); diff --git a/src/LibHac/FsService/FileSystemProxy.cs b/src/LibHac/FsService/FileSystemProxy.cs index 8bd7223c..266be3ad 100644 --- a/src/LibHac/FsService/FileSystemProxy.cs +++ b/src/LibHac/FsService/FileSystemProxy.cs @@ -34,7 +34,28 @@ namespace LibHac.FsService public Result OpenFileSystemWithId(out IFileSystem fileSystem, ref FsPath path, TitleId titleId, FileSystemProxyType type) { - throw new NotImplementedException(); + fileSystem = default; + + // Missing permission check, speed emulation storage type wrapper, and FileSystemInterfaceAdapter + + bool canMountSystemDataPrivate = false; + + 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); + } + + private PathNormalizer.Option GetPathNormalizerOptions(U8Span path) + { + int hostMountLength = StringUtils.GetLength(CommonMountNames.HostRootFileSystemMountName, + PathTools.MountNameLengthMax); + + bool isHostPath = StringUtils.Compare(path, CommonMountNames.HostRootFileSystemMountName, hostMountLength) == 0; + + PathNormalizer.Option hostOption = isHostPath ? PathNormalizer.Option.PreserveUnc : PathNormalizer.Option.None; + return PathNormalizer.Option.HasMountName | PathNormalizer.Option.PreserveTailSeparator | hostOption; } public Result OpenFileSystemWithPatch(out IFileSystem fileSystem, TitleId titleId, FileSystemProxyType type) @@ -661,9 +682,16 @@ 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(); + // Missing permission check + + return FsProxyCore.OpenHostFileSystem(out fileSystem, new U8Span(path.Str), option.HasFlag(MountHostOption.PseudoCaseSensitive)); + } + + public Result OpenHostFileSystem(out IFileSystem fileSystem, ref FsPath path) + { + return OpenHostFileSystemWithOption(out fileSystem, ref path, MountHostOption.None); } public Result OpenSdCardFileSystem(out IFileSystem fileSystem) diff --git a/src/LibHac/FsService/FileSystemProxyCore.cs b/src/LibHac/FsService/FileSystemProxyCore.cs index 5badf4d6..3531c222 100644 --- a/src/LibHac/FsService/FileSystemProxyCore.cs +++ b/src/LibHac/FsService/FileSystemProxyCore.cs @@ -1,9 +1,13 @@ using System; +using System.Buffers.Text; +using System.Runtime.CompilerServices; using LibHac.Common; using LibHac.Fs; using LibHac.Fs.Shim; using LibHac.FsSystem; using LibHac.FsService.Creators; +using LibHac.FsSystem.NcaUtils; +using LibHac.Ncm; using LibHac.Spl; using RightsId = LibHac.Fs.RightsId; @@ -22,7 +26,7 @@ namespace LibHac.FsService private GlobalAccessLogMode LogMode { get; set; } public bool IsSdCardAccessible { get; set; } - + public FileSystemProxyCore(FileSystemCreators fsCreators, ExternalKeySet externalKeys, IDeviceOperator deviceOperator) { FsCreators = fsCreators; @@ -30,6 +34,587 @@ namespace LibHac.FsService DeviceOperator = deviceOperator; } + public Result OpenFileSystem(out IFileSystem fileSystem, U8Span path, FileSystemProxyType type, + bool canMountSystemDataPrivate, TitleId titleId) + { + fileSystem = default; + + // 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)); + + // 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; + + // Don't continue if the rest of the path is empty + if (!shouldContinue) + return ResultFs.InvalidArgument.Log(); + + if (type == FileSystemProxyType.Logo && mountNameInfo.IsGameCard) + { + rc = OpenGameCardFileSystem(out fileSystem, new GameCardHandle(mountNameInfo.GcHandle), + GameCardPartition.Logo); + + if (rc.IsSuccess()) + return Result.Success; + + if (!ResultFs.PartitionNotFound.Includes(rc)) + return rc; + } + + rc = IsContentPathDir(ref path2, out bool isDirectory); + if (rc.IsFailure()) return rc; + + if (isDirectory) + { + 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); + + if (rc.IsSuccess()) + { + // Must be the end of the path to open Application Package FS type + if (path2.Length == 0 || path2[0] == 0) + { + if (type == FileSystemProxyType.Package) + { + fileSystem = nspFileSystem; + return Result.Success; + } + + return ResultFs.InvalidArgument.Log(); + } + + baseFileSystem = nspFileSystem; + } + + if (!mountNameInfo.CanMountNca) + { + return ResultFs.InvalidNcaMountPoint.Log(); + } + + TitleId openTitleId = mountNameInfo.IsHostFs ? new TitleId(ulong.MaxValue) : titleId; + + rc = TryOpenNca(ref path2, out Nca nca, baseFileSystem, openTitleId); + if (rc.IsFailure()) return rc; + + rc = OpenNcaStorage(out IStorage ncaSectionStorage, nca, out NcaFormatType fsType, type, + mountNameInfo.IsGameCard, canMountSystemDataPrivate); + if (rc.IsFailure()) return rc; + + switch (fsType) + { + case NcaFormatType.Romfs: + return FsCreators.RomFileSystemCreator.Create(out fileSystem, ncaSectionStorage); + case NcaFormatType.Pfs0: + return FsCreators.PartitionFileSystemCreator.Create(out fileSystem, ncaSectionStorage); + default: + return ResultFs.InvalidNcaFsType.Log(); + } + } + + /// + /// Stores info obtained by parsing a common mount name. + /// + private struct MountNameInfo + { + public bool IsGameCard; + public int GcHandle; + public bool IsHostFs; + public bool CanMountNca; + } + + private Result OpenFileSystemFromMountName(ref U8Span path, out IFileSystem fileSystem, out bool shouldContinue, + out MountNameInfo info) + { + fileSystem = default; + + info = new MountNameInfo(); + 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; + } + + else if (StringUtils.Compare(path, CommonMountNames.ContentStorageSystemMountName, + CommonMountNames.ContentStorageSystemMountName.Length) == 0) + { + path = path.Slice(CommonMountNames.ContentStorageSystemMountName.Length); + + Result rc = OpenContentStorageFileSystem(out fileSystem, ContentStorageId.System); + if (rc.IsFailure()) return rc; + + info.CanMountNca = true; + } + + else if (StringUtils.Compare(path, CommonMountNames.ContentStorageUserMountName, + CommonMountNames.ContentStorageUserMountName.Length) == 0) + { + path = path.Slice(CommonMountNames.ContentStorageUserMountName.Length); + + Result rc = OpenContentStorageFileSystem(out fileSystem, ContentStorageId.User); + if (rc.IsFailure()) return rc; + + info.CanMountNca = true; + } + + else if (StringUtils.Compare(path, CommonMountNames.ContentStorageSdCardMountName, + CommonMountNames.ContentStorageSdCardMountName.Length) == 0) + { + path = path.Slice(CommonMountNames.ContentStorageSdCardMountName.Length); + + Result rc = OpenContentStorageFileSystem(out fileSystem, ContentStorageId.SdCard); + if (rc.IsFailure()) return rc; + + info.CanMountNca = true; + } + + else if (StringUtils.Compare(path, CommonMountNames.BisCalibrationFilePartitionMountName, + CommonMountNames.BisCalibrationFilePartitionMountName.Length) == 0) + { + path = path.Slice(CommonMountNames.BisCalibrationFilePartitionMountName.Length); + + Result rc = OpenBisFileSystem(out fileSystem, string.Empty, BisPartitionId.CalibrationFile); + if (rc.IsFailure()) return rc; + } + + else if (StringUtils.Compare(path, CommonMountNames.BisSafeModePartitionMountName, + CommonMountNames.BisSafeModePartitionMountName.Length) == 0) + { + path = path.Slice(CommonMountNames.BisSafeModePartitionMountName.Length); + + Result rc = OpenBisFileSystem(out fileSystem, string.Empty, BisPartitionId.SafeMode); + if (rc.IsFailure()) return rc; + } + + else if (StringUtils.Compare(path, CommonMountNames.BisUserPartitionMountName, + CommonMountNames.BisUserPartitionMountName.Length) == 0) + { + path = path.Slice(CommonMountNames.BisUserPartitionMountName.Length); + + Result rc = OpenBisFileSystem(out fileSystem, string.Empty, BisPartitionId.User); + if (rc.IsFailure()) return rc; + } + + else if (StringUtils.Compare(path, CommonMountNames.BisSystemPartitionMountName, + CommonMountNames.BisSystemPartitionMountName.Length) == 0) + { + path = path.Slice(CommonMountNames.BisSystemPartitionMountName.Length); + + Result rc = OpenBisFileSystem(out fileSystem, string.Empty, BisPartitionId.System); + if (rc.IsFailure()) return rc; + } + + else if (StringUtils.Compare(path, CommonMountNames.SdCardFileSystemMountName, + CommonMountNames.SdCardFileSystemMountName.Length) == 0) + { + path = path.Slice(CommonMountNames.SdCardFileSystemMountName.Length); + + Result rc = OpenSdCardFileSystem(out fileSystem); + if (rc.IsFailure()) return rc; + } + + else if (StringUtils.Compare(path, CommonMountNames.HostRootFileSystemMountName, + CommonMountNames.HostRootFileSystemMountName.Length) == 0) + { + path = path.Slice(CommonMountNames.HostRootFileSystemMountName.Length); + + info.IsHostFs = true; + info.CanMountNca = true; + + Result rc = OpenHostFileSystem(out fileSystem, U8Span.Empty, openCaseSensitive: false); + if (rc.IsFailure()) return rc; + } + + else if (StringUtils.Compare(path, CommonMountNames.RegisteredUpdatePartitionMountName, + CommonMountNames.RegisteredUpdatePartitionMountName.Length) == 0) + { + path = path.Slice(CommonMountNames.RegisteredUpdatePartitionMountName.Length); + + info.CanMountNca = true; + + throw new NotImplementedException(); + } + + else + { + return ResultFs.PathNotFound.Log(); + } + + if (StringUtils.GetLength(path, FsPath.MaxLength) == 0) + { + shouldContinue = false; + } + + return Result.Success; + } + + private Result IsContentPathDir(ref U8Span path, out bool isDirectory) + { + isDirectory = default; + + ReadOnlySpan mountSeparator = new[] { (byte)':', (byte)'/' }; + + if (StringUtils.Compare(mountSeparator, path, mountSeparator.Length) != 0) + { + return ResultFs.PathNotFound.Log(); + } + + path = path.Slice(1); + int pathLen = StringUtils.GetLength(path); + + if (path[pathLen - 1] == '/') + { + isDirectory = true; + return Result.Success; + } + + // Now make sure the path has a content file extension + if (pathLen < 5) + return ResultFs.PathNotFound.Log(); + + ReadOnlySpan fileExtension = path.Value.Slice(pathLen - 4); + + ReadOnlySpan ncaExtension = new[] { (byte)'.', (byte)'n', (byte)'c', (byte)'a' }; + ReadOnlySpan nspExtension = new[] { (byte)'.', (byte)'n', (byte)'s', (byte)'p' }; + + if (StringUtils.CompareCaseInsensitive(fileExtension, ncaExtension) == 0 || + StringUtils.CompareCaseInsensitive(fileExtension, nspExtension) == 0) + { + isDirectory = false; + return Result.Success; + } + + 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 sb = new U8StringBuilder(fullPath.Str); + sb.Append(path) + .Append(new[] { (byte)'d', (byte)'a', (byte)'t', (byte)'a', (byte)'/' }); + + if (sb.Overflowed) + return ResultFs.TooLongPath.Log(); + + Result 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' }; + + // Search for the end of the nsp part of the path + int nspPathLen = 0; + + while (true) + { + U8Span currentSpan; + + while (true) + { + currentSpan = path.Slice(nspPathLen); + if (StringUtils.CompareCaseInsensitive(nspExtension, currentSpan, 4) == 0) + break; + + if (currentSpan.Length == 0 || currentSpan[0] == 0) + { + return ResultFs.PathNotFound.Log(); + } + + nspPathLen++; + } + + // The nsp filename must be the end of the entire path or the end of a path segment + if (currentSpan.Length <= 4 || currentSpan[4] == 0 || currentSpan[4] == (byte)'/') + break; + + nspPathLen += 4; + } + + nspPathLen += 4; + + if (nspPathLen > FsPath.MaxLength + 1) + return ResultFs.TooLongPath.Log(); + + Result rc = FsPath.FromSpan(out FsPath nspPath, path.Slice(0, nspPathLen)); + if (rc.IsFailure()) return rc; + + rc = FileStorageBasedFileSystem.CreateNew(out FileStorageBasedFileSystem nspFileStorage, baseFileSystem, + new U8Span(nspPath.Str), OpenMode.Read); + if (rc.IsFailure()) return rc; + + rc = FsCreators.PartitionFileSystemCreator.Create(out outFileSystem, nspFileStorage); + + if (rc.IsSuccess()) + { + path = path.Slice(nspPathLen); + } + + return rc; + } + + private Result TryOpenNca(ref U8Span path, out Nca nca, IFileSystem baseFileSystem, TitleId programId) + { + nca = default; + + Result rc = FileStorageBasedFileSystem.CreateNew(out FileStorageBasedFileSystem ncaFileStorage, + baseFileSystem, path, OpenMode.Read); + if (rc.IsFailure()) return rc; + + rc = FsCreators.StorageOnNcaCreator.OpenNca(out Nca ncaTemp, ncaFileStorage); + if (rc.IsFailure()) return rc; + + if (programId.Value == ulong.MaxValue) + { + ulong ncaProgramId = ncaTemp.Header.TitleId; + + if (ncaProgramId != ulong.MaxValue && programId.Value != ncaProgramId) + { + return ResultFs.InvalidNcaProgramId.Log(); + } + } + + nca = ncaTemp; + return Result.Success; + } + + private Result OpenNcaStorage(out IStorage ncaStorage, Nca nca, out NcaFormatType fsType, + FileSystemProxyType fsProxyType, bool isGameCard, bool canMountSystemDataPrivate) + { + ncaStorage = default; + fsType = default; + + NcaContentType contentType = nca.Header.ContentType; + + switch (fsProxyType) + { + case FileSystemProxyType.Code: + case FileSystemProxyType.Rom: + case FileSystemProxyType.Logo: + case FileSystemProxyType.RegisteredUpdate: + if (contentType != NcaContentType.Program) + return ResultFs.PreconditionViolation.Log(); + + break; + + case FileSystemProxyType.Control: + if (contentType != NcaContentType.Control) + return ResultFs.PreconditionViolation.Log(); + + break; + case FileSystemProxyType.Manual: + if (contentType != NcaContentType.Manual) + return ResultFs.PreconditionViolation.Log(); + + break; + case FileSystemProxyType.Meta: + if (contentType != NcaContentType.Meta) + return ResultFs.PreconditionViolation.Log(); + + break; + case FileSystemProxyType.Data: + if (contentType != NcaContentType.Data && contentType != NcaContentType.PublicData) + return ResultFs.PreconditionViolation.Log(); + + if (contentType == NcaContentType.Data && !canMountSystemDataPrivate) + return ResultFs.PermissionDenied.Log(); + + break; + default: + return ResultFs.InvalidArgument.Log(); + } + + if (nca.Header.DistributionType == DistributionType.GameCard && !isGameCard) + return ResultFs.PermissionDenied.Log(); + + Result rc = SetNcaExternalKey(nca); + if (rc.IsFailure()) return rc; + + rc = GetNcaSectionIndex(out int sectionIndex, fsProxyType); + if (rc.IsFailure()) return rc; + + rc = FsCreators.StorageOnNcaCreator.Create(out ncaStorage, out NcaFsHeader fsHeader, nca, + sectionIndex, fsProxyType == FileSystemProxyType.Code); + if (rc.IsFailure()) return rc; + + fsType = fsHeader.FormatType; + return Result.Success; + } + + private Result SetNcaExternalKey(Nca nca) + { + var rightsId = new RightsId(nca.Header.RightsId); + var zero = new RightsId(0, 0); + + 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; + + // todo: Set key in nca reader + + return Result.Success; + } + + private Result GetNcaSectionIndex(out int index, FileSystemProxyType fspType) + { + switch (fspType) + { + case FileSystemProxyType.Code: + case FileSystemProxyType.Control: + case FileSystemProxyType.Manual: + case FileSystemProxyType.Meta: + case FileSystemProxyType.Data: + index = 0; + return Result.Success; + case FileSystemProxyType.Rom: + case FileSystemProxyType.RegisteredUpdate: + index = 1; + return Result.Success; + case FileSystemProxyType.Logo: + index = 2; + return Result.Success; + default: + index = default; + return ResultFs.InvalidArgument.Log(); + } + } + public Result OpenBisFileSystem(out IFileSystem fileSystem, string rootPath, BisPartitionId partitionId) { return FsCreators.BuiltInStorageFileSystemCreator.Create(out fileSystem, rootPath, partitionId); @@ -155,7 +740,54 @@ 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()) + { + ReadOnlySpan rootHostPath = new[] { (byte)'C', (byte)':', (byte)'/' }; + rc = hostFs.GetEntryType(out _, new U8Span(rootHostPath)); + + // 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/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); 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 || diff --git a/src/LibHac/FsSystem/PathTools.cs b/src/LibHac/FsSystem/PathTools.cs index 55aac6da..3e4ad0af 100644 --- a/src/LibHac/FsSystem/PathTools.cs +++ b/src/LibHac/FsSystem/PathTools.cs @@ -497,200 +497,6 @@ namespace LibHac.FsSystem private static bool IsValidMountNameChar(byte c) => IsValidMountNameChar((char)c); - private static Result GetPathRoot(out ReadOnlySpan afterRootPath, Span pathRootBuffer, out int outRootLength, ReadOnlySpan path) - { - outRootLength = 0; - afterRootPath = path; - - if (path.Length == 0) return Result.Success; - - int mountNameStart; - if (IsDirectorySeparator(path[0])) - { - mountNameStart = 1; - } - else - { - mountNameStart = 0; - } - - int rootLength = 0; - - for (int i = mountNameStart; i < mountNameStart + MountNameLengthMax; i++) - { - if (i >= path.Length || path[i] == 0) break; - - // Set the length to 0 if there's no mount name - if (IsDirectorySeparator(path[i])) - { - outRootLength = 0; - return Result.Success; - } - - if (path[i] == MountSeparator) - { - rootLength = i + 1; - break; - } - } - - if (mountNameStart >= rootLength - 1 || path[rootLength - 1] != MountSeparator) - { - return ResultFs.InvalidPathFormat.Log(); - } - - if (mountNameStart < rootLength) - { - for (int i = mountNameStart; i < rootLength; i++) - { - if (path[i] == '.') - { - return ResultFs.InvalidCharacter.Log(); - } - } - } - - if (!pathRootBuffer.IsEmpty) - { - if (rootLength > pathRootBuffer.Length) - { - return ResultFs.TooLongPath.Log(); - } - - path.Slice(0, rootLength).CopyTo(pathRootBuffer); - } - - afterRootPath = path.Slice(rootLength); - outRootLength = rootLength; - return Result.Success; - } - - public static Result Normalize(Span normalizedPath, out int normalizedLength, ReadOnlySpan path, bool hasMountName) - { - normalizedLength = 0; - - int rootLength = 0; - ReadOnlySpan mainPath = path; - - if (hasMountName) - { - Result pathRootRc = GetPathRoot(out mainPath, normalizedPath, out rootLength, path); - if (pathRootRc.IsFailure()) return pathRootRc; - } - - var sb = new PathBuilder(normalizedPath.Slice(rootLength)); - - var state = NormalizeState.Initial; - - for (int i = 0; i < mainPath.Length; i++) - { - Result rc = Result.Success; - byte c = mainPath[i]; - - // Read input strings as null-terminated - if (c == 0) break; - - switch (state) - { - case NormalizeState.Initial when IsDirectorySeparator(c): - state = NormalizeState.Delimiter; - break; - - case NormalizeState.Initial: - return ResultFs.InvalidPathFormat.Log(); - - case NormalizeState.Normal when IsDirectorySeparator(c): - state = NormalizeState.Delimiter; - break; - - case NormalizeState.Normal: - rc = sb.Append(c); - break; - - case NormalizeState.Delimiter when IsDirectorySeparator(c): - break; - - case NormalizeState.Delimiter when c == '.': - state = NormalizeState.Dot; - rc = sb.AppendWithPrecedingSeparator(c); - break; - - case NormalizeState.Delimiter: - state = NormalizeState.Normal; - rc = sb.AppendWithPrecedingSeparator(c); - break; - - case NormalizeState.Dot when IsDirectorySeparator(c): - state = NormalizeState.Delimiter; - rc = sb.GoUpLevels(1); - break; - - case NormalizeState.Dot when c == '.': - state = NormalizeState.DoubleDot; - rc = sb.Append(c); - break; - - case NormalizeState.Dot: - state = NormalizeState.Normal; - rc = sb.Append(c); - break; - - case NormalizeState.DoubleDot when IsDirectorySeparator(c): - state = NormalizeState.Delimiter; - rc = sb.GoUpLevels(2); - break; - - case NormalizeState.DoubleDot: - state = NormalizeState.Normal; - break; - } - - if (rc.IsFailure()) - { - if (ResultFs.TooLongPath.Includes(rc)) - { - // Make sure pending delimiters are added to the string if possible - if (state == NormalizeState.Delimiter) - { - sb.Append((byte)DirectorySeparator); - } - } - - normalizedLength = sb.Length; - sb.Terminate(); - return rc; - } - } - - Result finalRc = Result.Success; - - switch (state) - { - case NormalizeState.Dot: - state = NormalizeState.Delimiter; - finalRc = sb.GoUpLevels(1); - break; - - case NormalizeState.DoubleDot: - state = NormalizeState.Delimiter; - finalRc = sb.GoUpLevels(2); - break; - } - - // Add the pending delimiter if the path is empty - // or if the path has only a mount name with no trailing delimiter - if (state == NormalizeState.Delimiter && sb.Length == 0 || - rootLength > 0 && sb.Length == 0) - { - finalRc = sb.Append((byte)'/'); - } - - normalizedLength = sb.Length; - sb.Terminate(); - - return finalRc; - } - private enum NormalizeState { Initial, diff --git a/tests/LibHac.Tests/PathToolsTests.cs b/tests/LibHac.Tests/PathToolsTests.cs index 403a79ea..16d59c18 100644 --- a/tests/LibHac.Tests/PathToolsTests.cs +++ b/tests/LibHac.Tests/PathToolsTests.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using LibHac.Common; -using LibHac.Fs; using LibHac.FsSystem; using Xunit; @@ -172,110 +171,6 @@ namespace LibHac.Tests return paths.Select(x => new object[] { x }).ToArray(); } - public static object[][] NormalizedPathTestItemsU8NoMountName = - { - new object[] {"/", "/", Result.Success}, - new object[] {"/.", "/", Result.Success}, - new object[] {"/..", "", ResultFs.DirectoryUnobtainable.Value}, - new object[] {"/abc", "/abc", Result.Success}, - new object[] {"/a/..", "/", Result.Success}, - new object[] {"/a/b/c", "/a/b/c", Result.Success}, - new object[] {"/a/b/../c", "/a/c", Result.Success}, - new object[] {"/a/b/c/..", "/a/b", Result.Success}, - new object[] {"/a/b/c/.", "/a/b/c", Result.Success}, - new object[] {"/a/../../..", "", ResultFs.DirectoryUnobtainable.Value}, - new object[] {"/a/../../../a/b/c", "", ResultFs.DirectoryUnobtainable.Value}, - new object[] {"//a/b//.//c", "/a/b/c", Result.Success}, - new object[] {"/../a/b/c/.", "", ResultFs.DirectoryUnobtainable.Value}, - new object[] {"/./aaa/bbb/ccc/.", "/aaa/bbb/ccc", Result.Success}, - - new object[] {"/a/b/c/", "/a/b/c", Result.Success}, - new object[] {"/aa/./bb/../cc/", "/aa/cc", Result.Success}, - new object[] {"/./b/../c/", "/c", Result.Success}, - new object[] {"/a/../../../", "", ResultFs.DirectoryUnobtainable.Value}, - new object[] {"//a/b//.//c/", "/a/b/c", Result.Success}, - new object[] {"/tmp/../", "/", Result.Success}, - new object[] {"abc", "", ResultFs.InvalidPathFormat.Value } - }; - - public static object[][] NormalizedPathTestItemsU8MountName = - { - new object[] {"mount:/a/b/../c", "mount:/a/c", Result.Success}, - new object[] {"a:/a/b/c", "a:/a/b/c", Result.Success}, - new object[] {"mount:/a/b/../c", "mount:/a/c", Result.Success}, - new object[] {"mount:", "mount:/", Result.Success}, - new object[] {"abc:/a/../../../a/b/c", "", ResultFs.DirectoryUnobtainable.Value}, - new object[] {"abc:/./b/../c/", "abc:/c", Result.Success}, - new object[] {"abc:/.", "abc:/", Result.Success}, - new object[] {"abc:/..", "", ResultFs.DirectoryUnobtainable.Value}, - new object[] {"abc:/", "abc:/", Result.Success}, - new object[] {"abc://a/b//.//c", "abc:/a/b/c", Result.Success}, - new object[] {"abc:/././/././a/b//.//c", "abc:/a/b/c", Result.Success}, - new object[] {"mount:/d./aa", "mount:/d./aa", Result.Success}, - new object[] {"mount:/d/..", "mount:/", Result.Success} - }; - - [Theory] - [MemberData(nameof(NormalizedPathTestItemsU8NoMountName))] - public static void NormalizePathU8NoMountName(string path, string expected, Result expectedResult) - { - var u8Path = path.ToU8String(); - Span buffer = stackalloc byte[0x301]; - - Result rc = PathTools.Normalize(buffer, out _, u8Path, false); - - string actual = StringUtils.Utf8ZToString(buffer); - - Assert.Equal(expectedResult, rc); - if (expectedResult == Result.Success) - { - Assert.Equal(expected, actual); - } - } - - [Theory] - [MemberData(nameof(NormalizedPathTestItemsU8MountName))] - public static void NormalizePathU8MountName(string path, string expected, Result expectedResult) - { - var u8Path = path.ToU8String(); - Span buffer = stackalloc byte[0x301]; - - Result rc = PathTools.Normalize(buffer, out _, u8Path, true); - - string actual = StringUtils.Utf8ZToString(buffer); - - Assert.Equal(expectedResult, rc); - if (expectedResult == Result.Success) - { - Assert.Equal(expected, actual); - } - } - - public static object[][] NormalizedPathTestItemsU8TooShort = - { - new object[] {"/a/b/c", "", 0}, - new object[] {"/a/b/c", "/a/", 4}, - new object[] {"/a/b/c", "/a/b", 5}, - new object[] {"/a/b/c", "/a/b/", 6} - }; - - [Theory] - [MemberData(nameof(NormalizedPathTestItemsU8TooShort))] - public static void NormalizePathU8TooShortDest(string path, string expected, int destSize) - { - var u8Path = path.ToU8String(); - - Span buffer = stackalloc byte[destSize]; - - Result rc = PathTools.Normalize(buffer, out int normalizedLength, u8Path, false); - - string actual = StringUtils.Utf8ZToString(buffer); - - Assert.Equal(ResultFs.TooLongPath.Value, rc); - Assert.Equal(Math.Max(0, destSize - 1), normalizedLength); - Assert.Equal(expected, actual); - } - public static object[][] GetFileNameTestItems = { new object[] {"/a/bb/ccc", "ccc"},