Merge pull request #121 from Thealexbarney/open-filesystem

- Add OpenFileSystem in FileSystemProxy
- Add OpenHostFileSystem
- Add FS client shims for OpenHostFileSystem
This commit is contained in:
Alex Barney 2020-03-17 21:32:07 -07:00 committed by GitHub
commit cd5d46c81b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 1263 additions and 322 deletions

View File

@ -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,

1 Module,DescriptionStart,DescriptionEnd,Name,Summary
81 2,4645,,InvalidHashedPartitionFileSystemMagic, 2,4643,,InvalidPartitionFileSystemHash,
82 2,4646,,InvalidPartitionFileSystemEntryNameOffset, 2,4644,,InvalidPartitionFileSystemMagic,
83 2,4661,4679,BuiltInStorageCorrupted, 2,4645,,InvalidHashedPartitionFileSystemMagic,
84 2,4646,,InvalidPartitionFileSystemEntryNameOffset,
85 2,4661,4679,BuiltInStorageCorrupted,
86 2,4662,,InvalidGptPartitionSignature,
87 2,4681,4699,FatFileSystemCorrupted,
88 2,4701,4719,HostFileSystemCorrupted,

View File

@ -14,6 +14,8 @@ namespace LibHac.Common
public ReadOnlySpan<byte> Value => _buffer;
public int Length => _buffer.Length;
public static U8Span Empty => default;
public byte this[int i]
{
get => _buffer[i];

View File

@ -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<byte> buffer)

View File

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

View File

@ -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)
{

View File

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

View File

@ -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<Result> operation,
Func<string> 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]

View File

@ -187,4 +187,11 @@ namespace LibHac.Fs
Nand = 1,
SdCard = 2
}
[Flags]
public enum MountHostOption
{
None = 0,
PseudoCaseSensitive = 1
}
}

View File

@ -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)
{

View File

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

View File

@ -171,6 +171,10 @@ namespace LibHac.Fs
/// <summary>Error code: 2002-4501; Range: 4501-4599; Inner value: 0x232a02</summary>
public static Result.Base NcaCorrupted { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new Result.Base(ModuleFs, 4501, 4599); }
/// <summary>Error code: 2002-4512; Inner value: 0x234002</summary>
public static Result.Base InvalidNcaFsType => new Result.Base(ModuleFs, 4512);
/// <summary>Error code: 2002-4527; Inner value: 0x235e02</summary>
public static Result.Base InvalidNcaProgramId => new Result.Base(ModuleFs, 4527);
/// <summary>Error code: 2002-4601; Range: 4601-4639; Inner value: 0x23f202</summary>
public static Result.Base IntegrityVerificationStorageCorrupted { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new Result.Base(ModuleFs, 4601, 4639); }

374
src/LibHac/Fs/Shim/Host.cs Normal file
View File

@ -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
{
/// <summary>
/// Contains functions for mounting file systems from a host computer.
/// </summary>
/// <remarks>
/// All functions in this file are based on SDK 9.3
/// </remarks>
public static class Host
{
private static ReadOnlySpan<byte> 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<byte> 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<byte> nameBuffer)
{
const int requiredNameBufferSize = HostRootFileSystemPathLength;
Debug.Assert(nameBuffer.Length >= requiredNameBufferSize);
int size = StringUtils.Copy(nameBuffer, HostRootFileSystemPath);
Debug.Assert(size == requiredNameBufferSize - 1);
return Result.Success;
}
}
/// <summary>
/// Mounts the C:\ drive of a host Windows computer at @Host:/
/// </summary>
/// <param name="fs">The <see cref="FileSystemClient"/> to use.</param>
/// <returns>The <see cref="Result"/> of the operation.</returns>
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;
}
/// <summary>
/// Mounts the C:\ drive of a host Windows computer at @Host:/
/// </summary>
/// <param name="fs">The <see cref="FileSystemClient"/> to use.</param>
/// <param name="option">Options for mounting the host file system.</param>
/// <returns>The <see cref="Result"/> of the operation.</returns>
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;
}
/// <summary>
/// Unmounts the file system at @Host:/
/// </summary>
/// <param name="fs">The <see cref="FileSystemClient"/> to use.</param>
public static void UnmountHostRoot(this FileSystemClient fs)
{
fs.Unmount(HostRootFileSystemMountName);
}
/// <summary>
/// Mounts a directory on a host Windows computer at the specified mount point.
/// </summary>
/// <param name="fs">The <see cref="FileSystemClient"/> to use.</param>
/// <param name="mountName">The mount name at which the file system will be mounted.</param>
/// <param name="path">The path on the host computer to mount. e.g. C:\Windows\System32</param>
/// <returns>The <see cref="Result"/> of the operation.</returns>
public static Result MountHost(this FileSystemClient fs, U8Span mountName, U8Span path)
{
return MountHostImpl(fs, mountName, path, null);
}
/// <summary>
/// Mounts a directory on a host Windows computer at the specified mount point.
/// </summary>
/// <param name="fs">The <see cref="FileSystemClient"/> to use.</param>
/// <param name="mountName">The mount name at which the file system will be mounted.</param>
/// <param name="path">The path on the host computer to mount. e.g. C:\Windows\System32</param>
/// <param name="option">Options for mounting the host file system.</param>
/// <returns>The <see cref="Result"/> of the operation.</returns>
public static Result MountHost(this FileSystemClient fs, U8Span mountName, U8Span path, MountHostOption option)
{
return MountHostImpl(fs, mountName, path, option);
}
/// <summary>
/// Mounts a directory on a host Windows computer at the specified mount point.
/// </summary>
/// <param name="fs">The <see cref="FileSystemClient"/> to use.</param>
/// <param name="mountName">The mount name at which the file system will be mounted.</param>
/// <param name="path">The path on the host computer to mount. e.g. C:\Windows\System32</param>
/// <param name="optionalOption">Options for mounting the host file system. Specifying this parameter is optional.</param>
/// <param name="caller">The caller of this function.</param>
/// <returns>The <see cref="Result"/> of the operation.</returns>
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;
}
/// <summary>
/// Creates an <see cref="ICommonMountNameGenerator"/> based on the <paramref name="mountName"/> and
/// <paramref name="path"/>, and verifies the <paramref name="mountName"/>.
/// </summary>
/// <param name="nameGenerator">If successful, the created <see cref="ICommonMountNameGenerator"/>.</param>
/// <param name="mountName">The mount name at which the file system will be mounted.</param>
/// <param name="path">The path that will be opened on the host computer. e.g. C:\Windows\System32</param>
/// <returns>The <see cref="Result"/> of the operation.</returns>
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;
}
/// <summary>
/// Verifies parameters and opens a host file system.
/// </summary>
/// <param name="fs">The <see cref="FileSystemClient"/> to use.</param>
/// <param name="fileSystem">If successful, the opened host file system.</param>
/// <param name="mountName">The mount name to be verified.</param>
/// <param name="path">The path on the host computer to open. e.g. C:\Windows\System32</param>
/// <param name="option">Options for opening the host file system.</param>
/// <returns>The <see cref="Result"/> of the operation.</returns>
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);
}
/// <summary>
/// Opens a host file system via <see cref="IFileSystemProxy"/>.
/// </summary>
/// <param name="fs">The <see cref="FileSystemClient"/> to use.</param>
/// <param name="fileSystem">If successful, the opened host file system.</param>
/// <param name="path">The path on the host computer to open. e.g. /C:\Windows\System32/</param>
/// <param name="option">Options for opening the host file system.</param>
/// <returns>The <see cref="Result"/> of the operation.</returns>
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;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<byte> path)
{
throw new NotImplementedException();
}

View File

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

View File

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

View File

@ -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();
}
}
/// <summary>
/// Stores info obtained by parsing a common mount name.
/// </summary>
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<byte> 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<byte> fileExtension = path.Value.Slice(pathLen - 4);
ReadOnlySpan<byte> ncaExtension = new[] { (byte)'.', (byte)'n', (byte)'c', (byte)'a' };
ReadOnlySpan<byte> 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<byte> 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<byte> 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<RightsId>()))
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<byte> 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)

View File

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

View File

@ -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 ||

View File

@ -497,200 +497,6 @@ namespace LibHac.FsSystem
private static bool IsValidMountNameChar(byte c) => IsValidMountNameChar((char)c);
private static Result GetPathRoot(out ReadOnlySpan<byte> afterRootPath, Span<byte> pathRootBuffer, out int outRootLength, ReadOnlySpan<byte> 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<byte> normalizedPath, out int normalizedLength, ReadOnlySpan<byte> path, bool hasMountName)
{
normalizedLength = 0;
int rootLength = 0;
ReadOnlySpan<byte> 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,

View File

@ -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<byte> 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<byte> 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<byte> 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"},