Allow opening directories with OpenFileSystem

This commit is contained in:
Alex Barney 2020-03-13 00:09:44 -07:00
parent 036e048208
commit a006816a2e
10 changed files with 292 additions and 20 deletions

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

@ -15,5 +15,9 @@ namespace LibHac.Fs
public static readonly U8String SdCardFileSystemMountName = new U8String("@Sdcard");
public static readonly U8String HostRootFileSystemMountName = new U8String("@Host");
public static readonly U8String RegisteredUpdatePartitionMountName = new U8String("@RegUpdate");
public const char GameCardFileSystemMountNameUpdateSuffix = 'U';
public const char GameCardFileSystemMountNameNormalSuffix = 'N';
public const char GameCardFileSystemMountNameSecureSuffix = 'S';
}
}

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

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

@ -8,6 +8,7 @@ namespace LibHac.FsService.Creators
{
public class StorageOnNcaCreator : IStorageOnNcaCreator
{
// ReSharper disable once UnusedMember.Local
private bool IsEnabledProgramVerification { get; set; }
private Keyset Keyset { get; }
@ -39,7 +40,7 @@ namespace LibHac.FsService.Creators
storage = storageTemp;
fsHeader = nca.Header.GetFsHeader(fsIndex);
return Result.Success;
}

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

@ -1,4 +1,5 @@
using System;
using System.Buffers.Text;
using System.Runtime.CompilerServices;
using LibHac.Common;
using LibHac.Fs;
@ -41,11 +42,13 @@ namespace LibHac.FsService
// Get a reference to the path that will be advanced as each part of the path is parsed
U8Span path2 = path.Slice(0, StringUtils.GetLength(path));
Result rc = OpenFileSystemFromMountName(ref path2, out IFileSystem baseFileSystem, out bool successQQ,
// Open the root filesystem based on the path's mount name
Result rc = OpenFileSystemFromMountName(ref path2, out IFileSystem baseFileSystem, out bool shouldContinue,
out MountNameInfo mountNameInfo);
if (rc.IsFailure()) return rc;
if (!successQQ)
// Don't continue if the rest of the path is empty
if (!shouldContinue)
return ResultFs.InvalidArgument.Log();
if (type == FileSystemProxyType.Logo && mountNameInfo.IsGameCard)
@ -65,7 +68,19 @@ namespace LibHac.FsService
if (isDirectory)
{
throw new NotImplementedException();
if (!mountNameInfo.IsHostFs)
return ResultFs.PermissionDenied.Log();
if (type == FileSystemProxyType.Manual)
{
rc = TryOpenCaseSensitiveContentDirectory(out IFileSystem manualFileSystem, baseFileSystem, path2);
if (rc.IsFailure()) return rc;
fileSystem = new ReadOnlyFileSystem(manualFileSystem);
return Result.Success;
}
return TryOpenContentDirectory(path2, out fileSystem, baseFileSystem, type, true);
}
rc = TryOpenNsp(ref path2, out IFileSystem nspFileSystem, baseFileSystem);
@ -123,23 +138,52 @@ namespace LibHac.FsService
public bool CanMountNca;
}
private Result OpenFileSystemFromMountName(ref U8Span path, out IFileSystem fileSystem, out bool successQQ,
private Result OpenFileSystemFromMountName(ref U8Span path, out IFileSystem fileSystem, out bool shouldContinue,
out MountNameInfo info)
{
fileSystem = default;
info = new MountNameInfo();
successQQ = true;
shouldContinue = true;
if (StringUtils.Compare(path, CommonMountNames.GameCardFileSystemMountName,
CommonMountNames.GameCardFileSystemMountName.Length) == 0)
{
path = path.Slice(CommonMountNames.GameCardFileSystemMountName.Length);
if (StringUtils.GetLength(path.Value, 9) < 9)
return ResultFs.InvalidPath.Log();
GameCardPartition partition;
switch ((char)path[0])
{
case CommonMountNames.GameCardFileSystemMountNameUpdateSuffix:
partition = GameCardPartition.Update;
break;
case CommonMountNames.GameCardFileSystemMountNameNormalSuffix:
partition = GameCardPartition.Normal;
break;
case CommonMountNames.GameCardFileSystemMountNameSecureSuffix:
partition = GameCardPartition.Secure;
break;
default:
return ResultFs.InvalidPath.Log();
}
path = path.Slice(1);
bool handleParsed = Utf8Parser.TryParse(path, out int handle, out int bytesConsumed);
if (!handleParsed || handle == -1 || bytesConsumed != 8)
return ResultFs.InvalidPath.Log();
path = path.Slice(8);
Result rc = OpenGameCardFileSystem(out fileSystem, new GameCardHandle(handle), partition);
if (rc.IsFailure()) return rc;
info.GcHandle = handle;
info.IsGameCard = true;
info.CanMountNca = true;
throw new NotImplementedException();
}
else if (StringUtils.Compare(path, CommonMountNames.ContentStorageSystemMountName,
@ -228,7 +272,8 @@ namespace LibHac.FsService
info.IsHostFs = true;
info.CanMountNca = true;
throw new NotImplementedException();
Result rc = OpenHostFileSystem(out fileSystem, U8Span.Empty, openCaseSensitive: false);
if (rc.IsFailure()) return rc;
}
else if (StringUtils.Compare(path, CommonMountNames.RegisteredUpdatePartitionMountName,
@ -248,7 +293,7 @@ namespace LibHac.FsService
if (StringUtils.GetLength(path, FsPath.MaxLength) == 0)
{
successQQ = false;
shouldContinue = false;
}
return Result.Success;
@ -293,14 +338,106 @@ namespace LibHac.FsService
return ResultFs.PathNotFound.Log();
}
private Result TryOpenContentDirectory(U8Span path, out IFileSystem contentFileSystem,
IFileSystem baseFileSystem, FileSystemProxyType fsType, bool preserveUnc)
{
contentFileSystem = default;
FsPath fullPath;
unsafe { _ = &fullPath; } // workaround for CS0165
Result rc = FsCreators.SubDirectoryFileSystemCreator.Create(out IFileSystem subDirFs,
baseFileSystem, path, preserveUnc);
if (rc.IsFailure()) return rc;
return OpenSubDirectoryForFsType(out contentFileSystem, subDirFs, fsType);
}
private Result TryOpenCaseSensitiveContentDirectory(out IFileSystem contentFileSystem,
IFileSystem baseFileSystem, U8Span path)
{
contentFileSystem = default;
FsPath fullPath;
unsafe { _ = &fullPath; } // workaround for CS0165
var pathBuilder = new PathBuilder(fullPath.Str);
Result rc = pathBuilder.Append(path);
if (rc.IsFailure()) return rc;
rc = pathBuilder.Append(new[] { (byte)'d', (byte)'a', (byte)'t', (byte)'a', (byte)'/' });
if (rc.IsFailure()) return rc;
pathBuilder.Terminate();
rc = FsCreators.TargetManagerFileSystemCreator.GetCaseSensitivePath(out bool success, fullPath.Str);
if (rc.IsFailure()) return rc;
// Reopen the host filesystem as case sensitive
if (success)
{
baseFileSystem.Dispose();
rc = OpenHostFileSystem(out baseFileSystem, U8Span.Empty, openCaseSensitive: true);
if (rc.IsFailure()) return rc;
}
return FsCreators.SubDirectoryFileSystemCreator.Create(out contentFileSystem, baseFileSystem, fullPath);
}
private Result OpenSubDirectoryForFsType(out IFileSystem fileSystem, IFileSystem baseFileSystem,
FileSystemProxyType fsType)
{
fileSystem = default;
ReadOnlySpan<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' };
int searchEnd = path.Length - 4;
// Search for the end of the nsp part of the path
int nspPathLen = 0;
@ -447,6 +584,7 @@ namespace LibHac.FsService
if (Crypto.CryptoUtil.IsSameBytes(rightsId.AsBytes(), zero.AsBytes(), Unsafe.SizeOf<RightsId>()))
return Result.Success;
// ReSharper disable once UnusedVariable
Result rc = ExternalKeys.Get(rightsId, out AccessKey accessKey);
if (rc.IsFailure()) return rc;
@ -604,7 +742,53 @@ namespace LibHac.FsService
public Result OpenGameCardFileSystem(out IFileSystem fileSystem, GameCardHandle handle,
GameCardPartition partitionId)
{
return FsCreators.GameCardFileSystemCreator.Create(out fileSystem, handle, partitionId);
Result rc;
int tries = 0;
do
{
rc = FsCreators.GameCardFileSystemCreator.Create(out fileSystem, handle, partitionId);
if (!ResultFs.DataCorrupted.Includes(rc))
break;
tries++;
} while (tries < 2);
return rc;
}
public Result OpenHostFileSystem(out IFileSystem fileSystem, U8Span path, bool openCaseSensitive)
{
fileSystem = default;
Result rc;
if (!path.IsEmpty())
{
rc = Util.VerifyHostPath(path);
if (rc.IsFailure()) return rc;
}
rc = FsCreators.TargetManagerFileSystemCreator.Create(out IFileSystem hostFs, openCaseSensitive);
if (rc.IsFailure()) return rc;
if (path.IsEmpty())
{
rc = hostFs.GetEntryType(out _, "C:/".ToU8Span());
// Nintendo ignores all results other than this one
if (ResultFs.TargetNotFound.Includes(rc))
return rc;
fileSystem = hostFs;
return Result.Success;
}
rc = FsCreators.SubDirectoryFileSystemCreator.Create(out IFileSystem subDirFs, hostFs, path, preserveUnc: true);
if (rc.IsFailure()) return rc;
fileSystem = subDirFs;
return Result.Success;
}
public Result RegisterExternalKey(ref RightsId rightsId, ref AccessKey externalKey)

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