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:
Alex Barney 2021-04-01 01:12:53 -07:00
parent 6dbecd6257
commit 4ea2896b72
4 changed files with 520 additions and 67 deletions

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

View file

@ -39,6 +39,13 @@ namespace LibHac.Fs
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)
{
if (IsWindowsDrive(path))

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

View file

@ -1,39 +1,133 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text.Unicode;
using System.Threading;
using LibHac.Common;
using LibHac.Fs;
using LibHac.Fs.Fsa;
using LibHac.FsSystem.Impl;
using LibHac.Util;
using static LibHac.Fs.StringTraits;
namespace LibHac.FsSystem
{
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>
/// Opens a directory on local storage as an <see cref="IFileSystem"/>.
/// The directory will be created if it does not exist.
/// </summary>
/// <param name="basePath">The path that will be the root of the <see cref="LocalFileSystem"/>.</param>
public LocalFileSystem(string basePath)
/// <param name="rootPath">The path that will be the root of the <see cref="LocalFileSystem"/>.</param>
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);
@ -42,7 +136,14 @@ namespace LibHac.FsSystem
Result rc = PathNormalizer.Normalize(normalizedPath.Str, out _, path, false, false);
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;
}
@ -69,7 +170,7 @@ namespace LibHac.FsSystem
{
UnsafeHelpers.SkipParamInit(out attributes);
Result rc = ResolveFullPath(out string fullPath, path);
Result rc = ResolveFullPath(out string fullPath, path, true);
if (rc.IsFailure()) return rc;
rc = GetFileInfo(out FileInfo info, fullPath);
@ -86,7 +187,7 @@ namespace LibHac.FsSystem
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;
rc = GetFileInfo(out FileInfo info, fullPath);
@ -116,7 +217,7 @@ namespace LibHac.FsSystem
{
UnsafeHelpers.SkipParamInit(out fileSize);
Result rc = ResolveFullPath(out string fullPath, path);
Result rc = ResolveFullPath(out string fullPath, path, true);
if (rc.IsFailure()) return rc;
rc = GetFileInfo(out FileInfo info, fullPath);
@ -132,7 +233,7 @@ namespace LibHac.FsSystem
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;
rc = GetDirInfo(out DirectoryInfo dir, fullPath);
@ -153,7 +254,7 @@ namespace LibHac.FsSystem
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;
rc = GetFileInfo(out FileInfo file, fullPath);
@ -181,46 +282,58 @@ namespace LibHac.FsSystem
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;
rc = GetDirInfo(out DirectoryInfo dir, fullPath);
if (rc.IsFailure()) return rc;
return DeleteDirectoryInternal(dir, false);
return TargetLockedAvoidance.RetryToAvoidTargetLocked(
() => DeleteDirectoryInternal(dir, false), _fsClient);
}
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;
rc = GetDirInfo(out DirectoryInfo dir, fullPath);
if (rc.IsFailure()) return rc;
return DeleteDirectoryInternal(dir, true);
return TargetLockedAvoidance.RetryToAvoidTargetLocked(
() => DeleteDirectoryInternal(dir, true), _fsClient);
}
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;
foreach (string file in Directory.EnumerateFiles(fullPath))
{
rc = GetFileInfo(out FileInfo fileInfo, file);
if (rc.IsFailure()) return rc;
rc = TargetLockedAvoidance.RetryToAvoidTargetLocked(
() =>
{
rc = GetFileInfo(out FileInfo fileInfo, file);
if (rc.IsFailure()) return rc;
return DeleteFileInternal(fileInfo);
}, _fsClient);
rc = DeleteFileInternal(fileInfo);
if (rc.IsFailure()) return rc;
}
foreach (string dir in Directory.EnumerateDirectories(fullPath))
{
rc = GetDirInfo(out DirectoryInfo dirInfo, dir);
if (rc.IsFailure()) return rc;
rc = TargetLockedAvoidance.RetryToAvoidTargetLocked(
() =>
{
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;
}
@ -229,19 +342,20 @@ namespace LibHac.FsSystem
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;
rc = GetFileInfo(out FileInfo file, fullPath);
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)
{
UnsafeHelpers.SkipParamInit(out directory);
Result rc = ResolveFullPath(out string fullPath, path);
Result rc = ResolveFullPath(out string fullPath, path, true);
if (rc.IsFailure()) return rc;
rc = GetDirInfo(out DirectoryInfo dirInfo, fullPath);
@ -252,24 +366,20 @@ namespace LibHac.FsSystem
return ResultFs.PathNotFound.Log();
}
try
{
IEnumerator<FileSystemInfo> entryEnumerator = dirInfo.EnumerateFileSystemInfos().GetEnumerator();
IDirectory dirTemp = null;
rc = TargetLockedAvoidance.RetryToAvoidTargetLocked(() =>
OpenDirectoryInternal(out dirTemp, mode, dirInfo), _fsClient);
if (rc.IsFailure()) return rc;
directory = new LocalDirectory(entryEnumerator, dirInfo, mode);
return Result.Success;
}
catch (Exception ex) when (ex.HResult < 0)
{
return HResult.HResultToHorizonResult(ex.HResult).Log();
}
directory = dirTemp;
return Result.Success;
}
protected override Result DoOpenFile(out IFile file, U8Span path, OpenMode mode)
{
UnsafeHelpers.SkipParamInit(out file);
Result rc = ResolveFullPath(out string fullPath, path);
Result rc = ResolveFullPath(out string fullPath, path, true);
if (rc.IsFailure()) return rc;
rc = GetEntryType(out DirectoryEntryType entryType, path);
@ -280,7 +390,10 @@ namespace LibHac.FsSystem
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;
file = new LocalFile(fileStream, mode);
@ -292,10 +405,10 @@ namespace LibHac.FsSystem
Result rc = CheckSubPath(oldPath, newPath);
if (rc.IsFailure()) return rc;
rc = ResolveFullPath(out string fullCurrentPath, oldPath);
rc = ResolveFullPath(out string fullCurrentPath, oldPath, true);
if (rc.IsFailure()) return rc;
rc = ResolveFullPath(out string fullNewPath, newPath);
rc = ResolveFullPath(out string fullNewPath, newPath, false);
if (rc.IsFailure()) return rc;
// Official FS behavior is to do nothing in this case
@ -307,15 +420,16 @@ namespace LibHac.FsSystem
rc = GetDirInfo(out DirectoryInfo newDirInfo, fullNewPath);
if (rc.IsFailure()) return rc;
return RenameDirInternal(currentDirInfo, newDirInfo);
return TargetLockedAvoidance.RetryToAvoidTargetLocked(
() => RenameDirInternal(currentDirInfo, newDirInfo), _fsClient);
}
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;
rc = ResolveFullPath(out string fullNewPath, newPath);
rc = ResolveFullPath(out string fullNewPath, newPath, false);
if (rc.IsFailure()) return rc;
// Official FS behavior is to do nothing in this case
@ -327,14 +441,15 @@ namespace LibHac.FsSystem
rc = GetFileInfo(out FileInfo newFileInfo, fullNewPath);
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)
{
UnsafeHelpers.SkipParamInit(out entryType);
Result rc = ResolveFullPath(out string fullPath, path);
Result rc = ResolveFullPath(out string fullPath, path, true);
if (rc.IsFailure()) return rc;
rc = GetDirInfo(out DirectoryInfo dir, fullPath);
@ -362,7 +477,7 @@ namespace LibHac.FsSystem
{
UnsafeHelpers.SkipParamInit(out timeStamp);
Result rc = ResolveFullPath(out string fullPath, path);
Result rc = ResolveFullPath(out string fullPath, path, true);
if (rc.IsFailure()) return rc;
rc = GetFileInfo(out FileInfo file, fullPath);
@ -370,22 +485,43 @@ namespace LibHac.FsSystem
if (!file.Exists) return ResultFs.PathNotFound.Log();
timeStamp.Created = new DateTimeOffset(file.CreationTimeUtc).ToUnixTimeSeconds();
timeStamp.Accessed = new DateTimeOffset(file.LastAccessTimeUtc).ToUnixTimeSeconds();
timeStamp.Modified = new DateTimeOffset(file.LastWriteTime).ToUnixTimeSeconds();
if (_useUnixTime)
{
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;
}
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;
}
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;
}
@ -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)
{
UnsafeHelpers.SkipParamInit(out fileSize);
@ -472,7 +625,8 @@ namespace LibHac.FsSystem
private static Result DeleteDirectoryInternal(DirectoryInfo dir, bool recursive)
{
if (!dir.Exists) return ResultFs.PathNotFound.Log();
if (!dir.Exists)
return ResultFs.PathNotFound.Log();
try
{
@ -483,14 +637,13 @@ namespace LibHac.FsSystem
return HResult.HResultToHorizonResult(ex.HResult).Log();
}
EnsureDeleted(dir);
return Result.Success;
return EnsureDeleted(dir);
}
private static Result DeleteFileInternal(FileInfo file)
{
if (!file.Exists) return ResultFs.PathNotFound.Log();
if (!file.Exists)
return ResultFs.PathNotFound.Log();
try
{
@ -501,9 +654,7 @@ namespace LibHac.FsSystem
return HResult.HResultToHorizonResult(ex.HResult).Log();
}
EnsureDeleted(file);
return Result.Success;
return EnsureDeleted(file);
}
private static Result CreateDirInternal(DirectoryInfo dir, NxFileAttributes attributes)
@ -594,10 +745,11 @@ namespace LibHac.FsSystem
// Delete operations on IFileSystem should be synchronous
// DeleteFile and RemoveDirectory only mark the file for deletion on Windows,
// 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 retryDelay = 500;
const int delayRetryCount = 100;
const int retryDelay = 10;
// The entry is usually deleted within the first 5-10 tries
for (int i = 0; i < noDelayRetryCount; i++)
@ -605,15 +757,210 @@ namespace LibHac.FsSystem
entry.Refresh();
if (!entry.Exists)
return;
return Result.Success;
}
// Nintendo's solution is to check every 500 ms with no timeout
while (entry.Exists)
for (int i = 0; i < delayRetryCount; i++)
{
Thread.Sleep(retryDelay);
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;
}
}
}