diff --git a/src/LibHac/Common/InteropWin32.cs b/src/LibHac/Common/InteropWin32.cs new file mode 100644 index 00000000..69f55fae --- /dev/null +++ b/src/LibHac/Common/InteropWin32.cs @@ -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 bytes, Span 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 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 FileName => MemoryMarshal.CreateSpan(ref _fileName[0], 260); + public Span AlternateFileName => MemoryMarshal.CreateSpan(ref _alternateFileName[0], 14); + } + } +} diff --git a/src/LibHac/Fs/Common/WindowsPath.cs b/src/LibHac/Fs/Common/WindowsPath.cs index 6a96ef50..78eeae5a 100644 --- a/src/LibHac/Fs/Common/WindowsPath.cs +++ b/src/LibHac/Fs/Common/WindowsPath.cs @@ -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)) diff --git a/src/LibHac/FsSystem/Impl/TargetLockedAvoidance.cs b/src/LibHac/FsSystem/Impl/TargetLockedAvoidance.cs new file mode 100644 index 00000000..5a9cfc7f --- /dev/null +++ b/src/LibHac/FsSystem/Impl/TargetLockedAvoidance.cs @@ -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 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; + } + } +} diff --git a/src/LibHac/FsSystem/LocalFileSystem.cs b/src/LibHac/FsSystem/LocalFileSystem.cs index 718017c7..726ff612 100644 --- a/src/LibHac/FsSystem/LocalFileSystem.cs +++ b/src/LibHac/FsSystem/LocalFileSystem.cs @@ -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; } + /// + /// Specifies the case-sensitivity of a . + /// + public enum PathMode + { + /// + /// Uses the default case-sensitivity of the underlying file system. + /// + DefaultCaseSensitivity, + + /// + /// Treats the file system as case-sensitive. + /// + 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; + } /// /// Opens a directory on local storage as an . /// The directory will be created if it does not exist. /// - /// The path that will be the root of the . - public LocalFileSystem(string basePath) + /// The path that will be the root of the . + 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 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 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 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; } } }