mirror of
https://github.com/Thealexbarney/LibHac.git
synced 2024-11-14 10:49:41 +01:00
Improve LocalFileSystem
- Add case-sensitive mode. - Avoid TargetLocked results by waiting and retrying a few times. - Allow getting either windows or unix timestamps. - Try a finite number of times if an entry has been deleted before returning TargetLocked.
This commit is contained in:
parent
6dbecd6257
commit
4ea2896b72
4 changed files with 520 additions and 67 deletions
64
src/LibHac/Common/InteropWin32.cs
Normal file
64
src/LibHac/Common/InteropWin32.cs
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
using System;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
#pragma warning disable 649
|
||||||
|
|
||||||
|
namespace LibHac.Common
|
||||||
|
{
|
||||||
|
public static unsafe class InteropWin32
|
||||||
|
{
|
||||||
|
[DllImport("kernel32.dll")]
|
||||||
|
private static extern int MultiByteToWideChar(uint codePage, uint dwFlags, byte* lpMultiByteStr,
|
||||||
|
int cbMultiByte, char* lpWideCharStr, int cchWideChar);
|
||||||
|
|
||||||
|
public static int MultiByteToWideChar(int codePage, ReadOnlySpan<byte> bytes, Span<char> chars)
|
||||||
|
{
|
||||||
|
fixed (byte* pBytes = bytes)
|
||||||
|
fixed (char* pChars = chars)
|
||||||
|
{
|
||||||
|
return MultiByteToWideChar((uint)codePage, 0, pBytes, bytes.Length, pChars, chars.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll")]
|
||||||
|
public static extern bool FindClose(IntPtr handle);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll")]
|
||||||
|
public static extern IntPtr FindFirstFileW(char* lpFileName, Win32FindData* lpFindFileData);
|
||||||
|
|
||||||
|
public static IntPtr FindFirstFileW(ReadOnlySpan<char> fileName, out Win32FindData findFileData)
|
||||||
|
{
|
||||||
|
fixed (char* pfileName = fileName)
|
||||||
|
{
|
||||||
|
Unsafe.SkipInit(out findFileData);
|
||||||
|
return FindFirstFileW(pfileName, (Win32FindData*)Unsafe.AsPointer(ref findFileData));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct Win32FindData
|
||||||
|
{
|
||||||
|
public uint FileAttributes;
|
||||||
|
private uint _creationTimeLow;
|
||||||
|
private uint _creationTimeHigh;
|
||||||
|
private uint _lastAccessLow;
|
||||||
|
private uint _lastAccessHigh;
|
||||||
|
private uint _lastWriteLow;
|
||||||
|
private uint _lastWriteHigh;
|
||||||
|
private uint _fileSizeHigh;
|
||||||
|
private uint _fileSizeLow;
|
||||||
|
public uint Reserved0;
|
||||||
|
public uint Reserved1;
|
||||||
|
private fixed char _fileName[260];
|
||||||
|
private fixed char _alternateFileName[14];
|
||||||
|
|
||||||
|
public long CreationTime => (long)((ulong)_creationTimeHigh << 32 | _creationTimeLow);
|
||||||
|
public long LastAccessTime => (long)((ulong)_lastAccessHigh << 32 | _lastAccessLow);
|
||||||
|
public long LastWriteTime => (long)((ulong)_lastWriteHigh << 32 | _lastWriteLow);
|
||||||
|
public long FileSize => (long)_fileSizeHigh << 32 | _fileSizeLow;
|
||||||
|
|
||||||
|
public Span<char> FileName => MemoryMarshal.CreateSpan(ref _fileName[0], 260);
|
||||||
|
public Span<char> AlternateFileName => MemoryMarshal.CreateSpan(ref _alternateFileName[0], 14);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -39,6 +39,13 @@ namespace LibHac.Fs
|
||||||
path.GetUnsafe(0) == AltDirectorySeparator && path.GetUnsafe(1) == AltDirectorySeparator);
|
path.GetUnsafe(0) == AltDirectorySeparator && path.GetUnsafe(1) == AltDirectorySeparator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool IsUnc(string path)
|
||||||
|
{
|
||||||
|
return (uint)path.Length >= UncPathPrefixLength &&
|
||||||
|
(path[0] == DirectorySeparator && path[1] == DirectorySeparator ||
|
||||||
|
path[0] == AltDirectorySeparator && path[1] == AltDirectorySeparator);
|
||||||
|
}
|
||||||
|
|
||||||
public static int GetWindowsPathSkipLength(U8Span path)
|
public static int GetWindowsPathSkipLength(U8Span path)
|
||||||
{
|
{
|
||||||
if (IsWindowsDrive(path))
|
if (IsWindowsDrive(path))
|
||||||
|
|
35
src/LibHac/FsSystem/Impl/TargetLockedAvoidance.cs
Normal file
35
src/LibHac/FsSystem/Impl/TargetLockedAvoidance.cs
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
#nullable enable
|
||||||
|
using System;
|
||||||
|
using LibHac.Fs;
|
||||||
|
using LibHac.Os;
|
||||||
|
|
||||||
|
namespace LibHac.FsSystem.Impl
|
||||||
|
{
|
||||||
|
internal static class TargetLockedAvoidance
|
||||||
|
{
|
||||||
|
private const int RetryCount = 2;
|
||||||
|
private const int SleepTimeMs = 2;
|
||||||
|
|
||||||
|
// Allow usage outside of a Horizon context by using standard .NET APIs
|
||||||
|
public static Result RetryToAvoidTargetLocked(Func<Result> func, FileSystemClient? fs = null)
|
||||||
|
{
|
||||||
|
Result rc = func();
|
||||||
|
|
||||||
|
for (int i = 0; i < RetryCount && ResultFs.TargetLocked.Includes(rc); i++)
|
||||||
|
{
|
||||||
|
if (fs is null)
|
||||||
|
{
|
||||||
|
System.Threading.Thread.Sleep(SleepTimeMs);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
fs.Hos.Os.SleepThread(TimeSpan.FromMilliSeconds(SleepTimeMs));
|
||||||
|
}
|
||||||
|
|
||||||
|
rc = func();
|
||||||
|
}
|
||||||
|
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,39 +1,133 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Buffers;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Text.Unicode;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using LibHac.Common;
|
using LibHac.Common;
|
||||||
using LibHac.Fs;
|
using LibHac.Fs;
|
||||||
using LibHac.Fs.Fsa;
|
using LibHac.Fs.Fsa;
|
||||||
|
using LibHac.FsSystem.Impl;
|
||||||
|
using LibHac.Util;
|
||||||
|
using static LibHac.Fs.StringTraits;
|
||||||
|
|
||||||
namespace LibHac.FsSystem
|
namespace LibHac.FsSystem
|
||||||
{
|
{
|
||||||
public class LocalFileSystem : IAttributeFileSystem
|
public class LocalFileSystem : IAttributeFileSystem
|
||||||
{
|
{
|
||||||
private string BasePath { get; }
|
/// <summary>
|
||||||
|
/// Specifies the case-sensitivity of a <see cref="LocalFileSystem"/>.
|
||||||
|
/// </summary>
|
||||||
|
public enum PathMode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Uses the default case-sensitivity of the underlying file system.
|
||||||
|
/// </summary>
|
||||||
|
DefaultCaseSensitivity,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Treats the file system as case-sensitive.
|
||||||
|
/// </summary>
|
||||||
|
CaseSensitive
|
||||||
|
}
|
||||||
|
|
||||||
|
private string _rootPath;
|
||||||
|
private readonly FileSystemClient _fsClient;
|
||||||
|
private PathMode _mode;
|
||||||
|
private readonly bool _useUnixTime;
|
||||||
|
|
||||||
|
public LocalFileSystem() : this(true) { }
|
||||||
|
|
||||||
|
public LocalFileSystem(bool useUnixTimeStamps)
|
||||||
|
{
|
||||||
|
_useUnixTime = useUnixTimeStamps;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalFileSystem(FileSystemClient fsClient, bool useUnixTimeStamps) : this(useUnixTimeStamps)
|
||||||
|
{
|
||||||
|
_fsClient = fsClient;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Opens a directory on local storage as an <see cref="IFileSystem"/>.
|
/// Opens a directory on local storage as an <see cref="IFileSystem"/>.
|
||||||
/// The directory will be created if it does not exist.
|
/// The directory will be created if it does not exist.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="basePath">The path that will be the root of the <see cref="LocalFileSystem"/>.</param>
|
/// <param name="rootPath">The path that will be the root of the <see cref="LocalFileSystem"/>.</param>
|
||||||
public LocalFileSystem(string basePath)
|
public LocalFileSystem(string rootPath)
|
||||||
{
|
{
|
||||||
BasePath = Path.GetFullPath(basePath);
|
_rootPath = Path.GetFullPath(rootPath);
|
||||||
|
|
||||||
if (!Directory.Exists(BasePath))
|
if (!Directory.Exists(_rootPath))
|
||||||
{
|
{
|
||||||
if (File.Exists(BasePath))
|
if (File.Exists(_rootPath))
|
||||||
{
|
{
|
||||||
throw new DirectoryNotFoundException($"The specified path is a file. ({basePath})");
|
throw new DirectoryNotFoundException($"The specified path is a file. ({rootPath})");
|
||||||
}
|
}
|
||||||
|
|
||||||
Directory.CreateDirectory(BasePath);
|
Directory.CreateDirectory(_rootPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Result ResolveFullPath(out string fullPath, U8Span path)
|
public static Result Create(out LocalFileSystem fileSystem, string rootPath,
|
||||||
|
PathMode pathMode = PathMode.DefaultCaseSensitivity, bool ensurePathExists = true)
|
||||||
|
{
|
||||||
|
UnsafeHelpers.SkipParamInit(out fileSystem);
|
||||||
|
|
||||||
|
var localFs = new LocalFileSystem();
|
||||||
|
Result rc = localFs.Initialize(rootPath, pathMode, ensurePathExists);
|
||||||
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
|
fileSystem = localFs;
|
||||||
|
return Result.Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Result Initialize(string rootPath, PathMode pathMode, bool ensurePathExists)
|
||||||
|
{
|
||||||
|
if (rootPath == null)
|
||||||
|
return ResultFs.NullptrArgument.Log();
|
||||||
|
|
||||||
|
_mode = pathMode;
|
||||||
|
|
||||||
|
// If the root path is empty, we interpret any incoming paths as rooted paths.
|
||||||
|
if (rootPath == string.Empty)
|
||||||
|
{
|
||||||
|
_rootPath = rootPath;
|
||||||
|
return Result.Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_rootPath = Path.GetFullPath(rootPath);
|
||||||
|
}
|
||||||
|
catch (PathTooLongException)
|
||||||
|
{
|
||||||
|
return ResultFs.TooLongPath.Log();
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return ResultFs.InvalidCharacter.Log();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Directory.Exists(_rootPath))
|
||||||
|
{
|
||||||
|
if (!ensurePathExists || File.Exists(_rootPath))
|
||||||
|
return ResultFs.PathNotFound.Log();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(_rootPath);
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex.HResult < 0)
|
||||||
|
{
|
||||||
|
return HResult.HResultToHorizonResult(ex.HResult).Log();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Result ResolveFullPath(out string fullPath, U8Span path, bool checkCaseSensitivity)
|
||||||
{
|
{
|
||||||
UnsafeHelpers.SkipParamInit(out fullPath);
|
UnsafeHelpers.SkipParamInit(out fullPath);
|
||||||
|
|
||||||
|
@ -42,7 +136,14 @@ namespace LibHac.FsSystem
|
||||||
Result rc = PathNormalizer.Normalize(normalizedPath.Str, out _, path, false, false);
|
Result rc = PathNormalizer.Normalize(normalizedPath.Str, out _, path, false, false);
|
||||||
if (rc.IsFailure()) return rc;
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
fullPath = PathTools.Combine(BasePath, normalizedPath.ToString());
|
fullPath = PathTools.Combine(_rootPath, normalizedPath.ToString());
|
||||||
|
|
||||||
|
if (_mode == PathMode.CaseSensitive && checkCaseSensitivity)
|
||||||
|
{
|
||||||
|
rc = CheckPathCaseSensitively(fullPath);
|
||||||
|
if (rc.IsFailure()) return rc;
|
||||||
|
}
|
||||||
|
|
||||||
return Result.Success;
|
return Result.Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,7 +170,7 @@ namespace LibHac.FsSystem
|
||||||
{
|
{
|
||||||
UnsafeHelpers.SkipParamInit(out attributes);
|
UnsafeHelpers.SkipParamInit(out attributes);
|
||||||
|
|
||||||
Result rc = ResolveFullPath(out string fullPath, path);
|
Result rc = ResolveFullPath(out string fullPath, path, true);
|
||||||
if (rc.IsFailure()) return rc;
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
rc = GetFileInfo(out FileInfo info, fullPath);
|
rc = GetFileInfo(out FileInfo info, fullPath);
|
||||||
|
@ -86,7 +187,7 @@ namespace LibHac.FsSystem
|
||||||
|
|
||||||
protected override Result DoSetFileAttributes(U8Span path, NxFileAttributes attributes)
|
protected override Result DoSetFileAttributes(U8Span path, NxFileAttributes attributes)
|
||||||
{
|
{
|
||||||
Result rc = ResolveFullPath(out string fullPath, path);
|
Result rc = ResolveFullPath(out string fullPath, path, true);
|
||||||
if (rc.IsFailure()) return rc;
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
rc = GetFileInfo(out FileInfo info, fullPath);
|
rc = GetFileInfo(out FileInfo info, fullPath);
|
||||||
|
@ -116,7 +217,7 @@ namespace LibHac.FsSystem
|
||||||
{
|
{
|
||||||
UnsafeHelpers.SkipParamInit(out fileSize);
|
UnsafeHelpers.SkipParamInit(out fileSize);
|
||||||
|
|
||||||
Result rc = ResolveFullPath(out string fullPath, path);
|
Result rc = ResolveFullPath(out string fullPath, path, true);
|
||||||
if (rc.IsFailure()) return rc;
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
rc = GetFileInfo(out FileInfo info, fullPath);
|
rc = GetFileInfo(out FileInfo info, fullPath);
|
||||||
|
@ -132,7 +233,7 @@ namespace LibHac.FsSystem
|
||||||
|
|
||||||
protected override Result DoCreateDirectory(U8Span path, NxFileAttributes archiveAttribute)
|
protected override Result DoCreateDirectory(U8Span path, NxFileAttributes archiveAttribute)
|
||||||
{
|
{
|
||||||
Result rc = ResolveFullPath(out string fullPath, path);
|
Result rc = ResolveFullPath(out string fullPath, path, false);
|
||||||
if (rc.IsFailure()) return rc;
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
rc = GetDirInfo(out DirectoryInfo dir, fullPath);
|
rc = GetDirInfo(out DirectoryInfo dir, fullPath);
|
||||||
|
@ -153,7 +254,7 @@ namespace LibHac.FsSystem
|
||||||
|
|
||||||
protected override Result DoCreateFile(U8Span path, long size, CreateFileOptions options)
|
protected override Result DoCreateFile(U8Span path, long size, CreateFileOptions options)
|
||||||
{
|
{
|
||||||
Result rc = ResolveFullPath(out string fullPath, path);
|
Result rc = ResolveFullPath(out string fullPath, path, false);
|
||||||
if (rc.IsFailure()) return rc;
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
rc = GetFileInfo(out FileInfo file, fullPath);
|
rc = GetFileInfo(out FileInfo file, fullPath);
|
||||||
|
@ -181,46 +282,58 @@ namespace LibHac.FsSystem
|
||||||
|
|
||||||
protected override Result DoDeleteDirectory(U8Span path)
|
protected override Result DoDeleteDirectory(U8Span path)
|
||||||
{
|
{
|
||||||
Result rc = ResolveFullPath(out string fullPath, path);
|
Result rc = ResolveFullPath(out string fullPath, path, true);
|
||||||
if (rc.IsFailure()) return rc;
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
rc = GetDirInfo(out DirectoryInfo dir, fullPath);
|
rc = GetDirInfo(out DirectoryInfo dir, fullPath);
|
||||||
if (rc.IsFailure()) return rc;
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
return DeleteDirectoryInternal(dir, false);
|
return TargetLockedAvoidance.RetryToAvoidTargetLocked(
|
||||||
|
() => DeleteDirectoryInternal(dir, false), _fsClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Result DoDeleteDirectoryRecursively(U8Span path)
|
protected override Result DoDeleteDirectoryRecursively(U8Span path)
|
||||||
{
|
{
|
||||||
Result rc = ResolveFullPath(out string fullPath, path);
|
Result rc = ResolveFullPath(out string fullPath, path, true);
|
||||||
if (rc.IsFailure()) return rc;
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
rc = GetDirInfo(out DirectoryInfo dir, fullPath);
|
rc = GetDirInfo(out DirectoryInfo dir, fullPath);
|
||||||
if (rc.IsFailure()) return rc;
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
return DeleteDirectoryInternal(dir, true);
|
return TargetLockedAvoidance.RetryToAvoidTargetLocked(
|
||||||
|
() => DeleteDirectoryInternal(dir, true), _fsClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Result DoCleanDirectoryRecursively(U8Span path)
|
protected override Result DoCleanDirectoryRecursively(U8Span path)
|
||||||
{
|
{
|
||||||
Result rc = ResolveFullPath(out string fullPath, path);
|
Result rc = ResolveFullPath(out string fullPath, path, true);
|
||||||
if (rc.IsFailure()) return rc;
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
foreach (string file in Directory.EnumerateFiles(fullPath))
|
foreach (string file in Directory.EnumerateFiles(fullPath))
|
||||||
{
|
{
|
||||||
rc = GetFileInfo(out FileInfo fileInfo, file);
|
rc = TargetLockedAvoidance.RetryToAvoidTargetLocked(
|
||||||
if (rc.IsFailure()) return rc;
|
() =>
|
||||||
|
{
|
||||||
|
rc = GetFileInfo(out FileInfo fileInfo, file);
|
||||||
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
|
return DeleteFileInternal(fileInfo);
|
||||||
|
}, _fsClient);
|
||||||
|
|
||||||
rc = DeleteFileInternal(fileInfo);
|
|
||||||
if (rc.IsFailure()) return rc;
|
if (rc.IsFailure()) return rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (string dir in Directory.EnumerateDirectories(fullPath))
|
foreach (string dir in Directory.EnumerateDirectories(fullPath))
|
||||||
{
|
{
|
||||||
rc = GetDirInfo(out DirectoryInfo dirInfo, dir);
|
rc = TargetLockedAvoidance.RetryToAvoidTargetLocked(
|
||||||
if (rc.IsFailure()) return rc;
|
() =>
|
||||||
|
{
|
||||||
|
rc = GetDirInfo(out DirectoryInfo dirInfo, dir);
|
||||||
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
|
return DeleteDirectoryInternal(dirInfo, true);
|
||||||
|
}, _fsClient);
|
||||||
|
|
||||||
rc = DeleteDirectoryInternal(dirInfo, true);
|
|
||||||
if (rc.IsFailure()) return rc;
|
if (rc.IsFailure()) return rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -229,19 +342,20 @@ namespace LibHac.FsSystem
|
||||||
|
|
||||||
protected override Result DoDeleteFile(U8Span path)
|
protected override Result DoDeleteFile(U8Span path)
|
||||||
{
|
{
|
||||||
Result rc = ResolveFullPath(out string fullPath, path);
|
Result rc = ResolveFullPath(out string fullPath, path, true);
|
||||||
if (rc.IsFailure()) return rc;
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
rc = GetFileInfo(out FileInfo file, fullPath);
|
rc = GetFileInfo(out FileInfo file, fullPath);
|
||||||
if (rc.IsFailure()) return rc;
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
return DeleteFileInternal(file);
|
return TargetLockedAvoidance.RetryToAvoidTargetLocked(
|
||||||
|
() => DeleteFileInternal(file), _fsClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Result DoOpenDirectory(out IDirectory directory, U8Span path, OpenDirectoryMode mode)
|
protected override Result DoOpenDirectory(out IDirectory directory, U8Span path, OpenDirectoryMode mode)
|
||||||
{
|
{
|
||||||
UnsafeHelpers.SkipParamInit(out directory);
|
UnsafeHelpers.SkipParamInit(out directory);
|
||||||
Result rc = ResolveFullPath(out string fullPath, path);
|
Result rc = ResolveFullPath(out string fullPath, path, true);
|
||||||
if (rc.IsFailure()) return rc;
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
rc = GetDirInfo(out DirectoryInfo dirInfo, fullPath);
|
rc = GetDirInfo(out DirectoryInfo dirInfo, fullPath);
|
||||||
|
@ -252,24 +366,20 @@ namespace LibHac.FsSystem
|
||||||
return ResultFs.PathNotFound.Log();
|
return ResultFs.PathNotFound.Log();
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
IDirectory dirTemp = null;
|
||||||
{
|
rc = TargetLockedAvoidance.RetryToAvoidTargetLocked(() =>
|
||||||
IEnumerator<FileSystemInfo> entryEnumerator = dirInfo.EnumerateFileSystemInfos().GetEnumerator();
|
OpenDirectoryInternal(out dirTemp, mode, dirInfo), _fsClient);
|
||||||
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
directory = new LocalDirectory(entryEnumerator, dirInfo, mode);
|
directory = dirTemp;
|
||||||
return Result.Success;
|
return Result.Success;
|
||||||
}
|
|
||||||
catch (Exception ex) when (ex.HResult < 0)
|
|
||||||
{
|
|
||||||
return HResult.HResultToHorizonResult(ex.HResult).Log();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Result DoOpenFile(out IFile file, U8Span path, OpenMode mode)
|
protected override Result DoOpenFile(out IFile file, U8Span path, OpenMode mode)
|
||||||
{
|
{
|
||||||
UnsafeHelpers.SkipParamInit(out file);
|
UnsafeHelpers.SkipParamInit(out file);
|
||||||
|
|
||||||
Result rc = ResolveFullPath(out string fullPath, path);
|
Result rc = ResolveFullPath(out string fullPath, path, true);
|
||||||
if (rc.IsFailure()) return rc;
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
rc = GetEntryType(out DirectoryEntryType entryType, path);
|
rc = GetEntryType(out DirectoryEntryType entryType, path);
|
||||||
|
@ -280,7 +390,10 @@ namespace LibHac.FsSystem
|
||||||
return ResultFs.PathNotFound.Log();
|
return ResultFs.PathNotFound.Log();
|
||||||
}
|
}
|
||||||
|
|
||||||
rc = OpenFileInternal(out FileStream fileStream, fullPath, mode);
|
FileStream fileStream = null;
|
||||||
|
|
||||||
|
rc = TargetLockedAvoidance.RetryToAvoidTargetLocked(() =>
|
||||||
|
OpenFileInternal(out fileStream, fullPath, mode), _fsClient);
|
||||||
if (rc.IsFailure()) return rc;
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
file = new LocalFile(fileStream, mode);
|
file = new LocalFile(fileStream, mode);
|
||||||
|
@ -292,10 +405,10 @@ namespace LibHac.FsSystem
|
||||||
Result rc = CheckSubPath(oldPath, newPath);
|
Result rc = CheckSubPath(oldPath, newPath);
|
||||||
if (rc.IsFailure()) return rc;
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
rc = ResolveFullPath(out string fullCurrentPath, oldPath);
|
rc = ResolveFullPath(out string fullCurrentPath, oldPath, true);
|
||||||
if (rc.IsFailure()) return rc;
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
rc = ResolveFullPath(out string fullNewPath, newPath);
|
rc = ResolveFullPath(out string fullNewPath, newPath, false);
|
||||||
if (rc.IsFailure()) return rc;
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
// Official FS behavior is to do nothing in this case
|
// Official FS behavior is to do nothing in this case
|
||||||
|
@ -307,15 +420,16 @@ namespace LibHac.FsSystem
|
||||||
rc = GetDirInfo(out DirectoryInfo newDirInfo, fullNewPath);
|
rc = GetDirInfo(out DirectoryInfo newDirInfo, fullNewPath);
|
||||||
if (rc.IsFailure()) return rc;
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
return RenameDirInternal(currentDirInfo, newDirInfo);
|
return TargetLockedAvoidance.RetryToAvoidTargetLocked(
|
||||||
|
() => RenameDirInternal(currentDirInfo, newDirInfo), _fsClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Result DoRenameFile(U8Span oldPath, U8Span newPath)
|
protected override Result DoRenameFile(U8Span oldPath, U8Span newPath)
|
||||||
{
|
{
|
||||||
Result rc = ResolveFullPath(out string fullCurrentPath, oldPath);
|
Result rc = ResolveFullPath(out string fullCurrentPath, oldPath, true);
|
||||||
if (rc.IsFailure()) return rc;
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
rc = ResolveFullPath(out string fullNewPath, newPath);
|
rc = ResolveFullPath(out string fullNewPath, newPath, false);
|
||||||
if (rc.IsFailure()) return rc;
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
// Official FS behavior is to do nothing in this case
|
// Official FS behavior is to do nothing in this case
|
||||||
|
@ -327,14 +441,15 @@ namespace LibHac.FsSystem
|
||||||
rc = GetFileInfo(out FileInfo newFileInfo, fullNewPath);
|
rc = GetFileInfo(out FileInfo newFileInfo, fullNewPath);
|
||||||
if (rc.IsFailure()) return rc;
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
return RenameFileInternal(currentFileInfo, newFileInfo);
|
return TargetLockedAvoidance.RetryToAvoidTargetLocked(
|
||||||
|
() => RenameFileInternal(currentFileInfo, newFileInfo), _fsClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Result DoGetEntryType(out DirectoryEntryType entryType, U8Span path)
|
protected override Result DoGetEntryType(out DirectoryEntryType entryType, U8Span path)
|
||||||
{
|
{
|
||||||
UnsafeHelpers.SkipParamInit(out entryType);
|
UnsafeHelpers.SkipParamInit(out entryType);
|
||||||
|
|
||||||
Result rc = ResolveFullPath(out string fullPath, path);
|
Result rc = ResolveFullPath(out string fullPath, path, true);
|
||||||
if (rc.IsFailure()) return rc;
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
rc = GetDirInfo(out DirectoryInfo dir, fullPath);
|
rc = GetDirInfo(out DirectoryInfo dir, fullPath);
|
||||||
|
@ -362,7 +477,7 @@ namespace LibHac.FsSystem
|
||||||
{
|
{
|
||||||
UnsafeHelpers.SkipParamInit(out timeStamp);
|
UnsafeHelpers.SkipParamInit(out timeStamp);
|
||||||
|
|
||||||
Result rc = ResolveFullPath(out string fullPath, path);
|
Result rc = ResolveFullPath(out string fullPath, path, true);
|
||||||
if (rc.IsFailure()) return rc;
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
rc = GetFileInfo(out FileInfo file, fullPath);
|
rc = GetFileInfo(out FileInfo file, fullPath);
|
||||||
|
@ -370,22 +485,43 @@ namespace LibHac.FsSystem
|
||||||
|
|
||||||
if (!file.Exists) return ResultFs.PathNotFound.Log();
|
if (!file.Exists) return ResultFs.PathNotFound.Log();
|
||||||
|
|
||||||
timeStamp.Created = new DateTimeOffset(file.CreationTimeUtc).ToUnixTimeSeconds();
|
if (_useUnixTime)
|
||||||
timeStamp.Accessed = new DateTimeOffset(file.LastAccessTimeUtc).ToUnixTimeSeconds();
|
{
|
||||||
timeStamp.Modified = new DateTimeOffset(file.LastWriteTime).ToUnixTimeSeconds();
|
timeStamp.Created = new DateTimeOffset(file.CreationTimeUtc).ToUnixTimeSeconds();
|
||||||
|
timeStamp.Accessed = new DateTimeOffset(file.LastAccessTimeUtc).ToUnixTimeSeconds();
|
||||||
|
timeStamp.Modified = new DateTimeOffset(file.LastWriteTimeUtc).ToUnixTimeSeconds();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
timeStamp.Created = new DateTimeOffset(file.CreationTimeUtc).ToFileTime();
|
||||||
|
timeStamp.Accessed = new DateTimeOffset(file.LastAccessTimeUtc).ToFileTime();
|
||||||
|
timeStamp.Modified = new DateTimeOffset(file.LastWriteTimeUtc).ToFileTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
timeStamp.IsLocalTime = false;
|
||||||
|
|
||||||
return Result.Success;
|
return Result.Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Result DoGetFreeSpaceSize(out long freeSpace, U8Span path)
|
protected override Result DoGetFreeSpaceSize(out long freeSpace, U8Span path)
|
||||||
{
|
{
|
||||||
freeSpace = new DriveInfo(BasePath).AvailableFreeSpace;
|
UnsafeHelpers.SkipParamInit(out freeSpace);
|
||||||
|
|
||||||
|
Result rc = ResolveFullPath(out string fullPath, path, true);
|
||||||
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
|
freeSpace = new DriveInfo(fullPath).AvailableFreeSpace;
|
||||||
return Result.Success;
|
return Result.Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Result DoGetTotalSpaceSize(out long totalSpace, U8Span path)
|
protected override Result DoGetTotalSpaceSize(out long totalSpace, U8Span path)
|
||||||
{
|
{
|
||||||
totalSpace = new DriveInfo(BasePath).TotalSize;
|
UnsafeHelpers.SkipParamInit(out totalSpace);
|
||||||
|
|
||||||
|
Result rc = ResolveFullPath(out string fullPath, path, true);
|
||||||
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
|
totalSpace = new DriveInfo(fullPath).TotalSize;
|
||||||
return Result.Success;
|
return Result.Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -425,6 +561,23 @@ namespace LibHac.FsSystem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Result OpenDirectoryInternal(out IDirectory directory, OpenDirectoryMode mode,
|
||||||
|
DirectoryInfo dirInfo)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
IEnumerator<FileSystemInfo> entryEnumerator = dirInfo.EnumerateFileSystemInfos().GetEnumerator();
|
||||||
|
|
||||||
|
directory = new LocalDirectory(entryEnumerator, dirInfo, mode);
|
||||||
|
return Result.Success;
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex.HResult < 0)
|
||||||
|
{
|
||||||
|
UnsafeHelpers.SkipParamInit(out directory);
|
||||||
|
return HResult.HResultToHorizonResult(ex.HResult).Log();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static Result GetSizeInternal(out long fileSize, FileInfo file)
|
private static Result GetSizeInternal(out long fileSize, FileInfo file)
|
||||||
{
|
{
|
||||||
UnsafeHelpers.SkipParamInit(out fileSize);
|
UnsafeHelpers.SkipParamInit(out fileSize);
|
||||||
|
@ -472,7 +625,8 @@ namespace LibHac.FsSystem
|
||||||
|
|
||||||
private static Result DeleteDirectoryInternal(DirectoryInfo dir, bool recursive)
|
private static Result DeleteDirectoryInternal(DirectoryInfo dir, bool recursive)
|
||||||
{
|
{
|
||||||
if (!dir.Exists) return ResultFs.PathNotFound.Log();
|
if (!dir.Exists)
|
||||||
|
return ResultFs.PathNotFound.Log();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
@ -483,14 +637,13 @@ namespace LibHac.FsSystem
|
||||||
return HResult.HResultToHorizonResult(ex.HResult).Log();
|
return HResult.HResultToHorizonResult(ex.HResult).Log();
|
||||||
}
|
}
|
||||||
|
|
||||||
EnsureDeleted(dir);
|
return EnsureDeleted(dir);
|
||||||
|
|
||||||
return Result.Success;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Result DeleteFileInternal(FileInfo file)
|
private static Result DeleteFileInternal(FileInfo file)
|
||||||
{
|
{
|
||||||
if (!file.Exists) return ResultFs.PathNotFound.Log();
|
if (!file.Exists)
|
||||||
|
return ResultFs.PathNotFound.Log();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
@ -501,9 +654,7 @@ namespace LibHac.FsSystem
|
||||||
return HResult.HResultToHorizonResult(ex.HResult).Log();
|
return HResult.HResultToHorizonResult(ex.HResult).Log();
|
||||||
}
|
}
|
||||||
|
|
||||||
EnsureDeleted(file);
|
return EnsureDeleted(file);
|
||||||
|
|
||||||
return Result.Success;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Result CreateDirInternal(DirectoryInfo dir, NxFileAttributes attributes)
|
private static Result CreateDirInternal(DirectoryInfo dir, NxFileAttributes attributes)
|
||||||
|
@ -594,10 +745,11 @@ namespace LibHac.FsSystem
|
||||||
// Delete operations on IFileSystem should be synchronous
|
// Delete operations on IFileSystem should be synchronous
|
||||||
// DeleteFile and RemoveDirectory only mark the file for deletion on Windows,
|
// DeleteFile and RemoveDirectory only mark the file for deletion on Windows,
|
||||||
// so we need to poll the filesystem until it's actually gone
|
// so we need to poll the filesystem until it's actually gone
|
||||||
private static void EnsureDeleted(FileSystemInfo entry)
|
private static Result EnsureDeleted(FileSystemInfo entry)
|
||||||
{
|
{
|
||||||
const int noDelayRetryCount = 1000;
|
const int noDelayRetryCount = 1000;
|
||||||
const int retryDelay = 500;
|
const int delayRetryCount = 100;
|
||||||
|
const int retryDelay = 10;
|
||||||
|
|
||||||
// The entry is usually deleted within the first 5-10 tries
|
// The entry is usually deleted within the first 5-10 tries
|
||||||
for (int i = 0; i < noDelayRetryCount; i++)
|
for (int i = 0; i < noDelayRetryCount; i++)
|
||||||
|
@ -605,15 +757,210 @@ namespace LibHac.FsSystem
|
||||||
entry.Refresh();
|
entry.Refresh();
|
||||||
|
|
||||||
if (!entry.Exists)
|
if (!entry.Exists)
|
||||||
return;
|
return Result.Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nintendo's solution is to check every 500 ms with no timeout
|
for (int i = 0; i < delayRetryCount; i++)
|
||||||
while (entry.Exists)
|
|
||||||
{
|
{
|
||||||
Thread.Sleep(retryDelay);
|
Thread.Sleep(retryDelay);
|
||||||
entry.Refresh();
|
entry.Refresh();
|
||||||
|
|
||||||
|
if (!entry.Exists)
|
||||||
|
return Result.Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return ResultFs.TargetLocked.Log();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Result GetCaseSensitivePath(out int bytesWritten, Span<byte> buffer, U8Span path,
|
||||||
|
U8Span workingDirectoryPath)
|
||||||
|
{
|
||||||
|
UnsafeHelpers.SkipParamInit(out bytesWritten);
|
||||||
|
|
||||||
|
string pathUtf16 = StringUtils.Utf8ZToString(path);
|
||||||
|
string workingDirectoryPathUtf16 = StringUtils.Utf8ZToString(workingDirectoryPath);
|
||||||
|
|
||||||
|
Result rc = GetCaseSensitivePathFull(out string caseSensitivePath, out int rootPathLength, pathUtf16,
|
||||||
|
workingDirectoryPathUtf16);
|
||||||
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
|
OperationStatus status = Utf8.FromUtf16(caseSensitivePath.AsSpan(rootPathLength),
|
||||||
|
buffer.Slice(0, buffer.Length - 1), out _, out int utf8BytesWritten);
|
||||||
|
|
||||||
|
if (status == OperationStatus.DestinationTooSmall)
|
||||||
|
return ResultFs.TooLongPath.Log();
|
||||||
|
|
||||||
|
if (status == OperationStatus.InvalidData || status == OperationStatus.NeedMoreData)
|
||||||
|
return ResultFs.InvalidCharacter.Log();
|
||||||
|
|
||||||
|
buffer[utf8BytesWritten] = NullTerminator;
|
||||||
|
bytesWritten = utf8BytesWritten;
|
||||||
|
|
||||||
|
return Result.Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Result CheckPathCaseSensitively(string path)
|
||||||
|
{
|
||||||
|
Result rc = GetCaseSensitivePathFull(out string caseSensitivePath, out _, path, _rootPath);
|
||||||
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
|
if (path.Length != caseSensitivePath.Length)
|
||||||
|
return ResultFs.PathNotFound.Log();
|
||||||
|
|
||||||
|
for (int i = 0; i < path.Length; i++)
|
||||||
|
{
|
||||||
|
if (!(path[i] == caseSensitivePath[i] || WindowsPath.IsDosDelimiter(path[i]) &&
|
||||||
|
WindowsPath.IsDosDelimiter(caseSensitivePath[i])))
|
||||||
|
{
|
||||||
|
return ResultFs.PathNotFound.Log();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Result GetCaseSensitivePathFull(out string caseSensitivePath, out int rootPathLength,
|
||||||
|
string path, string workingDirectoryPath)
|
||||||
|
{
|
||||||
|
caseSensitivePath = default;
|
||||||
|
UnsafeHelpers.SkipParamInit(out rootPathLength);
|
||||||
|
|
||||||
|
string fullPath;
|
||||||
|
int workingDirectoryPathLength;
|
||||||
|
|
||||||
|
if (WindowsPath.IsPathRooted(path))
|
||||||
|
{
|
||||||
|
fullPath = path;
|
||||||
|
workingDirectoryPathLength = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// We only want to send back the relative part of the path starting with a '/', so
|
||||||
|
// track where the root path ends.
|
||||||
|
if (WindowsPath.IsDosDelimiter(workingDirectoryPath[^1]))
|
||||||
|
{
|
||||||
|
workingDirectoryPathLength = workingDirectoryPath.Length - 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
workingDirectoryPathLength = workingDirectoryPath.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
fullPath = Combine(workingDirectoryPath, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
Result rc = GetCorrectCasedPath(out caseSensitivePath, fullPath);
|
||||||
|
if (rc.IsFailure()) return rc;
|
||||||
|
|
||||||
|
rootPathLength = workingDirectoryPathLength;
|
||||||
|
return Result.Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Combine(string path1, string path2)
|
||||||
|
{
|
||||||
|
if (path1 == null || path2 == null) throw new NullReferenceException();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(path1)) return path2;
|
||||||
|
if (string.IsNullOrEmpty(path2)) return path1;
|
||||||
|
|
||||||
|
bool path1HasSeparator = WindowsPath.IsDosDelimiter(path1[path1.Length - 1]);
|
||||||
|
bool path2HasSeparator = WindowsPath.IsDosDelimiter(path2[0]);
|
||||||
|
|
||||||
|
if (!path1HasSeparator && !path2HasSeparator)
|
||||||
|
{
|
||||||
|
return path1 + DirectorySeparator + path2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path1HasSeparator ^ path2HasSeparator)
|
||||||
|
{
|
||||||
|
return path1 + path2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return path1 + path2.Substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly char[] SplitChars = { (char)DirectorySeparator, (char)AltDirectorySeparator };
|
||||||
|
|
||||||
|
// Copyright (c) Microsoft Corporation.
|
||||||
|
// Licensed under the MIT License.
|
||||||
|
public static Result GetCorrectCasedPath(out string casedPath, string path)
|
||||||
|
{
|
||||||
|
UnsafeHelpers.SkipParamInit(out casedPath);
|
||||||
|
|
||||||
|
string exactPath = string.Empty;
|
||||||
|
int itemsToSkip = 0;
|
||||||
|
if (WindowsPath.IsUnc(path))
|
||||||
|
{
|
||||||
|
// With the Split method, a UNC path like \\server\share, we need to skip
|
||||||
|
// trying to enumerate the server and share, so skip the first two empty
|
||||||
|
// strings, then server, and finally share name.
|
||||||
|
itemsToSkip = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (string item in path.Split(SplitChars))
|
||||||
|
{
|
||||||
|
if (itemsToSkip-- > 0)
|
||||||
|
{
|
||||||
|
// This handles the UNC server and share and 8.3 short path syntax
|
||||||
|
exactPath += item + (char)DirectorySeparator;
|
||||||
|
}
|
||||||
|
else if (string.IsNullOrEmpty(exactPath))
|
||||||
|
{
|
||||||
|
// This handles the drive letter or / root path start
|
||||||
|
exactPath = item + (char)DirectorySeparator;
|
||||||
|
}
|
||||||
|
else if (string.IsNullOrEmpty(item))
|
||||||
|
{
|
||||||
|
// This handles the trailing slash case
|
||||||
|
if (!exactPath.EndsWith((char)DirectorySeparator))
|
||||||
|
{
|
||||||
|
exactPath += (char)DirectorySeparator;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else if (item.Contains('~'))
|
||||||
|
{
|
||||||
|
// This handles short path names
|
||||||
|
exactPath += (char)DirectorySeparator + item;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Use GetFileSystemEntries to get the correct casing of this element
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string[] entries = Directory.GetFileSystemEntries(exactPath, item);
|
||||||
|
if (entries.Length > 0)
|
||||||
|
{
|
||||||
|
int itemIndex = entries[0].LastIndexOf((char)AltDirectorySeparator);
|
||||||
|
|
||||||
|
// GetFileSystemEntries will return paths in the root directory in this format: C:/Foo
|
||||||
|
if (itemIndex == -1)
|
||||||
|
{
|
||||||
|
itemIndex = entries[0].LastIndexOf((char)DirectorySeparator);
|
||||||
|
exactPath += entries[0].Substring(itemIndex + 1);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
exactPath += (char)DirectorySeparator + entries[0].Substring(itemIndex + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// If previous call didn't return anything, something failed so we just return the path we were given
|
||||||
|
return ResultFs.PathNotFound.Log();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// If we can't enumerate, we stop and just return the original path
|
||||||
|
return ResultFs.PathNotFound.Log();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
casedPath = exactPath;
|
||||||
|
return Result.Success;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue