More closely match original FS behavior in IFileSystem

This commit is contained in:
Alex Barney 2019-07-07 15:14:39 -05:00
parent 6adcc8cce0
commit 6f1596ae5f
18 changed files with 707 additions and 114 deletions

View file

@ -1,5 +1,6 @@
mode: ContinuousDeployment
increment: Patch
next-version: 0.5.0
branches:
master:
tag: alpha

View file

@ -19,7 +19,7 @@ namespace LibHac.Fs
if (!BaseFs.DirectoryExists(WorkingDir))
{
BaseFs.CreateDirectory(WorkingDir);
BaseFs.CreateDirectory(CommittedDir);
BaseFs.EnsureDirectoryExists(CommittedDir);
}
if (BaseFs.DirectoryExists(CommittedDir))

View file

@ -20,7 +20,7 @@ namespace LibHac.Fs
{
if (IsDisposed) throw new ObjectDisposedException(null);
if ((Mode & OpenMode.Read) == 0) ThrowHelper.ThrowResult(ResultFs.InvalidOpenModeOperation, "File does not allow reading.");
if ((Mode & OpenMode.Read) == 0) ThrowHelper.ThrowResult(ResultFs.InvalidOpenModeForRead, "File does not allow reading.");
if (span == null) throw new ArgumentNullException(nameof(span));
if (offset < 0) ThrowHelper.ThrowResult(ResultFs.ValueOutOfRange, "Offset must be non-negative.");
@ -36,7 +36,7 @@ namespace LibHac.Fs
{
if (IsDisposed) throw new ObjectDisposedException(null);
if ((Mode & OpenMode.Write) == 0) ThrowHelper.ThrowResult(ResultFs.InvalidOpenModeOperation, "File does not allow writing.");
if ((Mode & OpenMode.Write) == 0) ThrowHelper.ThrowResult(ResultFs.InvalidOpenModeForWrite, "File does not allow writing.");
if (span == null) throw new ArgumentNullException(nameof(span));
if (offset < 0) ThrowHelper.ThrowResult(ResultFs.ValueOutOfRange, "Offset must be non-negative.");

View file

@ -19,7 +19,7 @@ namespace LibHac.Fs
if (entry.Type == DirectoryEntryType.Directory)
{
destFs.CreateDirectory(subDstPath);
destFs.EnsureDirectoryExists(subDstPath);
IDirectory subSrcDir = sourceFs.OpenDirectory(subSrcPath, OpenDirectoryMode.All);
IDirectory subDstDir = destFs.OpenDirectory(subDstPath, OpenDirectoryMode.All);
@ -28,7 +28,7 @@ namespace LibHac.Fs
if (entry.Type == DirectoryEntryType.File)
{
destFs.CreateFile(subDstPath, entry.Size, options);
destFs.CreateOrOverwriteFile(subDstPath, entry.Size, options);
using (IFile srcFile = sourceFs.OpenFile(subSrcPath, OpenMode.Read))
using (IFile dstFile = destFs.OpenFile(subDstPath, OpenMode.Write | OpenMode.Append))
@ -209,6 +209,54 @@ namespace LibHac.Fs
{
return fs.GetEntryType(path) == DirectoryEntryType.File;
}
public static void EnsureDirectoryExists(this IFileSystem fs, string path)
{
path = PathTools.Normalize(path);
if (fs.DirectoryExists(path)) return;
// Find the first subdirectory in the chain that doesn't exist
int i;
for (i = path.Length - 1; i > 0; i--)
{
if (path[i] == '/')
{
string subPath = path.Substring(0, i);
if (fs.DirectoryExists(subPath))
{
break;
}
}
}
// path[i] will be a '/', so skip that character
i++;
for (; i < path.Length; i++)
{
if (path[i] == '/')
{
string subPath = path.Substring(0, i);
fs.CreateDirectory(subPath);
}
}
}
public static void CreateOrOverwriteFile(this IFileSystem fs, string path, long size)
{
fs.CreateOrOverwriteFile(path, size, CreateFileOptions.None);
}
public static void CreateOrOverwriteFile(this IFileSystem fs, string path, long size, CreateFileOptions options)
{
path = PathTools.Normalize(path);
if (fs.FileExists(path)) fs.DeleteFile(path);
fs.CreateFile(path, size, CreateFileOptions.None);
}
}
[Flags]

View file

@ -509,7 +509,7 @@ namespace LibHac.Fs
return Result.Success;
}
internal Result GetMountName(ReadOnlySpan<char> path, out ReadOnlySpan<char> mountName, out ReadOnlySpan<char> subPath)
internal static Result GetMountName(ReadOnlySpan<char> path, out ReadOnlySpan<char> mountName, out ReadOnlySpan<char> subPath)
{
int mountLen = 0;
int maxMountLen = Math.Min(path.Length, PathTools.MountNameLength);

View file

@ -19,7 +19,7 @@ namespace LibHac.Fs
if (entry.Type == DirectoryEntryType.Directory)
{
fs.CreateDirectory(subDstPath);
fs.EnsureDirectoryExists(subDstPath);
fs.CopyDirectory(subSrcPath, subDstPath, options, logger);
}
@ -27,7 +27,7 @@ namespace LibHac.Fs
if (entry.Type == DirectoryEntryType.File)
{
logger?.LogMessage(subSrcPath);
fs.CreateFile(subDstPath, entry.Size, options);
fs.CreateOrOverwriteFile(subDstPath, entry.Size, options);
fs.CopyFile(subSrcPath, subDstPath, logger);
}
@ -109,5 +109,67 @@ namespace LibHac.Fs
}
}
}
public static bool DirectoryExists(this FileSystemManager fs, string path)
{
return fs.GetEntryType(path) == DirectoryEntryType.Directory;
}
public static bool FileExists(this FileSystemManager fs, string path)
{
return fs.GetEntryType(path) == DirectoryEntryType.File;
}
public static void EnsureDirectoryExists(this FileSystemManager fs, string path)
{
path = PathTools.Normalize(path);
if (fs.DirectoryExists(path)) return;
PathTools.GetMountNameLength(path, out int mountNameLength).ThrowIfFailure();
// Find the first subdirectory in the path that doesn't exist
int i;
for (i = path.Length - 1; i > mountNameLength + 2; i--)
{
if (path[i] == '/')
{
string subPath = path.Substring(0, i);
if (fs.DirectoryExists(subPath))
{
break;
}
}
}
// path[i] will be a '/', so skip that character
i++;
for (; i < path.Length; i++)
{
if (path[i] == '/')
{
string subPath = path.Substring(0, i);
fs.CreateDirectory(subPath);
}
}
fs.CreateDirectory(path);
}
public static void CreateOrOverwriteFile(this FileSystemManager fs, string path, long size)
{
fs.CreateOrOverwriteFile(path, size, CreateFileOptions.None);
}
public static void CreateOrOverwriteFile(this FileSystemManager fs, string path, long size, CreateFileOptions options)
{
path = PathTools.Normalize(path);
if (fs.FileExists(path)) fs.DeleteFile(path);
fs.CreateFile(path, size, CreateFileOptions.None);
}
}
}

View file

@ -1,5 +1,4 @@
using System;
using System.IO;
namespace LibHac.Fs
{
@ -12,7 +11,13 @@ namespace LibHac.Fs
/// Creates all directories and subdirectories in the specified path unless they already exist.
/// </summary>
/// <param name="path">The full path of the directory to create.</param>
/// <exception cref="IOException">An I/O error occurred while creating the directory.</exception>
/// <remarks>
/// A <see cref="HorizonResultException"/> will be thrown with the given <see cref="Result"/> under the following conditions:
///
/// The parent directory of the specified path does not exist: <see cref="ResultFs.PathNotFound"/>
/// Specified path already exists as either a file or directory: <see cref="ResultFs.PathAlreadyExists"/>
/// Insufficient free space to create the directory: <see cref="ResultFs.InsufficientFreeSpace"/>
/// </remarks>
void CreateDirectory(string path);
/// <summary>
@ -22,39 +27,58 @@ namespace LibHac.Fs
/// <param name="size">The initial size of the created file.</param>
/// <param name="options">Flags to control how the file is created.
/// Should usually be <see cref="CreateFileOptions.None"/></param>
/// <exception cref="IOException">An I/O error occurred while creating the file.</exception>
/// <remarks>
/// A <see cref="HorizonResultException"/> will be thrown with the given <see cref="Result"/> under the following conditions:
///
/// The parent directory of the specified path does not exist: <see cref="ResultFs.PathNotFound"/>
/// Specified path already exists as either a file or directory: <see cref="ResultFs.PathAlreadyExists"/>
/// Insufficient free space to create the file: <see cref="ResultFs.InsufficientFreeSpace"/>
/// </remarks>
void CreateFile(string path, long size, CreateFileOptions options);
/// <summary>
/// Deletes the specified directory.
/// </summary>
/// <param name="path">The full path of the directory to delete.</param>
/// <exception cref="DirectoryNotFoundException">The specified directory does not exist.</exception>
/// <exception cref="IOException">An I/O error occurred while deleting the directory.</exception>
/// <remarks>
/// A <see cref="HorizonResultException"/> will be thrown with the given <see cref="Result"/> under the following conditions:
///
/// The specified path does not exist or is a file: <see cref="ResultFs.PathNotFound"/>
/// The specified directory is not empty: <see cref="ResultFs.DirectoryNotEmpty"/>
/// </remarks>
void DeleteDirectory(string path);
/// <summary>
/// Deletes the specified directory and any subdirectories and files in the directory.
/// </summary>
/// <param name="path">The full path of the directory to delete.</param>
/// <exception cref="DirectoryNotFoundException">The specified directory does not exist.</exception>
/// <exception cref="IOException">An I/O error occurred while deleting the directory.</exception>
/// <remarks>
/// A <see cref="HorizonResultException"/> will be thrown with the given <see cref="Result"/> under the following conditions:
///
/// The specified path does not exist or is a file: <see cref="ResultFs.PathNotFound"/>
/// </remarks>
void DeleteDirectoryRecursively(string path);
/// <summary>
/// Deletes any subdirectories and files in the specified directory.
/// </summary>
/// <param name="path">The full path of the directory to clean.</param>
/// <exception cref="DirectoryNotFoundException">The specified directory does not exist.</exception>
/// <exception cref="IOException">An I/O error occurred while deleting the directory.</exception>
/// <remarks>
/// A <see cref="HorizonResultException"/> will be thrown with the given <see cref="Result"/> under the following conditions:
///
/// The specified path does not exist or is a file: <see cref="ResultFs.PathNotFound"/>
/// </remarks>
void CleanDirectoryRecursively(string path);
/// <summary>
/// Deletes the specified file.
/// </summary>
/// <param name="path">The full path of the file to delete.</param>
/// <exception cref="FileNotFoundException">The specified file does not exist.</exception>
/// <exception cref="IOException">An I/O error occurred while deleting the file.</exception>
/// <remarks>
/// A <see cref="HorizonResultException"/> will be thrown with the given <see cref="Result"/> under the following conditions:
///
/// The specified path does not exist or is a directory: <see cref="ResultFs.PathNotFound"/>
/// </remarks>
void DeleteFile(string path);
/// <summary>
@ -63,8 +87,11 @@ namespace LibHac.Fs
/// <param name="path">The directory's full path.</param>
/// <param name="mode">Specifies which sub-entries should be enumerated.</param>
/// <returns>An <see cref="IDirectory"/> instance for the specified directory.</returns>
/// <exception cref="DirectoryNotFoundException">The specified directory does not exist.</exception>
/// <exception cref="IOException">An I/O error occurred while opening the directory.</exception>
/// <remarks>
/// A <see cref="HorizonResultException"/> will be thrown with the given <see cref="Result"/> under the following conditions:
///
/// The specified path does not exist or is a file: <see cref="ResultFs.PathNotFound"/>
/// </remarks>
IDirectory OpenDirectory(string path, OpenDirectoryMode mode);
/// <summary>
@ -73,8 +100,11 @@ namespace LibHac.Fs
/// <param name="path">The full path of the file to open.</param>
/// <param name="mode">Specifies the access permissions of the created <see cref="IFile"/>.</param>
/// <returns>An <see cref="IFile"/> instance for the specified path.</returns>
/// <exception cref="FileNotFoundException">The specified file does not exist.</exception>
/// <exception cref="IOException">An I/O error occurred while deleting the file.</exception>
/// <remarks>
/// A <see cref="HorizonResultException"/> will be thrown with the given <see cref="Result"/> under the following conditions:
///
/// The specified path does not exist or is a directory: <see cref="ResultFs.PathNotFound"/>
/// </remarks>
IFile OpenFile(string path, OpenMode mode);
/// <summary>
@ -82,8 +112,16 @@ namespace LibHac.Fs
/// </summary>
/// <param name="srcPath">The full path of the directory to rename.</param>
/// <param name="dstPath">The new full path of the directory.</param>
/// <exception cref="DirectoryNotFoundException">The specified directory does not exist.</exception>
/// <exception cref="IOException">An I/O error occurred while deleting the directory.</exception>
/// <returns>An <see cref="IFile"/> instance for the specified path.</returns>
/// <remarks>
/// If <paramref name="srcPath"/> and <paramref name="dstPath"/> are the same, this function does nothing and returns successfully.
/// A <see cref="HorizonResultException"/> will be thrown with the given <see cref="Result"/> under the following conditions:
///
/// <paramref name="srcPath"/> does not exist or is a file: <see cref="ResultFs.PathNotFound"/>
/// <paramref name="dstPath"/>'s parent directory does not exist: <see cref="ResultFs.PathNotFound"/>
/// <paramref name="dstPath"/> already exists as either a file or directory: <see cref="ResultFs.PathAlreadyExists"/>
/// Either <paramref name="srcPath"/> or <paramref name="dstPath"/> is a subpath of the other: <see cref="ResultFs.DestinationIsSubPathOfSource"/>
/// </remarks>
void RenameDirectory(string srcPath, string dstPath);
/// <summary>
@ -91,7 +129,14 @@ namespace LibHac.Fs
/// </summary>
/// <param name="srcPath">The full path of the file to rename.</param>
/// <param name="dstPath">The new full path of the file.</param>
/// <exception cref="IOException">An I/O error occurred while deleting the file.</exception>
/// <remarks>
/// If <paramref name="srcPath"/> and <paramref name="dstPath"/> are the same, this function does nothing and returns successfully.
/// A <see cref="HorizonResultException"/> will be thrown with the given <see cref="Result"/> under the following conditions:
///
/// <paramref name="srcPath"/> does not exist or is a directory: <see cref="ResultFs.PathNotFound"/>
/// <paramref name="dstPath"/>'s parent directory does not exist: <see cref="ResultFs.PathNotFound"/>
/// <paramref name="dstPath"/> already exists as either a file or directory: <see cref="ResultFs.PathAlreadyExists"/>
/// </remarks>
void RenameFile(string srcPath, string dstPath);
/// <summary>
@ -99,6 +144,11 @@ namespace LibHac.Fs
/// </summary>
/// <param name="path">The full path to check.</param>
/// <returns>The <see cref="DirectoryEntryType"/> of the file.</returns>
/// <remarks>
/// This function operates slightly differently than it does in Horizon OS.
/// Instead of returning <see cref="ResultFs.PathNotFound"/> when an entry is missing,
/// the function will return <see cref="DirectoryEntryType.NotFound"/>.
/// </remarks>
DirectoryEntryType GetEntryType(string path);
/// <summary>
@ -121,6 +171,11 @@ namespace LibHac.Fs
/// <param name="path">The path of the file or directory.</param>
/// <returns>The timestamps for the specified file or directory.
/// This value is expressed as a Unix timestamp</returns>
/// <remarks>
/// A <see cref="HorizonResultException"/> will be thrown with the given <see cref="Result"/> under the following conditions:
///
/// The specified path does not exist: <see cref="ResultFs.PathNotFound"/>
/// </remarks>
FileTimeStampRaw GetFileTimeStampRaw(string path);
/// <summary>

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
@ -20,8 +21,22 @@ namespace LibHac.Fs
LocalPath = fs.ResolveLocalPath(path);
Mode = mode;
try
{
DirInfo = new DirectoryInfo(LocalPath);
}
catch (Exception ex) when (ex is ArgumentNullException || ex is ArgumentException ||
ex is PathTooLongException)
{
ThrowHelper.ThrowResult(ResultFs.PathNotFound, ex);
throw;
}
if (!DirInfo.Exists)
{
ThrowHelper.ThrowResult(ResultFs.PathNotFound);
}
}
public IEnumerable<DirectoryEntry> Read()
{

View file

@ -5,15 +5,16 @@ namespace LibHac.Fs
{
public class LocalFile : FileBase
{
private string Path { get; }
private const int ErrorHandleDiskFull = unchecked((int)0x80070027);
private const int ErrorDiskFull = unchecked((int)0x80070070);
private FileStream Stream { get; }
private StreamFile File { get; }
public LocalFile(string path, OpenMode mode)
{
Path = path;
Mode = mode;
Stream = new FileStream(Path, FileMode.Open, GetFileAccess(mode), GetFileShare(mode));
Stream = OpenFile(path, mode);
File = new StreamFile(Stream, mode);
ToDispose.Add(File);
@ -47,9 +48,17 @@ namespace LibHac.Fs
}
public override void SetSize(long size)
{
try
{
File.SetSize(size);
}
catch (IOException ex) when (ex.HResult == ErrorDiskFull || ex.HResult == ErrorHandleDiskFull)
{
ThrowHelper.ThrowResult(ResultFs.InsufficientFreeSpace, ex);
throw;
}
}
private static FileAccess GetFileAccess(OpenMode mode)
{
@ -61,5 +70,25 @@ namespace LibHac.Fs
{
return mode.HasFlag(OpenMode.Write) ? FileShare.Read : FileShare.ReadWrite;
}
private static FileStream OpenFile(string path, OpenMode mode)
{
try
{
return new FileStream(path, FileMode.Open, GetFileAccess(mode), GetFileShare(mode));
}
catch (Exception ex) when (ex is ArgumentNullException || ex is ArgumentException ||
ex is PathTooLongException || ex is DirectoryNotFoundException ||
ex is FileNotFoundException || ex is NotSupportedException)
{
ThrowHelper.ThrowResult(ResultFs.PathNotFound, ex);
throw;
}
catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException)
{
// todo: Should a HorizonResultException be thrown?
throw;
}
}
}
}

View file

@ -1,10 +1,16 @@
using System;
using System.IO;
using System.Security;
namespace LibHac.Fs
{
public class LocalFileSystem : IAttributeFileSystem
{
private const int ErrorHandleDiskFull = unchecked((int)0x80070027);
private const int ErrorFileExists = unchecked((int)0x80070050);
private const int ErrorDiskFull = unchecked((int)0x80070070);
private const int ErrorDirNotEmpty = unchecked((int)0x80070091);
private string BasePath { get; }
/// <summary>
@ -46,81 +52,116 @@ namespace LibHac.Fs
public long GetFileSize(string path)
{
path = PathTools.Normalize(path);
var info = new FileInfo(ResolveLocalPath(path));
return info.Length;
string localPath = ResolveLocalPath(PathTools.Normalize(path));
FileInfo info = GetFileInfo(localPath);
return GetSizeInternal(info);
}
public void CreateDirectory(string path)
{
path = PathTools.Normalize(path);
Directory.CreateDirectory(ResolveLocalPath(path));
string localPath = ResolveLocalPath(PathTools.Normalize(path));
DirectoryInfo dir = GetDirInfo(localPath);
if (dir.Exists)
{
ThrowHelper.ThrowResult(ResultFs.PathAlreadyExists);
}
if (dir.Parent?.Exists != true)
{
ThrowHelper.ThrowResult(ResultFs.PathNotFound);
}
CreateDirInternal(dir);
}
public void CreateFile(string path, long size, CreateFileOptions options)
{
path = PathTools.Normalize(path);
string localPath = ResolveLocalPath(path);
string localDir = ResolveLocalPath(PathTools.GetParentDirectory(path));
string localPath = ResolveLocalPath(PathTools.Normalize(path));
if (localDir != null) Directory.CreateDirectory(localDir);
FileInfo file = GetFileInfo(localPath);
using (FileStream stream = File.Create(localPath))
if (file.Exists)
{
stream.SetLength(size);
ThrowHelper.ThrowResult(ResultFs.PathAlreadyExists);
}
if (file.Directory?.Exists != true)
{
ThrowHelper.ThrowResult(ResultFs.PathNotFound);
}
using (FileStream stream = CreateFileInternal(file))
{
SetStreamLengthInternal(stream, size);
}
}
public void DeleteDirectory(string path)
{
path = PathTools.Normalize(path);
string localPath = ResolveLocalPath(PathTools.Normalize(path));
Directory.Delete(ResolveLocalPath(path));
DirectoryInfo dir = GetDirInfo(localPath);
DeleteDirectoryInternal(dir, false);
}
public void DeleteDirectoryRecursively(string path)
{
path = PathTools.Normalize(path);
string localPath = ResolveLocalPath(PathTools.Normalize(path));
Directory.Delete(ResolveLocalPath(path), true);
DirectoryInfo dir = GetDirInfo(localPath);
DeleteDirectoryInternal(dir, true);
}
public void CleanDirectoryRecursively(string path)
{
path = PathTools.Normalize(path);
string localPath = ResolveLocalPath(path);
string localPath = ResolveLocalPath(PathTools.Normalize(path));
foreach (string file in Directory.EnumerateFiles(localPath))
{
File.Delete(file);
DeleteFileInternal(GetFileInfo(file));
}
foreach (string dir in Directory.EnumerateDirectories(localPath))
{
Directory.Delete(dir, true);
DeleteDirectoryInternal(GetDirInfo(dir), true);
}
}
public void DeleteFile(string path)
{
path = PathTools.Normalize(path);
string localPath = ResolveLocalPath(PathTools.Normalize(path));
string resolveLocalPath = ResolveLocalPath(path);
File.Delete(resolveLocalPath);
FileInfo file = GetFileInfo(localPath);
DeleteFileInternal(file);
}
public IDirectory OpenDirectory(string path, OpenDirectoryMode mode)
{
path = PathTools.Normalize(path);
if (GetEntryType(path) == DirectoryEntryType.File)
{
ThrowHelper.ThrowResult(ResultFs.PathNotFound);
}
return new LocalDirectory(this, path, mode);
}
public IFile OpenFile(string path, OpenMode mode)
{
path = PathTools.Normalize(path);
string localPath = ResolveLocalPath(PathTools.Normalize(path));
if (GetEntryType(path) == DirectoryEntryType.Directory)
{
ThrowHelper.ThrowResult(ResultFs.PathNotFound);
}
string localPath = ResolveLocalPath(path);
return new LocalFile(localPath, mode);
}
@ -129,38 +170,49 @@ namespace LibHac.Fs
srcPath = PathTools.Normalize(srcPath);
dstPath = PathTools.Normalize(dstPath);
string srcLocalPath = ResolveLocalPath(srcPath);
string dstLocalPath = ResolveLocalPath(dstPath);
// Official FS behavior is to do nothing in this case
if (srcPath == dstPath) return;
string directoryName = ResolveLocalPath(PathTools.GetParentDirectory(dstPath));
if (directoryName != null) Directory.CreateDirectory(directoryName);
Directory.Move(srcLocalPath, dstLocalPath);
// FS does the subpath check before verifying the path exists
if (PathTools.IsSubPath(srcPath.AsSpan(), dstPath.AsSpan()))
{
ThrowHelper.ThrowResult(ResultFs.DestinationIsSubPathOfSource);
}
DirectoryInfo srcDir = GetDirInfo(ResolveLocalPath(srcPath));
DirectoryInfo dstDir = GetDirInfo(ResolveLocalPath(dstPath));
RenameDirInternal(srcDir, dstDir);
}
public void RenameFile(string srcPath, string dstPath)
{
srcPath = PathTools.Normalize(srcPath);
dstPath = PathTools.Normalize(dstPath);
string srcLocalPath = ResolveLocalPath(PathTools.Normalize(srcPath));
string dstLocalPath = ResolveLocalPath(PathTools.Normalize(dstPath));
string srcLocalPath = ResolveLocalPath(srcPath);
string dstLocalPath = ResolveLocalPath(dstPath);
string dstLocalDir = ResolveLocalPath(PathTools.GetParentDirectory(dstPath));
// Official FS behavior is to do nothing in this case
if (srcLocalPath == dstLocalPath) return;
if (dstLocalDir != null) Directory.CreateDirectory(dstLocalDir);
File.Move(srcLocalPath, dstLocalPath);
FileInfo srcFile = GetFileInfo(srcLocalPath);
FileInfo dstFile = GetFileInfo(dstLocalPath);
RenameFileInternal(srcFile, dstFile);
}
public DirectoryEntryType GetEntryType(string path)
{
path = PathTools.Normalize(path);
string localPath = ResolveLocalPath(path);
string localPath = ResolveLocalPath(PathTools.Normalize(path));
if (Directory.Exists(localPath))
DirectoryInfo dir = GetDirInfo(localPath);
if (dir.Exists)
{
return DirectoryEntryType.Directory;
}
if (File.Exists(localPath))
FileInfo file = GetFileInfo(localPath);
if (file.Exists)
{
return DirectoryEntryType.File;
}
@ -170,8 +222,9 @@ namespace LibHac.Fs
public FileTimeStampRaw GetFileTimeStampRaw(string path)
{
path = PathTools.Normalize(path);
string localPath = ResolveLocalPath(path);
string localPath = ResolveLocalPath(PathTools.Normalize(path));
if (!GetFileInfo(localPath).Exists) ThrowHelper.ThrowResult(ResultFs.PathNotFound);
FileTimeStampRaw timeStamp = default;
@ -196,5 +249,232 @@ namespace LibHac.Fs
public void QueryEntry(Span<byte> outBuffer, ReadOnlySpan<byte> inBuffer, string path, QueryId queryId) =>
ThrowHelper.ThrowResult(ResultFs.UnsupportedOperation);
private static long GetSizeInternal(FileInfo file)
{
try
{
return file.Length;
}
catch (FileNotFoundException ex)
{
ThrowHelper.ThrowResult(ResultFs.PathNotFound, ex);
throw;
}
catch (Exception ex) when (ex is SecurityException || ex is UnauthorizedAccessException)
{
// todo: Should a HorizonResultException be thrown?
throw;
}
}
private static FileStream CreateFileInternal(FileInfo file)
{
try
{
return new FileStream(file.FullName, FileMode.CreateNew, FileAccess.ReadWrite);
}
catch (DirectoryNotFoundException ex)
{
ThrowHelper.ThrowResult(ResultFs.PathNotFound, ex);
throw;
}
catch (IOException ex) when (ex.HResult == ErrorDiskFull || ex.HResult == ErrorHandleDiskFull)
{
ThrowHelper.ThrowResult(ResultFs.InsufficientFreeSpace, ex);
throw;
}
catch (IOException ex) when (ex.HResult == ErrorFileExists)
{
ThrowHelper.ThrowResult(ResultFs.PathAlreadyExists, ex);
throw;
}
catch (Exception ex) when (ex is SecurityException || ex is UnauthorizedAccessException)
{
// todo: Should a HorizonResultException be thrown?
throw;
}
}
private static void SetStreamLengthInternal(Stream stream, long size)
{
try
{
stream.SetLength(size);
}
catch (IOException ex) when (ex.HResult == ErrorDiskFull || ex.HResult == ErrorHandleDiskFull)
{
ThrowHelper.ThrowResult(ResultFs.InsufficientFreeSpace, ex);
throw;
}
}
private static void DeleteDirectoryInternal(DirectoryInfo dir, bool recursive)
{
if (!dir.Exists) ThrowHelper.ThrowResult(ResultFs.PathNotFound);
try
{
dir.Delete(recursive);
}
catch (DirectoryNotFoundException ex)
{
ThrowHelper.ThrowResult(ResultFs.PathNotFound, ex);
throw;
}
catch (IOException ex) when (ex.HResult == ErrorDirNotEmpty)
{
ThrowHelper.ThrowResult(ResultFs.DirectoryNotEmpty, ex);
throw;
}
catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException)
{
// todo: Should a HorizonResultException be thrown?
throw;
}
EnsureDeleted(dir);
}
private static void DeleteFileInternal(FileInfo file)
{
if (!file.Exists) ThrowHelper.ThrowResult(ResultFs.PathNotFound);
try
{
file.Delete();
}
catch (DirectoryNotFoundException ex)
{
ThrowHelper.ThrowResult(ResultFs.PathNotFound, ex);
throw;
}
catch (IOException ex) when (ex.HResult == ErrorDirNotEmpty)
{
ThrowHelper.ThrowResult(ResultFs.DirectoryNotEmpty, ex);
throw;
}
catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException)
{
// todo: Should a HorizonResultException be thrown?
throw;
}
EnsureDeleted(file);
}
private static void CreateDirInternal(DirectoryInfo dir)
{
try
{
dir.Create();
}
catch (DirectoryNotFoundException ex)
{
ThrowHelper.ThrowResult(ResultFs.PathNotFound, ex);
throw;
}
catch (IOException ex) when (ex.HResult == ErrorDiskFull || ex.HResult == ErrorHandleDiskFull)
{
ThrowHelper.ThrowResult(ResultFs.InsufficientFreeSpace, ex);
throw;
}
catch (Exception ex) when (ex is SecurityException || ex is UnauthorizedAccessException)
{
// todo: Should a HorizonResultException be thrown?
throw;
}
}
private static void RenameDirInternal(DirectoryInfo source, DirectoryInfo dest)
{
if (!source.Exists) ThrowHelper.ThrowResult(ResultFs.PathNotFound);
if (dest.Exists) ThrowHelper.ThrowResult(ResultFs.PathAlreadyExists);
try
{
source.MoveTo(dest.FullName);
}
catch (DirectoryNotFoundException ex)
{
ThrowHelper.ThrowResult(ResultFs.PathNotFound, ex);
throw;
}
catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException)
{
// todo: Should a HorizonResultException be thrown?
throw;
}
}
private static void RenameFileInternal(FileInfo source, FileInfo dest)
{
if (!source.Exists) ThrowHelper.ThrowResult(ResultFs.PathNotFound);
if (dest.Exists) ThrowHelper.ThrowResult(ResultFs.PathAlreadyExists);
try
{
source.MoveTo(dest.FullName);
}
catch (DirectoryNotFoundException ex)
{
ThrowHelper.ThrowResult(ResultFs.PathNotFound, ex);
throw;
}
catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException)
{
// todo: Should a HorizonResultException be thrown?
throw;
}
}
// GetFileInfo and GetDirInfo detect invalid paths
private static FileInfo GetFileInfo(string path)
{
try
{
return new FileInfo(path);
}
catch (Exception ex) when (ex is ArgumentNullException || ex is ArgumentException ||
ex is PathTooLongException)
{
ThrowHelper.ThrowResult(ResultFs.PathNotFound, ex);
throw;
}
}
private static DirectoryInfo GetDirInfo(string path)
{
try
{
return new DirectoryInfo(path);
}
catch (Exception ex) when (ex is ArgumentNullException || ex is ArgumentException ||
ex is PathTooLongException)
{
ThrowHelper.ThrowResult(ResultFs.PathNotFound, ex);
throw;
}
}
// Delete operations on IFileSystem should be synchronous
// DeleteFile and RemoveDirectory only mark the file for deletion, so we need
// to poll the filesystem until it's actually gone
private static void EnsureDeleted(FileSystemInfo entry)
{
int tries = 0;
do
{
entry.Refresh();
tries++;
if (tries > 1000)
{
throw new IOException($"Unable to delete file {entry.FullName}");
}
} while (entry.Exists);
}
}
}

View file

@ -194,16 +194,24 @@ namespace LibHac.Fs
public static string GetParentDirectory(string path)
{
if (path.Length == 0) return "/";
Debug.Assert(IsNormalized(path.AsSpan()));
int i = path.Length - 1;
// Handles non-mounted root paths
if (i == 0) return string.Empty;
// A trailing separator should be ignored
if (path[i] == '/') i--;
// Handles mounted root paths
if (i >= 0 && path[i] == ':') return string.Empty;
while (i >= 0 && path[i] != '/') i--;
if (i < 1) return "/";
// Leave the '/' if the parent is the root directory
if (i == 0 || i > 0 && path[i - 1] == ':') i++;
return path.Substring(0, i);
}
@ -296,50 +304,74 @@ namespace LibHac.Fs
return state == NormalizeState.Normal || state == NormalizeState.Delimiter;
}
public static bool IsSubPath(ReadOnlySpan<char> rootPath, ReadOnlySpan<char> path)
/// <summary>
/// Checks if either of the 2 paths is a sub-path of the other. Input paths must be normalized.
/// </summary>
/// <param name="path1">The first path to be compared.</param>
/// <param name="path2">The second path to be compared.</param>
/// <returns></returns>
public static bool IsSubPath(ReadOnlySpan<char> path1, ReadOnlySpan<char> path2)
{
Debug.Assert(IsNormalized(rootPath));
Debug.Assert(IsNormalized(path));
Debug.Assert(IsNormalized(path1));
Debug.Assert(IsNormalized(path2));
if (path.Length <= rootPath.Length) return false;
if (path1.Length == 0 || path2.Length == 0) return true;
for (int i = 0; i < rootPath.Length; i++)
//Ignore any trailing slashes
if (path1[path1.Length - 1] == DirectorySeparator)
{
if (rootPath[i] != path[i]) return false;
path1 = path1.Slice(0, path1.Length - 1);
}
// The input root path might or might not have a trailing slash.
// Both are treated the same.
int rootLength = rootPath[rootPath.Length - 1] == DirectorySeparator
? rootPath.Length - 1
: rootPath.Length;
// Return true if the character after the root path is a separator,
// and if the possible sub path continues past that point.
return path[rootLength] == DirectorySeparator && path.Length > rootLength + 1;
if (path2[path2.Length - 1] == DirectorySeparator)
{
path2 = path2.Slice(0, path2.Length - 1);
}
public static bool IsSubPath(ReadOnlySpan<byte> rootPath, ReadOnlySpan<byte> path)
{
Debug.Assert(IsNormalized(rootPath));
Debug.Assert(IsNormalized(path));
ReadOnlySpan<char> shortPath = path1.Length < path2.Length ? path1 : path2;
ReadOnlySpan<char> longPath = path1.Length < path2.Length ? path2 : path1;
if (path.Length <= rootPath.Length) return false;
for (int i = 0; i < rootPath.Length; i++)
if (!shortPath.SequenceEqual(longPath.Slice(0, shortPath.Length)))
{
if (rootPath[i] != path[i]) return false;
return false;
}
// The input root path might or might not have a trailing slash.
// Both are treated the same.
int rootLength = rootPath[rootPath.Length - 1] == DirectorySeparator
? rootPath.Length - 1
: rootPath.Length;
return longPath.Length > shortPath.Length + 1 && longPath[shortPath.Length] == DirectorySeparator;
}
// Return true if the character after the root path is a separator,
// and if the possible sub path continues past that point.
return path[rootLength] == DirectorySeparator && path.Length > rootLength + 1;
/// <summary>
/// Checks if either of the 2 paths is a sub-path of the other. Input paths must be normalized.
/// </summary>
/// <param name="path1">The first path to be compared.</param>
/// <param name="path2">The second path to be compared.</param>
/// <returns></returns>
public static bool IsSubPath(ReadOnlySpan<byte> path1, ReadOnlySpan<byte> path2)
{
Debug.Assert(IsNormalized(path1));
Debug.Assert(IsNormalized(path2));
if (path1.Length == 0 || path2.Length == 0) return true;
//Ignore any trailing slashes
if (path1[path1.Length - 1] == DirectorySeparator)
{
path1 = path1.Slice(0, path1.Length - 1);
}
if (path2[path2.Length - 1] == DirectorySeparator)
{
path2 = path2.Slice(0, path2.Length - 1);
}
ReadOnlySpan<byte> shortPath = path1.Length < path2.Length ? path1 : path2;
ReadOnlySpan<byte> longPath = path1.Length < path2.Length ? path2 : path1;
if (!shortPath.SequenceEqual(longPath.Slice(0, shortPath.Length)))
{
return false;
}
return longPath.Length > shortPath.Length + 1 && longPath[shortPath.Length] == DirectorySeparator;
}
public static string Combine(string path1, string path2)
@ -366,6 +398,20 @@ namespace LibHac.Fs
}
public static Result GetMountName(string path, out string mountName)
{
Result rc = GetMountNameLength(path, out int length);
if (rc.IsFailure())
{
mountName = default;
return rc;
}
mountName = path.Substring(0, length);
return Result.Success;
}
public static Result GetMountNameLength(string path, out int length)
{
int maxLen = Math.Min(path.Length, MountNameLength);
@ -373,12 +419,12 @@ namespace LibHac.Fs
{
if (path[i] == MountSeparator)
{
mountName = path.Substring(0, i);
length = i;
return Result.Success;
}
}
mountName = default;
length = default;
return ResultFs.InvalidMountName;
}

View file

@ -70,6 +70,7 @@
public static Result DirectoryUnobtainable => new Result(ModuleFs, 6006);
public static Result NotNormalized => new Result(ModuleFs, 6007);
public static Result DestinationIsSubPathOfSource => new Result(ModuleFs, 6032);
public static Result PathNotFoundInSaveDataFileTable => new Result(ModuleFs, 6033);
public static Result DifferentDestFileSystem => new Result(ModuleFs, 6034);
public static Result InvalidOffset => new Result(ModuleFs, 6061);
@ -79,6 +80,8 @@
public static Result InvalidOpenModeOperation => new Result(ModuleFs, 6200);
public static Result AllowAppendRequiredForImplicitExtension => new Result(ModuleFs, 6201);
public static Result InvalidOpenModeForRead => new Result(ModuleFs, 6202);
public static Result InvalidOpenModeForWrite => new Result(ModuleFs, 6203);
public static Result UnsupportedOperation => new Result(ModuleFs, 6300);
public static Result UnsupportedOperationInMemoryStorageSetSize => new Result(ModuleFs, 6316);

View file

@ -362,7 +362,7 @@ namespace LibHac.Fs.Save
if (PathTools.IsSubPath(oldPathBytes, newPathBytes))
{
throw new IOException(Messages.DestPathIsSubPath);
ThrowHelper.ThrowResult(ResultFs.DestinationIsSubPathOfSource);
}
if (oldKey.Parent != newKey.Parent)

View file

@ -16,7 +16,7 @@
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/Thealexbarney/LibHac</RepositoryUrl>
<VersionPrefix>0.4.1</VersionPrefix>
<VersionPrefix>0.5.0</VersionPrefix>
<PathMap Condition=" '$(BuildType)' == 'Release' ">$(MSBuildProjectDirectory)=C:/LibHac/</PathMap>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>

View file

@ -1,8 +1,18 @@
namespace LibHac
using System;
namespace LibHac
{
internal static class ThrowHelper
{
public static void ThrowResult(Result result) => throw new HorizonResultException(result);
public static void ThrowResult(Result result, string message) => throw new HorizonResultException(result, message);
public static void ThrowResult(Result result, Exception innerException) =>
throw new HorizonResultException(result, string.Empty, innerException);
public static void ThrowResult(Result result, string message) =>
throw new HorizonResultException(result, message);
public static void ThrowResult(Result result, string message, Exception innerException) =>
throw new HorizonResultException(result, message, innerException);
}
}

View file

@ -35,7 +35,7 @@ namespace hactoolnet
if (entry.Type == DirectoryEntryType.Directory)
{
fs.CreateDirectory(subDstPath);
fs.EnsureDirectoryExists(subDstPath);
CopyDirectoryWithProgressInternal(fs, subSrcPath, subDstPath, options, logger);
}
@ -43,7 +43,7 @@ namespace hactoolnet
if (entry.Type == DirectoryEntryType.File)
{
logger?.LogMessage(subSrcPath);
fs.CreateFile(subDstPath, entry.Size, options);
fs.CreateOrOverwriteFile(subDstPath, entry.Size, options);
CopyFileWithProgress(fs, subSrcPath, subDstPath, logger);
}

View file

@ -7,7 +7,7 @@
</PropertyGroup>
<PropertyGroup>
<VersionPrefix>0.4.1</VersionPrefix>
<VersionPrefix>0.5.0</VersionPrefix>
<PathMap Condition=" '$(BuildType)' == 'Release' ">$(MSBuildProjectDirectory)=C:/hactoolnet/</PathMap>
</PropertyGroup>

View file

@ -69,6 +69,32 @@ namespace LibHac.Tests
new object[] {"/a/b/c/", "/a/b/cdef", false},
new object[] {"/a/b/c", "/a/b/cdef", false},
new object[] {"/a/b/c/", "/a/b/cd", false},
new object[] {"mount:/", "mount:/", false},
new object[] {"mount:/", "mount:/a", true},
new object[] {"mount:/", "mount:/a/", true},
new object[] {"mount:/a/b/c", "mount:/a/b/c/d", true},
new object[] {"mount:/a/b/c/", "mount:/a/b/c/d", true},
new object[] {"mount:/a/b/c", "mount:/a/b/c", false},
new object[] {"mount:/a/b/c/", "mount:/a/b/c/", false},
new object[] {"mount:/a/b/c/", "mount:/a/b/c", false},
new object[] {"mount:/a/b/c", "mount:/a/b/c/", false},
new object[] {"mount:/a/b/c/", "mount:/a/b/cdef", false},
new object[] {"mount:/a/b/c", "mount:/a/b/cdef", false},
new object[] { "mount:/a/b/c/", "mount:/a/b/cd", false},
};
public static object[][] ParentDirectoryTestItems =
{
new object[] {"/", ""},
new object[] {"/a", "/"},
new object[] {"/aa/aabc/f", "/aa/aabc"},
new object[] {"mount:/", ""},
new object[] {"mount:/a", "mount:/"},
new object[] {"mount:/aa/aabc/f", "mount:/aa/aabc"}
};
public static object[][] IsNormalizedTestItems = GetNormalizedPaths(true);
@ -107,6 +133,24 @@ namespace LibHac.Tests
Assert.Equal(expected, actual);
}
[Theory]
[MemberData(nameof(SubPathTestItems))]
public static void TestSubPathReverse(string rootPath, string path, bool expected)
{
bool actual = PathTools.IsSubPath(path.AsSpan(), rootPath.AsSpan());
Assert.Equal(expected, actual);
}
[Theory]
[MemberData(nameof(ParentDirectoryTestItems))]
public static void TestParentDirectory(string path, string expected)
{
string actual = PathTools.GetParentDirectory(path);
Assert.Equal(expected, actual);
}
private static object[][] GetNormalizedPaths(bool getNormalized)
{
var normalizedPaths = new HashSet<string>();