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);
|
||||
}
|
||||
|
||||
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))
|
||||
|
|
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.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 = TargetLockedAvoidance.RetryToAvoidTargetLocked(
|
||||
() =>
|
||||
{
|
||||
rc = GetFileInfo(out FileInfo fileInfo, file);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
rc = DeleteFileInternal(fileInfo);
|
||||
return DeleteFileInternal(fileInfo);
|
||||
}, _fsClient);
|
||||
|
||||
if (rc.IsFailure()) return rc;
|
||||
}
|
||||
|
||||
foreach (string dir in Directory.EnumerateDirectories(fullPath))
|
||||
{
|
||||
rc = TargetLockedAvoidance.RetryToAvoidTargetLocked(
|
||||
() =>
|
||||
{
|
||||
rc = GetDirInfo(out DirectoryInfo dirInfo, dir);
|
||||
if (rc.IsFailure()) return rc;
|
||||
|
||||
rc = DeleteDirectoryInternal(dirInfo, true);
|
||||
return DeleteDirectoryInternal(dirInfo, true);
|
||||
}, _fsClient);
|
||||
|
||||
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);
|
||||
directory = dirTemp;
|
||||
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)
|
||||
{
|
||||
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();
|
||||
|
||||
if (_useUnixTime)
|
||||
{
|
||||
timeStamp.Created = new DateTimeOffset(file.CreationTimeUtc).ToUnixTimeSeconds();
|
||||
timeStamp.Accessed = new DateTimeOffset(file.LastAccessTimeUtc).ToUnixTimeSeconds();
|
||||
timeStamp.Modified = new DateTimeOffset(file.LastWriteTime).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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue