From 6f1596ae5f8ed326b35a1a2f51fd40285d6039d8 Mon Sep 17 00:00:00 2001 From: Alex Barney Date: Sun, 7 Jul 2019 15:14:39 -0500 Subject: [PATCH] More closely match original FS behavior in IFileSystem --- GitVersion.yml | 1 + src/LibHac/Fs/DirectorySaveDataFileSystem.cs | 2 +- src/LibHac/Fs/FileBase.cs | 4 +- src/LibHac/Fs/FileSystemExtensions.cs | 52 ++- src/LibHac/Fs/FileSystemManager.cs | 2 +- src/LibHac/Fs/FileSystemManagerUtils.cs | 66 +++- src/LibHac/Fs/IFileSystem.cs | 91 ++++- src/LibHac/Fs/LocalDirectory.cs | 19 +- src/LibHac/Fs/LocalFile.cs | 37 +- src/LibHac/Fs/LocalFileSystem.cs | 364 ++++++++++++++++-- src/LibHac/Fs/PathTools.cs | 110 ++++-- src/LibHac/Fs/ResultFs.cs | 3 + .../Fs/Save/HierarchicalSaveFileTable.cs | 4 +- src/LibHac/LibHac.csproj | 2 +- src/LibHac/ThrowHelper.cs | 14 +- src/hactoolnet/FsUtils.cs | 4 +- src/hactoolnet/hactoolnet.csproj | 2 +- tests/LibHac.Tests/PathToolsTests.cs | 44 +++ 18 files changed, 707 insertions(+), 114 deletions(-) diff --git a/GitVersion.yml b/GitVersion.yml index 2ad71d6b..a3483dcc 100644 --- a/GitVersion.yml +++ b/GitVersion.yml @@ -1,5 +1,6 @@ mode: ContinuousDeployment increment: Patch +next-version: 0.5.0 branches: master: tag: alpha \ No newline at end of file diff --git a/src/LibHac/Fs/DirectorySaveDataFileSystem.cs b/src/LibHac/Fs/DirectorySaveDataFileSystem.cs index d7f109f3..3fe9f866 100644 --- a/src/LibHac/Fs/DirectorySaveDataFileSystem.cs +++ b/src/LibHac/Fs/DirectorySaveDataFileSystem.cs @@ -19,7 +19,7 @@ namespace LibHac.Fs if (!BaseFs.DirectoryExists(WorkingDir)) { BaseFs.CreateDirectory(WorkingDir); - BaseFs.CreateDirectory(CommittedDir); + BaseFs.EnsureDirectoryExists(CommittedDir); } if (BaseFs.DirectoryExists(CommittedDir)) diff --git a/src/LibHac/Fs/FileBase.cs b/src/LibHac/Fs/FileBase.cs index 3fd3b267..aae12e8d 100644 --- a/src/LibHac/Fs/FileBase.cs +++ b/src/LibHac/Fs/FileBase.cs @@ -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."); diff --git a/src/LibHac/Fs/FileSystemExtensions.cs b/src/LibHac/Fs/FileSystemExtensions.cs index 5a26555b..6a26db14 100644 --- a/src/LibHac/Fs/FileSystemExtensions.cs +++ b/src/LibHac/Fs/FileSystemExtensions.cs @@ -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] diff --git a/src/LibHac/Fs/FileSystemManager.cs b/src/LibHac/Fs/FileSystemManager.cs index 9dd2ede5..973ce264 100644 --- a/src/LibHac/Fs/FileSystemManager.cs +++ b/src/LibHac/Fs/FileSystemManager.cs @@ -509,7 +509,7 @@ namespace LibHac.Fs return Result.Success; } - internal Result GetMountName(ReadOnlySpan path, out ReadOnlySpan mountName, out ReadOnlySpan subPath) + internal static Result GetMountName(ReadOnlySpan path, out ReadOnlySpan mountName, out ReadOnlySpan subPath) { int mountLen = 0; int maxMountLen = Math.Min(path.Length, PathTools.MountNameLength); diff --git a/src/LibHac/Fs/FileSystemManagerUtils.cs b/src/LibHac/Fs/FileSystemManagerUtils.cs index 704fdf07..6ca37b03 100644 --- a/src/LibHac/Fs/FileSystemManagerUtils.cs +++ b/src/LibHac/Fs/FileSystemManagerUtils.cs @@ -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); + } } } diff --git a/src/LibHac/Fs/IFileSystem.cs b/src/LibHac/Fs/IFileSystem.cs index e4b3ba8f..827d2f15 100644 --- a/src/LibHac/Fs/IFileSystem.cs +++ b/src/LibHac/Fs/IFileSystem.cs @@ -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. /// /// The full path of the directory to create. - /// An I/O error occurred while creating the directory. + /// + /// A will be thrown with the given under the following conditions: + /// + /// The parent directory of the specified path does not exist: + /// Specified path already exists as either a file or directory: + /// Insufficient free space to create the directory: + /// void CreateDirectory(string path); /// @@ -22,39 +27,58 @@ namespace LibHac.Fs /// The initial size of the created file. /// Flags to control how the file is created. /// Should usually be - /// An I/O error occurred while creating the file. + /// + /// A will be thrown with the given under the following conditions: + /// + /// The parent directory of the specified path does not exist: + /// Specified path already exists as either a file or directory: + /// Insufficient free space to create the file: + /// void CreateFile(string path, long size, CreateFileOptions options); /// /// Deletes the specified directory. /// /// The full path of the directory to delete. - /// The specified directory does not exist. - /// An I/O error occurred while deleting the directory. + /// + /// A will be thrown with the given under the following conditions: + /// + /// The specified path does not exist or is a file: + /// The specified directory is not empty: + /// void DeleteDirectory(string path); /// /// Deletes the specified directory and any subdirectories and files in the directory. /// /// The full path of the directory to delete. - /// The specified directory does not exist. - /// An I/O error occurred while deleting the directory. + /// + /// A will be thrown with the given under the following conditions: + /// + /// The specified path does not exist or is a file: + /// void DeleteDirectoryRecursively(string path); /// /// Deletes any subdirectories and files in the specified directory. /// /// The full path of the directory to clean. - /// The specified directory does not exist. - /// An I/O error occurred while deleting the directory. + /// + /// A will be thrown with the given under the following conditions: + /// + /// The specified path does not exist or is a file: + /// void CleanDirectoryRecursively(string path); /// /// Deletes the specified file. /// /// The full path of the file to delete. - /// The specified file does not exist. - /// An I/O error occurred while deleting the file. + /// + /// A will be thrown with the given under the following conditions: + /// + /// The specified path does not exist or is a directory: + /// void DeleteFile(string path); /// @@ -63,8 +87,11 @@ namespace LibHac.Fs /// The directory's full path. /// Specifies which sub-entries should be enumerated. /// An instance for the specified directory. - /// The specified directory does not exist. - /// An I/O error occurred while opening the directory. + /// + /// A will be thrown with the given under the following conditions: + /// + /// The specified path does not exist or is a file: + /// IDirectory OpenDirectory(string path, OpenDirectoryMode mode); /// @@ -73,8 +100,11 @@ namespace LibHac.Fs /// The full path of the file to open. /// Specifies the access permissions of the created . /// An instance for the specified path. - /// The specified file does not exist. - /// An I/O error occurred while deleting the file. + /// + /// A will be thrown with the given under the following conditions: + /// + /// The specified path does not exist or is a directory: + /// IFile OpenFile(string path, OpenMode mode); /// @@ -82,8 +112,16 @@ namespace LibHac.Fs /// /// The full path of the directory to rename. /// The new full path of the directory. - /// The specified directory does not exist. - /// An I/O error occurred while deleting the directory. + /// An instance for the specified path. + /// + /// If and are the same, this function does nothing and returns successfully. + /// A will be thrown with the given under the following conditions: + /// + /// does not exist or is a file: + /// 's parent directory does not exist: + /// already exists as either a file or directory: + /// Either or is a subpath of the other: + /// void RenameDirectory(string srcPath, string dstPath); /// @@ -91,7 +129,14 @@ namespace LibHac.Fs /// /// The full path of the file to rename. /// The new full path of the file. - /// An I/O error occurred while deleting the file. + /// + /// If and are the same, this function does nothing and returns successfully. + /// A will be thrown with the given under the following conditions: + /// + /// does not exist or is a directory: + /// 's parent directory does not exist: + /// already exists as either a file or directory: + /// void RenameFile(string srcPath, string dstPath); /// @@ -99,6 +144,11 @@ namespace LibHac.Fs /// /// The full path to check. /// The of the file. + /// + /// This function operates slightly differently than it does in Horizon OS. + /// Instead of returning when an entry is missing, + /// the function will return . + /// DirectoryEntryType GetEntryType(string path); /// @@ -121,6 +171,11 @@ namespace LibHac.Fs /// The path of the file or directory. /// The timestamps for the specified file or directory. /// This value is expressed as a Unix timestamp + /// + /// A will be thrown with the given under the following conditions: + /// + /// The specified path does not exist: + /// FileTimeStampRaw GetFileTimeStampRaw(string path); /// diff --git a/src/LibHac/Fs/LocalDirectory.cs b/src/LibHac/Fs/LocalDirectory.cs index 247a2087..be1069b7 100644 --- a/src/LibHac/Fs/LocalDirectory.cs +++ b/src/LibHac/Fs/LocalDirectory.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; using System.Runtime.CompilerServices; @@ -20,7 +21,21 @@ namespace LibHac.Fs LocalPath = fs.ResolveLocalPath(path); Mode = mode; - DirInfo = new DirectoryInfo(LocalPath); + 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 Read() diff --git a/src/LibHac/Fs/LocalFile.cs b/src/LibHac/Fs/LocalFile.cs index 1c3859cc..950af64f 100644 --- a/src/LibHac/Fs/LocalFile.cs +++ b/src/LibHac/Fs/LocalFile.cs @@ -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); @@ -48,7 +49,15 @@ namespace LibHac.Fs public override void SetSize(long size) { - File.SetSize(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; + } + } } } diff --git a/src/LibHac/Fs/LocalFileSystem.cs b/src/LibHac/Fs/LocalFileSystem.cs index 98aef267..048e5c71 100644 --- a/src/LibHac/Fs/LocalFileSystem.cs +++ b/src/LibHac/Fs/LocalFileSystem.cs @@ -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; } /// @@ -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 outBuffer, ReadOnlySpan 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); + } } } diff --git a/src/LibHac/Fs/PathTools.cs b/src/LibHac/Fs/PathTools.cs index 496d8eb5..0b3c4b6a 100644 --- a/src/LibHac/Fs/PathTools.cs +++ b/src/LibHac/Fs/PathTools.cs @@ -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 rootPath, ReadOnlySpan path) + /// + /// Checks if either of the 2 paths is a sub-path of the other. Input paths must be normalized. + /// + /// The first path to be compared. + /// The second path to be compared. + /// + public static bool IsSubPath(ReadOnlySpan path1, ReadOnlySpan 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; + if (path2[path2.Length - 1] == DirectorySeparator) + { + path2 = path2.Slice(0, path2.Length - 1); + } - // 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; + ReadOnlySpan shortPath = path1.Length < path2.Length ? path1 : path2; + ReadOnlySpan 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 bool IsSubPath(ReadOnlySpan rootPath, ReadOnlySpan path) + /// + /// Checks if either of the 2 paths is a sub-path of the other. Input paths must be normalized. + /// + /// The first path to be compared. + /// The second path to be compared. + /// + public static bool IsSubPath(ReadOnlySpan path1, ReadOnlySpan 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; + if (path2[path2.Length - 1] == DirectorySeparator) + { + path2 = path2.Slice(0, path2.Length - 1); + } - // 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; + ReadOnlySpan shortPath = path1.Length < path2.Length ? path1 : path2; + ReadOnlySpan 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; } diff --git a/src/LibHac/Fs/ResultFs.cs b/src/LibHac/Fs/ResultFs.cs index 5ddc6542..549f5d60 100644 --- a/src/LibHac/Fs/ResultFs.cs +++ b/src/LibHac/Fs/ResultFs.cs @@ -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); diff --git a/src/LibHac/Fs/Save/HierarchicalSaveFileTable.cs b/src/LibHac/Fs/Save/HierarchicalSaveFileTable.cs index fe63c34d..6aec624a 100644 --- a/src/LibHac/Fs/Save/HierarchicalSaveFileTable.cs +++ b/src/LibHac/Fs/Save/HierarchicalSaveFileTable.cs @@ -344,7 +344,7 @@ namespace LibHac.Fs.Save { throw new IOException(Messages.DestPathAlreadyExists); } - + ReadOnlySpan oldPathBytes = Util.GetUtf8Bytes(srcPath); ReadOnlySpan newPathBytes = Util.GetUtf8Bytes(dstPath); @@ -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) diff --git a/src/LibHac/LibHac.csproj b/src/LibHac/LibHac.csproj index 8005b002..28b7057e 100644 --- a/src/LibHac/LibHac.csproj +++ b/src/LibHac/LibHac.csproj @@ -16,7 +16,7 @@ git https://github.com/Thealexbarney/LibHac - 0.4.1 + 0.5.0 $(MSBuildProjectDirectory)=C:/LibHac/ true snupkg diff --git a/src/LibHac/ThrowHelper.cs b/src/LibHac/ThrowHelper.cs index 2a642fcb..c13fa3e9 100644 --- a/src/LibHac/ThrowHelper.cs +++ b/src/LibHac/ThrowHelper.cs @@ -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); } } diff --git a/src/hactoolnet/FsUtils.cs b/src/hactoolnet/FsUtils.cs index 94eda776..6ece0c28 100644 --- a/src/hactoolnet/FsUtils.cs +++ b/src/hactoolnet/FsUtils.cs @@ -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); } diff --git a/src/hactoolnet/hactoolnet.csproj b/src/hactoolnet/hactoolnet.csproj index 884f509f..39713272 100644 --- a/src/hactoolnet/hactoolnet.csproj +++ b/src/hactoolnet/hactoolnet.csproj @@ -7,7 +7,7 @@ - 0.4.1 + 0.5.0 $(MSBuildProjectDirectory)=C:/hactoolnet/ diff --git a/tests/LibHac.Tests/PathToolsTests.cs b/tests/LibHac.Tests/PathToolsTests.cs index 76a00538..fb3a04d9 100644 --- a/tests/LibHac.Tests/PathToolsTests.cs +++ b/tests/LibHac.Tests/PathToolsTests.cs @@ -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();