From 8fc5a95532210475bf99b72d568b0bf04af0be19 Mon Sep 17 00:00:00 2001 From: Sewer56 Date: Mon, 29 Jul 2024 18:04:03 +0100 Subject: [PATCH 1/6] Added: Abstractions for Nx in NexusMods.Paths --- NexusMods.Paths.sln | 20 ++++ .../FileData/PathsMemoryMappedFileData.cs | 60 ++++++++++ .../FileProviders/FromAbsolutePathProvider.cs | 28 +++++ .../OutputAbsolutePathProvider.cs | 94 +++++++++++++++ .../NexusMods.Paths.Extensions.Nx.csproj | 16 +++ .../FileSystemAbstraction/BaseFileSystem.cs | 6 +- .../FileSystemAbstraction/IFileSystem.cs | 3 +- .../InMemoryFileSystem/InMemoryFileEntry.cs | 2 +- .../InMemoryFileSystem/InMemoryFileSystem.cs | 26 ++--- .../RealFileSystem/FileSystem.cs | 19 +++- src/NexusMods.Paths/Utilities/Pin.cs | 61 ++++++++++ tests/Directory.Build.targets | 9 -- .../FileData/PathsMemoryMappedDataTests.cs | 44 +++++++ .../FromAbsolutePathProviderTests.cs | 107 ++++++++++++++++++ .../OutputAbsolutePathProviderTests.cs | 83 ++++++++++++++ ...NexusMods.Paths.Extensions.Nx.Tests.csproj | 22 ++++ .../FileSystem/FileSystemTests.cs | 36 +++++- .../FileSystem/InMemoryFileSystemTests.cs | 24 +++- .../NexusMods.Paths.Tests.csproj | 10 ++ 19 files changed, 633 insertions(+), 37 deletions(-) create mode 100644 src/Extensions/NexusMods.Paths.Extensions.Nx/FileProviders/FileData/PathsMemoryMappedFileData.cs create mode 100644 src/Extensions/NexusMods.Paths.Extensions.Nx/FileProviders/FromAbsolutePathProvider.cs create mode 100644 src/Extensions/NexusMods.Paths.Extensions.Nx/FileProviders/OutputAbsolutePathProvider.cs create mode 100644 src/Extensions/NexusMods.Paths.Extensions.Nx/NexusMods.Paths.Extensions.Nx.csproj create mode 100644 src/NexusMods.Paths/Utilities/Pin.cs create mode 100644 tests/Extensions/NexusMods.Paths.Extensions.Nx.Tests/FileProviders/FileData/PathsMemoryMappedDataTests.cs create mode 100644 tests/Extensions/NexusMods.Paths.Extensions.Nx.Tests/FileProviders/FromAbsolutePathProviderTests.cs create mode 100644 tests/Extensions/NexusMods.Paths.Extensions.Nx.Tests/FileProviders/OutputAbsolutePathProviderTests.cs create mode 100644 tests/Extensions/NexusMods.Paths.Extensions.Nx.Tests/NexusMods.Paths.Extensions.Nx.Tests.csproj diff --git a/NexusMods.Paths.sln b/NexusMods.Paths.sln index 9e94c7a..d057d4a 100644 --- a/NexusMods.Paths.sln +++ b/NexusMods.Paths.sln @@ -32,6 +32,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NexusMods.Paths.TestingHelp EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NexusMods.Paths.Benchmarks", "src\NexusMods.Paths.Benchmarks\NexusMods.Paths.Benchmarks.csproj", "{E86B44A1-D57E-4B0B-8F62-869E1784C49C}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions", "Extensions", "{1351E5E7-6B95-405D-92CE-D7ECAF67464C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NexusMods.Paths.Extensions.Nx", "src\Extensions\NexusMods.Paths.Extensions.Nx\NexusMods.Paths.Extensions.Nx.csproj", "{4656B671-8002-461D-8C4C-74A77546C187}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions", "Extensions", "{332D93F6-E1F1-4F51-88D6-B4B364DDBDBC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NexusMods.Paths.Extensions.Nx.Tests", "tests\Extensions\NexusMods.Paths.Extensions.Nx.Tests\NexusMods.Paths.Extensions.Nx.Tests.csproj", "{D5012909-9405-4DE0-84E7-354A5EFDF0FC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -42,6 +50,10 @@ Global {30CBEB4A-E0C0-4B11-A0CF-F97BFACEEF89} = {6ED01F9D-5E12-4EB2-9601-64A2ADC719DE} {FABE9B73-49FF-472C-9013-F52876F25E1D} = {0377EBE6-F147-4233-86AD-32C821B9567E} {E86B44A1-D57E-4B0B-8F62-869E1784C49C} = {0377EBE6-F147-4233-86AD-32C821B9567E} + {1351E5E7-6B95-405D-92CE-D7ECAF67464C} = {0377EBE6-F147-4233-86AD-32C821B9567E} + {4656B671-8002-461D-8C4C-74A77546C187} = {1351E5E7-6B95-405D-92CE-D7ECAF67464C} + {332D93F6-E1F1-4F51-88D6-B4B364DDBDBC} = {6ED01F9D-5E12-4EB2-9601-64A2ADC719DE} + {D5012909-9405-4DE0-84E7-354A5EFDF0FC} = {332D93F6-E1F1-4F51-88D6-B4B364DDBDBC} EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {A92DED3D-BC67-4E04-9A06-9A1B302B3070}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -60,5 +72,13 @@ Global {E86B44A1-D57E-4B0B-8F62-869E1784C49C}.Debug|Any CPU.Build.0 = Debug|Any CPU {E86B44A1-D57E-4B0B-8F62-869E1784C49C}.Release|Any CPU.ActiveCfg = Release|Any CPU {E86B44A1-D57E-4B0B-8F62-869E1784C49C}.Release|Any CPU.Build.0 = Release|Any CPU + {4656B671-8002-461D-8C4C-74A77546C187}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4656B671-8002-461D-8C4C-74A77546C187}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4656B671-8002-461D-8C4C-74A77546C187}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4656B671-8002-461D-8C4C-74A77546C187}.Release|Any CPU.Build.0 = Release|Any CPU + {D5012909-9405-4DE0-84E7-354A5EFDF0FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D5012909-9405-4DE0-84E7-354A5EFDF0FC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D5012909-9405-4DE0-84E7-354A5EFDF0FC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D5012909-9405-4DE0-84E7-354A5EFDF0FC}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/src/Extensions/NexusMods.Paths.Extensions.Nx/FileProviders/FileData/PathsMemoryMappedFileData.cs b/src/Extensions/NexusMods.Paths.Extensions.Nx/FileProviders/FileData/PathsMemoryMappedFileData.cs new file mode 100644 index 0000000..2b7f834 --- /dev/null +++ b/src/Extensions/NexusMods.Paths.Extensions.Nx/FileProviders/FileData/PathsMemoryMappedFileData.cs @@ -0,0 +1,60 @@ +using System; +using NexusMods.Archives.Nx.Interfaces; + +namespace NexusMods.Paths.Extensions.Nx.FileProviders.FileData; + +/// +/// Provides access to data of a Paths memory-mapped file. +/// +public unsafe class PathsMemoryMappedFileData : IFileData +{ + private readonly MemoryMappedFileHandle _memoryMappedFileHandle; + private readonly bool _disposeHandle; + private bool _disposed; + + /// + public byte* Data { get; } + + /// + public ulong DataLength { get; } + + /// + /// Paths memory mapped file data. + /// + /// The handle to use + /// The start offset in the file + /// The length of the data to map + /// Disposes the handle on close. + public PathsMemoryMappedFileData(MemoryMappedFileHandle handle, ulong start, ulong length, bool disposeHandle = true) + { + _memoryMappedFileHandle = handle; + _disposeHandle = disposeHandle; + if (start >= handle.Length) + { + Data = handle.Pointer; + DataLength = 0; + } + else + { + Data = handle.Pointer + start; + DataLength = Math.Min(length, handle.Length - start); + } + } + + /// + ~PathsMemoryMappedFileData() => Dispose(); + + /// + public void Dispose() + { + if (_disposed) + return; + + _disposed = true; + + if (_disposeHandle) + _memoryMappedFileHandle.Dispose(); + + GC.SuppressFinalize(this); + } +} diff --git a/src/Extensions/NexusMods.Paths.Extensions.Nx/FileProviders/FromAbsolutePathProvider.cs b/src/Extensions/NexusMods.Paths.Extensions.Nx/FileProviders/FromAbsolutePathProvider.cs new file mode 100644 index 0000000..207d8f9 --- /dev/null +++ b/src/Extensions/NexusMods.Paths.Extensions.Nx/FileProviders/FromAbsolutePathProvider.cs @@ -0,0 +1,28 @@ +using NexusMods.Archives.Nx.Interfaces; +using NexusMods.Paths.Extensions.Nx.FileProviders.FileData; +using System.IO; +using System.IO.MemoryMappedFiles; + +namespace NexusMods.Paths.Extensions.Nx.FileProviders; + +/// +/// A provider for creating instances from an absolute path +/// +public class FromAbsolutePathProvider : IFileDataProvider +{ + /// + /// The full path to the file from which the data will be fetched. + /// + public required AbsolutePath FilePath { get; init; } + + /// + public IFileData GetFileData(ulong start, ulong length) + { + // TODO: This could probably be better, as it's unoptimal for chunked files. + // Ideally the file should be opened once in the provider and then calls in GetFileData + // could work on slices of the larger MMF. + var fileSystem = FilePath.FileSystem; + var handle = fileSystem.CreateMemoryMappedFile(FilePath, FileMode.Open, MemoryMappedFileAccess.Read, 0); + return new PathsMemoryMappedFileData(handle, start, length); + } +} diff --git a/src/Extensions/NexusMods.Paths.Extensions.Nx/FileProviders/OutputAbsolutePathProvider.cs b/src/Extensions/NexusMods.Paths.Extensions.Nx/FileProviders/OutputAbsolutePathProvider.cs new file mode 100644 index 0000000..3c497e3 --- /dev/null +++ b/src/Extensions/NexusMods.Paths.Extensions.Nx/FileProviders/OutputAbsolutePathProvider.cs @@ -0,0 +1,94 @@ +using System; +using System.IO; +using System.IO.MemoryMappedFiles; +using NexusMods.Archives.Nx.FileProviders.FileData; +using NexusMods.Archives.Nx.Headers.Managed; +using NexusMods.Archives.Nx.Interfaces; +using NexusMods.Paths.Extensions.Nx.FileProviders.FileData; + +namespace NexusMods.Paths.Extensions.Nx.FileProviders; + +/// +/// A provider for creating instances which allow +/// the user to output information to an absolute path. +/// +public class OutputAbsolutePathProvider : IOutputDataProvider +{ + /// + public string RelativePath { get; } + + /// + public FileEntry Entry { get; } + + /// + /// Full path to the file. + /// + public AbsolutePath FullPath { get; } + + private readonly MemoryMappedFileHandle? _mappedFileHandle; + private bool _isDisposed; + private readonly bool _isEmpty; + + /// + /// Initializes outputting a file to an absolute path. + /// + /// The absolute path to output the file. + /// The relative path of the file (context from the Nx archive). + /// The individual file entry (context from the Nx archive). + public OutputAbsolutePathProvider(AbsolutePath fullPath, string relativePath, FileEntry entry) + { + RelativePath = relativePath; + Entry = entry; + FullPath = fullPath; + + TryCreate: + try + { + if (entry.DecompressedSize <= 0) + { + using var _ = FullPath.FileSystem.CreateFile(FullPath); + _isEmpty = true; + return; + } + + // Ensure the directory exists + FullPath.FileSystem.CreateDirectory(FullPath.Parent); + + // Delete the file if it exists to ensure we start with an empty file + if (FullPath.FileSystem.FileExists(FullPath)) + FullPath.FileSystem.DeleteFile(FullPath); + + // Create the memory mapped file + _mappedFileHandle = FullPath.FileSystem.CreateMemoryMappedFile(FullPath, FileMode.CreateNew, MemoryMappedFileAccess.ReadWrite, entry.DecompressedSize); + } + catch (DirectoryNotFoundException) + { + // This is written this way because explicit check is slow. + FullPath.FileSystem.CreateDirectory(FullPath.Parent); + goto TryCreate; + } + } + + /// + public IFileData GetFileData(ulong start, ulong length) + { + if (_isEmpty) + return new ArrayFileData(Array.Empty(), 0, 0); + + return new PathsMemoryMappedFileData(_mappedFileHandle!.Value, start, length, false); + } + + /// + ~OutputAbsolutePathProvider() => Dispose(); + + /// + public void Dispose() + { + if (_isDisposed) + return; + + _isDisposed = true; + _mappedFileHandle?.Dispose(); + GC.SuppressFinalize(this); + } +} diff --git a/src/Extensions/NexusMods.Paths.Extensions.Nx/NexusMods.Paths.Extensions.Nx.csproj b/src/Extensions/NexusMods.Paths.Extensions.Nx/NexusMods.Paths.Extensions.Nx.csproj new file mode 100644 index 0000000..74f764e --- /dev/null +++ b/src/Extensions/NexusMods.Paths.Extensions.Nx/NexusMods.Paths.Extensions.Nx.csproj @@ -0,0 +1,16 @@ + + + + + true + + + + + + + + + + + diff --git a/src/NexusMods.Paths/FileSystemAbstraction/BaseFileSystem.cs b/src/NexusMods.Paths/FileSystemAbstraction/BaseFileSystem.cs index 5e62594..529241e 100644 --- a/src/NexusMods.Paths/FileSystemAbstraction/BaseFileSystem.cs +++ b/src/NexusMods.Paths/FileSystemAbstraction/BaseFileSystem.cs @@ -424,9 +424,9 @@ public virtual UnixFileMode GetUnixFileMode(AbsolutePath absolutePath) } /// - public MemoryMappedFileHandle CreateMemoryMappedFile(AbsolutePath absPath, FileMode mode, MemoryMappedFileAccess access) + public MemoryMappedFileHandle CreateMemoryMappedFile(AbsolutePath absPath, FileMode mode, MemoryMappedFileAccess access, ulong fileSize) { - return InternalCreateMemoryMappedFile(GetMappedPath(absPath), mode, access); + return InternalCreateMemoryMappedFile(GetMappedPath(absPath), mode, access, fileSize); } #endregion @@ -470,6 +470,6 @@ public MemoryMappedFileHandle CreateMemoryMappedFile(AbsolutePath absPath, FileM protected abstract void InternalMoveFile(AbsolutePath source, AbsolutePath dest, bool overwrite); /// - protected abstract MemoryMappedFileHandle InternalCreateMemoryMappedFile(AbsolutePath absPath, FileMode mode, MemoryMappedFileAccess access); + protected abstract MemoryMappedFileHandle InternalCreateMemoryMappedFile(AbsolutePath absPath, FileMode mode, MemoryMappedFileAccess access, ulong fileSize); #endregion } diff --git a/src/NexusMods.Paths/FileSystemAbstraction/IFileSystem.cs b/src/NexusMods.Paths/FileSystemAbstraction/IFileSystem.cs index d6ea349..64e48da 100644 --- a/src/NexusMods.Paths/FileSystemAbstraction/IFileSystem.cs +++ b/src/NexusMods.Paths/FileSystemAbstraction/IFileSystem.cs @@ -289,5 +289,6 @@ Stream OpenFile(AbsolutePath path, /// Path of the file to memory map. /// The mode the file is opened with. /// What you intend to do with the memory mapped file. - MemoryMappedFileHandle CreateMemoryMappedFile(AbsolutePath absPath, FileMode mode, MemoryMappedFileAccess access); + /// The size of the file, if creating a new file. + MemoryMappedFileHandle CreateMemoryMappedFile(AbsolutePath absPath, FileMode mode, MemoryMappedFileAccess access, ulong fileSize); } diff --git a/src/NexusMods.Paths/FileSystemAbstraction/InMemoryFileSystem/InMemoryFileEntry.cs b/src/NexusMods.Paths/FileSystemAbstraction/InMemoryFileSystem/InMemoryFileEntry.cs index 9549585..58b5e4c 100644 --- a/src/NexusMods.Paths/FileSystemAbstraction/InMemoryFileSystem/InMemoryFileEntry.cs +++ b/src/NexusMods.Paths/FileSystemAbstraction/InMemoryFileSystem/InMemoryFileEntry.cs @@ -44,7 +44,7 @@ public FileVersionInfo GetFileVersionInfo() public MemoryStream CreateReadStream() { - var ms = new MemoryStream(_contents, 0, _contents.Length, false); + var ms = new MemoryStream(_contents, 0, _contents.Length, false, true); return ms; } diff --git a/src/NexusMods.Paths/FileSystemAbstraction/InMemoryFileSystem/InMemoryFileSystem.cs b/src/NexusMods.Paths/FileSystemAbstraction/InMemoryFileSystem/InMemoryFileSystem.cs index 8b38968..6469403 100644 --- a/src/NexusMods.Paths/FileSystemAbstraction/InMemoryFileSystem/InMemoryFileSystem.cs +++ b/src/NexusMods.Paths/FileSystemAbstraction/InMemoryFileSystem/InMemoryFileSystem.cs @@ -7,7 +7,6 @@ using System.Text; using JetBrains.Annotations; using NexusMods.Paths.Utilities; -using Reloaded.Memory.Utilities; namespace NexusMods.Paths; @@ -232,7 +231,7 @@ protected override IEnumerable InternalEnumerateFileEntries(Absolute /// protected override Stream InternalOpenFile(AbsolutePath path, FileMode mode, FileAccess access, FileShare share) { - var inMemoryFileEntry = InternalCreateFile(path, mode, access); + var inMemoryFileEntry = InternalCreateFile(path, mode, access, 0); return access switch { FileAccess.Read => inMemoryFileEntry.CreateReadStream(), @@ -241,7 +240,7 @@ protected override Stream InternalOpenFile(AbsolutePath path, FileMode mode, Fil }; } - private InMemoryFileEntry InternalCreateFile(AbsolutePath path, FileMode mode, FileAccess access) + private InMemoryFileEntry InternalCreateFile(AbsolutePath path, FileMode mode, FileAccess access, ulong fileSize) { if (access == FileAccess.Read && mode != FileMode.Open && mode != FileMode.OpenOrCreate) { @@ -259,7 +258,7 @@ private InMemoryFileEntry InternalCreateFile(AbsolutePath path, FileMode mode, F case FileMode.Create: { if (!_files.TryGetValue(path, out inMemoryFileEntry)) - inMemoryFileEntry = InternalAddFile(path, Array.Empty()); + inMemoryFileEntry = InternalAddFile(path, new byte[fileSize]); else inMemoryFileEntry.SetContents(Array.Empty()); break; @@ -268,13 +267,13 @@ private InMemoryFileEntry InternalCreateFile(AbsolutePath path, FileMode mode, F { if (_files.ContainsKey(path)) throw new IOException($"{FileMode.CreateNew} can't be used if the file already exists!"); - inMemoryFileEntry = InternalAddFile(path, Array.Empty()); + inMemoryFileEntry = InternalAddFile(path, new byte[fileSize]); break; } case FileMode.OpenOrCreate: { if (!_files.TryGetValue(path, out inMemoryFileEntry)) - inMemoryFileEntry = InternalAddFile(path, Array.Empty()); + inMemoryFileEntry = InternalAddFile(path, new byte[fileSize]); break; } case FileMode.Truncate: @@ -399,7 +398,7 @@ protected override void InternalMoveFile(AbsolutePath source, AbsolutePath dest, } /// - protected override unsafe MemoryMappedFileHandle InternalCreateMemoryMappedFile(AbsolutePath absPath, FileMode mode, MemoryMappedFileAccess access) + protected override unsafe MemoryMappedFileHandle InternalCreateMemoryMappedFile(AbsolutePath absPath, FileMode mode, MemoryMappedFileAccess access, ulong fileSize) { var fileAccess = access switch { @@ -409,16 +408,9 @@ protected override unsafe MemoryMappedFileHandle InternalCreateMemoryMappedFile( _ => throw new ArgumentOutOfRangeException(nameof(access), access, null) }; - var file = InternalCreateFile(absPath, mode, fileAccess); - var stream = fileAccess switch - { - FileAccess.Read => file.CreateReadStream(), - FileAccess.Write => file.CreateWriteStream(), - FileAccess.ReadWrite => file.CreateReadWriteStream(), - }; - - var buffer = stream.GetBuffer(); - var pin = new Pinnable(buffer); + var file = InternalCreateFile(absPath, mode, fileAccess, fileSize); + var buffer = file.GetContents(); + var pin = new Pin(buffer); return new MemoryMappedFileHandle(pin.Pointer, (nuint)file.Size.Value, pin); } diff --git a/src/NexusMods.Paths/FileSystemAbstraction/RealFileSystem/FileSystem.cs b/src/NexusMods.Paths/FileSystemAbstraction/RealFileSystem/FileSystem.cs index 4c2d92d..0df8e84 100644 --- a/src/NexusMods.Paths/FileSystemAbstraction/RealFileSystem/FileSystem.cs +++ b/src/NexusMods.Paths/FileSystemAbstraction/RealFileSystem/FileSystem.cs @@ -228,23 +228,34 @@ protected override void InternalMoveFile(AbsolutePath source, AbsolutePath dest, => File.Move(source.GetFullPath(), dest.GetFullPath(), overwrite); /// - protected override unsafe MemoryMappedFileHandle InternalCreateMemoryMappedFile(AbsolutePath absPath, FileMode mode, MemoryMappedFileAccess access) + protected override unsafe MemoryMappedFileHandle InternalCreateMemoryMappedFile(AbsolutePath absPath, FileMode mode, MemoryMappedFileAccess access, ulong fileSize) { var fs = new FileStream(absPath.GetFullPath(), new FileStreamOptions { Mode = mode, Access = GetFileAccess(access), Share = FileShare.Read, - BufferSize = 0 + BufferSize = 0, + PreallocationSize = (long)fileSize }); MemoryMappedFile? mmf = null; MemoryMappedViewAccessor? view = null; try { - mmf = MemoryMappedFile.CreateFromFile(fs, null, fs.Length, access, HandleInheritability.None, false); + // Note(sewer): + // If the file is empty, we can't create a memory mapped file. + // So instead return a null pointer. + // This pointer is null because this helps us detect invalid read/write via Access Violation (0xC0000005). + if (fileSize == 0) + fileSize = (ulong)fs.Length; + + if (fileSize == 0) + return new MemoryMappedFileHandle((byte*)0, 0, null); + + mmf = MemoryMappedFile.CreateFromFile(fs, null, (long)fileSize, access, HandleInheritability.None, false); view = mmf.CreateViewAccessor(0, 0, access); var ptrData = (byte*)view.SafeMemoryMappedViewHandle.DangerousGetHandle(); - return new MemoryMappedFileHandle(ptrData, (nuint)fs.Length, new FilesystemMemoryMappedHandle(view, mmf)); + return new MemoryMappedFileHandle(ptrData, (nuint)fileSize, new FilesystemMemoryMappedHandle(view, mmf)); } catch { diff --git a/src/NexusMods.Paths/Utilities/Pin.cs b/src/NexusMods.Paths/Utilities/Pin.cs new file mode 100644 index 0000000..bb61976 --- /dev/null +++ b/src/NexusMods.Paths/Utilities/Pin.cs @@ -0,0 +1,61 @@ +using System; +using System.Runtime.InteropServices; +namespace NexusMods.Paths.Utilities; + +/// +/// Pins an existing array to memory. +/// +/// The type of element being pinned in native memory. +internal unsafe class Pin : IDisposable where T : unmanaged +{ + /// + /// Pointer to the native value in question. + /// If the class was instantiated using an array, this is the pointer to the first element of the array. + /// + public T* Pointer { get; private set; } + + // Handle keeping the object pinned. + private GCHandle _handle; + private bool _disposed; + + /* Constructor/Destructor */ + + // Note: GCHandle.Alloc causes boxing(due to conversion to object), meaning our item is stored on the heap. + // This means that for value types, we do not need to store them explicitly. + + /// + /// Pins an array of values to the heap. + /// + /// + /// The values to be pinned on the heap. + /// Depending on runtime used, these may be copied; so please use property. + /// Do not use original array once passed to this function. + /// + public Pin(T[] value) + { + _handle = GCHandle.Alloc(value, GCHandleType.Pinned); + Pointer = (T*)_handle.AddrOfPinnedObject(); + } + + + /// + /// Allows an object to try to free resources and perform other cleanup operations before it is reclaimed by + /// garbage collection. + /// + ~Pin() => Dispose(); + + /// + public void Dispose() + { + // Add disposed check + if (_disposed) + return; + + _disposed = true; + if (_handle.IsAllocated) + _handle.Free(); + + Pointer = (T*)0; + GC.SuppressFinalize(this); + } +} diff --git a/tests/Directory.Build.targets b/tests/Directory.Build.targets index 9a4d777..e591941 100644 --- a/tests/Directory.Build.targets +++ b/tests/Directory.Build.targets @@ -4,13 +4,4 @@ true - - - - SharedUsings.cs - - - SharedAssemblyAttributes.cs - - diff --git a/tests/Extensions/NexusMods.Paths.Extensions.Nx.Tests/FileProviders/FileData/PathsMemoryMappedDataTests.cs b/tests/Extensions/NexusMods.Paths.Extensions.Nx.Tests/FileProviders/FileData/PathsMemoryMappedDataTests.cs new file mode 100644 index 0000000..95b7999 --- /dev/null +++ b/tests/Extensions/NexusMods.Paths.Extensions.Nx.Tests/FileProviders/FileData/PathsMemoryMappedDataTests.cs @@ -0,0 +1,44 @@ +using NexusMods.Paths.Extensions.Nx.FileProviders.FileData; +namespace NexusMods.Paths.Extensions.Nx.Tests.FileProviders.FileData; + +public unsafe class PathsMemoryMappedDataTests +{ + [Fact] + public void Constructor_RespectsStartOffsetAndLength() + { + // Arrange + var testData = new byte[] { 1, 2, 3, 4, 5 }; + fixed (byte* testDataPtr = &testData[0]) + { + var testHandle = new MemoryMappedFileHandle(testDataPtr, (nuint)testData.Length, null); + + // Act + var fileData = new PathsMemoryMappedFileData(testHandle, 1, 3, false); + + // Assert + fileData.DataLength.Should().Be(3ul); + fileData.Data[0].Should().Be(2); + fileData.Data[1].Should().Be(3); + fileData.Data[2].Should().Be(4); + } + } + + [Fact] + public void Constructor_HandlesOverflow() + { + // Arrange + var testData = new byte[] { 1, 2, 3, 4, 5 }; + fixed (byte* testDataPtr = &testData[0]) + { + var testHandle = new MemoryMappedFileHandle(testDataPtr, (nuint)testData.Length, null); + + // Act + var fileData = new PathsMemoryMappedFileData(testHandle, 3, 10, false); + + // Assert + fileData.DataLength.Should().Be(2ul); + fileData.Data[0].Should().Be(4); + fileData.Data[1].Should().Be(5); + } + } +} diff --git a/tests/Extensions/NexusMods.Paths.Extensions.Nx.Tests/FileProviders/FromAbsolutePathProviderTests.cs b/tests/Extensions/NexusMods.Paths.Extensions.Nx.Tests/FileProviders/FromAbsolutePathProviderTests.cs new file mode 100644 index 0000000..11e0652 --- /dev/null +++ b/tests/Extensions/NexusMods.Paths.Extensions.Nx.Tests/FileProviders/FromAbsolutePathProviderTests.cs @@ -0,0 +1,107 @@ +using NexusMods.Paths.Extensions.Nx.FileProviders; +namespace NexusMods.Paths.Extensions.Nx.Tests.FileProviders; + +public class FromAbsolutePathProviderTests +{ + public static IEnumerable FileSystemTypes() + { + yield return new object[] { FileSystem.Shared }; + yield return new object[] { new InMemoryFileSystem() }; + } + + [Theory] + [MemberData(nameof(FileSystemTypes))] + public async Task GetFileData_ReturnsCorrectData(IFileSystem fileSystem) + { + // Arrange + var path = await CreateTestFile(fileSystem, new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }); + var provider = new FromAbsolutePathProvider { FilePath = path }; + + // Act + var fileData = provider.GetFileData(2, 3); + + // Assert + fileData.DataLength.Should().Be(3ul); + unsafe + { + fileData.Data[0].Should().Be(2); + fileData.Data[1].Should().Be(3); + fileData.Data[2].Should().Be(4); + } + + // Cleanup + CleanupTestFile(fileSystem, path); + } + + [Theory] + [MemberData(nameof(FileSystemTypes))] + public async Task GetFileData_HandlesOverflow(IFileSystem fileSystem) + { + // Arrange + var path = await CreateTestFile(fileSystem, new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }); + var provider = new FromAbsolutePathProvider { FilePath = path }; + + // Act + var fileData = provider.GetFileData(8, 5); + + // Assert + fileData.DataLength.Should().Be(2ul); + unsafe + { + fileData.Data[0].Should().Be(8); + fileData.Data[1].Should().Be(9); + } + + // Cleanup + CleanupTestFile(fileSystem, path); + } + + [Theory] + [MemberData(nameof(FileSystemTypes))] + public async Task GetFileData_HandlesEmptyFile(IFileSystem fileSystem) + { + // Arrange + var path = await CreateTestFile(fileSystem, Array.Empty()); + var provider = new FromAbsolutePathProvider { FilePath = path }; + + // Act + var fileData = provider.GetFileData(0, 5); + + // Assert + fileData.DataLength.Should().Be(0ul); + + // Cleanup + CleanupTestFile(fileSystem, path); + } + + [Theory] + [MemberData(nameof(FileSystemTypes))] + public async Task GetFileData_HandlesOutOfBoundsOverflow(IFileSystem fileSystem) + { + // Arrange + var path = await CreateTestFile(fileSystem, new byte[] { 0, 1, 2, 3, 4 }); + var provider = new FromAbsolutePathProvider { FilePath = path }; + + // Act + var fileData = provider.GetFileData(10, 5); + + // Assert + fileData.DataLength.Should().Be(0ul); + + // Cleanup + CleanupTestFile(fileSystem, path); + } + + private static async Task CreateTestFile(IFileSystem fileSystem, byte[] testData) + { + var path = fileSystem.GetKnownPath(KnownPath.TempDirectory).Combine(Guid.NewGuid().ToString()); + await fileSystem.WriteAllBytesAsync(path, testData); + return path; + } + + private static void CleanupTestFile(IFileSystem fileSystem, AbsolutePath path) + { + if (fileSystem.FileExists(path)) + fileSystem.DeleteFile(path); + } +} diff --git a/tests/Extensions/NexusMods.Paths.Extensions.Nx.Tests/FileProviders/OutputAbsolutePathProviderTests.cs b/tests/Extensions/NexusMods.Paths.Extensions.Nx.Tests/FileProviders/OutputAbsolutePathProviderTests.cs new file mode 100644 index 0000000..52abfef --- /dev/null +++ b/tests/Extensions/NexusMods.Paths.Extensions.Nx.Tests/FileProviders/OutputAbsolutePathProviderTests.cs @@ -0,0 +1,83 @@ +using NexusMods.Archives.Nx.FileProviders.FileData; +using NexusMods.Archives.Nx.Headers.Managed; +using NexusMods.Paths.Extensions.Nx.FileProviders; +namespace NexusMods.Paths.Extensions.Nx.Tests.FileProviders; + +public class OutputAbsolutePathProviderTests +{ + public static IEnumerable FileSystemTypes() + { + yield return new object[] { FileSystem.Shared }; + yield return new object[] { new InMemoryFileSystem() }; + } + + [Theory] + [MemberData(nameof(FileSystemTypes))] + public void GetFileData_ReturnsCorrectData(IFileSystem fileSystem) + { + // Arrange + var path = CreateTestPath(fileSystem); + var entry = new FileEntry { DecompressedSize = 100 }; + var provider = new OutputAbsolutePathProvider(path, "relative/path.txt", entry); + + // Act + var fileData = provider.GetFileData(10, 5); + + // Assert + fileData.DataLength.Should().Be(5ul); + + // Cleanup + CleanupTestFile(fileSystem, path); + } + + [Theory] + [MemberData(nameof(FileSystemTypes))] + public void GetFileData_HandlesEmptyFile(IFileSystem fileSystem) + { + // Arrange + var path = CreateTestPath(fileSystem); + var entry = new FileEntry { DecompressedSize = 0 }; + var provider = new OutputAbsolutePathProvider(path, "relative/path.txt", entry); + + // Act + var fileData = provider.GetFileData(0, 5); + + // Assert + fileData.DataLength.Should().Be(0ul); + Assert.IsType(fileData); + + // Cleanup + CleanupTestFile(fileSystem, path); + } + + [Theory] + [MemberData(nameof(FileSystemTypes))] + public void Constructor_CreatesFile(IFileSystem fileSystem) + { + // Arrange + var path = CreateTestPath(fileSystem); + var entry = new FileEntry { DecompressedSize = 100 }; + + // Act + _ = new OutputAbsolutePathProvider(path, "relative/path.txt", entry); + + // Assert + fileSystem.FileExists(path).Should().BeTrue(); + + // Cleanup + CleanupTestFile(fileSystem, path); + } + + private static AbsolutePath CreateTestPath(IFileSystem fileSystem) + { + return fileSystem.GetKnownPath(KnownPath.TempDirectory).Combine(Guid.NewGuid().ToString()); + } + + private static void CleanupTestFile(IFileSystem fileSystem, AbsolutePath path) + { + if (fileSystem.FileExists(path)) + { + fileSystem.DeleteFile(path); + } + } +} diff --git a/tests/Extensions/NexusMods.Paths.Extensions.Nx.Tests/NexusMods.Paths.Extensions.Nx.Tests.csproj b/tests/Extensions/NexusMods.Paths.Extensions.Nx.Tests/NexusMods.Paths.Extensions.Nx.Tests.csproj new file mode 100644 index 0000000..880a849 --- /dev/null +++ b/tests/Extensions/NexusMods.Paths.Extensions.Nx.Tests/NexusMods.Paths.Extensions.Nx.Tests.csproj @@ -0,0 +1,22 @@ + + + + false + true + + + + + + + + + + + SharedUsings.cs + + + SharedAssemblyAttributes.cs + + + diff --git a/tests/NexusMods.Paths.Tests/FileSystem/FileSystemTests.cs b/tests/NexusMods.Paths.Tests/FileSystem/FileSystemTests.cs index 768178a..63c103b 100644 --- a/tests/NexusMods.Paths.Tests/FileSystem/FileSystemTests.cs +++ b/tests/NexusMods.Paths.Tests/FileSystem/FileSystemTests.cs @@ -93,7 +93,7 @@ public async Task Test_CreateMemoryMappedFile_CanOpen(RelativePath relativePath, unsafe { - using var mmf = fs.CreateMemoryMappedFile(file, FileMode.Open, MemoryMappedFileAccess.ReadWrite); + using var mmf = fs.CreateMemoryMappedFile(file, FileMode.Open, MemoryMappedFileAccess.ReadWrite, 0); mmf.Should().NotBeNull(); ((nuint)mmf.Pointer).Should().NotBe(0); mmf.Length.Should().Be((nuint)file.FileInfo.Size); @@ -102,4 +102,38 @@ public async Task Test_CreateMemoryMappedFile_CanOpen(RelativePath relativePath, mmf.AsSpan().SequenceEqual(contents).Should().BeTrue(); } } + + [Fact] + public async Task Test_CreateMemoryMappedFile_CanCreateAndWrite() + { + var fs = new Paths.FileSystem(); + var tempFile = fs.GetKnownPath(KnownPath.TempDirectory).Combine(Path.GetRandomFileName()); + var contents = new byte[] { 1, 2, 3, 4, 5 }; + + try + { + // Create a new MemoryMappedFile + using var mmf = fs.CreateMemoryMappedFile(tempFile, FileMode.CreateNew, MemoryMappedFileAccess.ReadWrite, (ulong)contents.Length); + + unsafe + { + mmf.Should().NotBeNull(); + ((nuint)mmf.Pointer).Should().NotBe(0); + mmf.Length.Should().Be((nuint)contents.Length); + + // Write data to the MemoryMappedFile + contents.CopyTo(new Span(mmf.Pointer, contents.Length)); + } + + // Verify the data was written correctly + var writtenData = await fs.ReadAllBytesAsync(tempFile); + writtenData.Should().BeEquivalentTo(contents); + } + finally + { + // Clean up + if (fs.FileExists(tempFile)) + fs.DeleteFile(tempFile); + } + } } diff --git a/tests/NexusMods.Paths.Tests/FileSystem/InMemoryFileSystemTests.cs b/tests/NexusMods.Paths.Tests/FileSystem/InMemoryFileSystemTests.cs index f847981..30cc1d1 100644 --- a/tests/NexusMods.Paths.Tests/FileSystem/InMemoryFileSystemTests.cs +++ b/tests/NexusMods.Paths.Tests/FileSystem/InMemoryFileSystemTests.cs @@ -352,7 +352,7 @@ public void Test_CreateMemoryMappedFile_CanOpen(InMemoryFileSystem fs, unsafe { - using var mmf = fs.CreateMemoryMappedFile(file, FileMode.Open, MemoryMappedFileAccess.ReadWrite); + using var mmf = fs.CreateMemoryMappedFile(file, FileMode.Open, MemoryMappedFileAccess.ReadWrite, 0); mmf.Should().NotBeNull(); ((nuint)mmf.Pointer).Should().NotBe(0); mmf.Length.Should().Be((nuint)file.FileInfo.Size); @@ -361,4 +361,26 @@ public void Test_CreateMemoryMappedFile_CanOpen(InMemoryFileSystem fs, mmf.AsSpan().SequenceEqual(contents).Should().BeTrue(); } } + + [Theory, AutoFileSystem] + public async Task Test_CreateMemoryMappedFile_CanCreateAndWrite(InMemoryFileSystem fs, + AbsolutePath file, byte[] contents) + { + // Create a new MemoryMappedFile + using var mmf = fs.CreateMemoryMappedFile(file, FileMode.CreateNew, MemoryMappedFileAccess.ReadWrite, (ulong)contents.Length); + + unsafe + { + mmf.Should().NotBeNull(); + ((nuint)mmf.Pointer).Should().NotBe(0); + mmf.Length.Should().Be((nuint)contents.Length); + + // Write data to the MemoryMappedFile + contents.CopyTo(new Span(mmf.Pointer, contents.Length)); + } + + // Verify the data was written correctly + var writtenData = await fs.ReadAllBytesAsync(file); + writtenData.Should().BeEquivalentTo(contents); + } } diff --git a/tests/NexusMods.Paths.Tests/NexusMods.Paths.Tests.csproj b/tests/NexusMods.Paths.Tests/NexusMods.Paths.Tests.csproj index bf4334a..e25c85d 100644 --- a/tests/NexusMods.Paths.Tests/NexusMods.Paths.Tests.csproj +++ b/tests/NexusMods.Paths.Tests/NexusMods.Paths.Tests.csproj @@ -14,4 +14,14 @@ Always + + + + + SharedUsings.cs + + + SharedAssemblyAttributes.cs + + From d81dc0385b66e04b5631e58f1fc8210268c54168 Mon Sep 17 00:00:00 2001 From: Sewer56 Date: Mon, 29 Jul 2024 18:25:31 +0100 Subject: [PATCH 2/6] Added: Extra note for the FromAbsolutePathProvider TODO --- .../FileProviders/FromAbsolutePathProvider.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Extensions/NexusMods.Paths.Extensions.Nx/FileProviders/FromAbsolutePathProvider.cs b/src/Extensions/NexusMods.Paths.Extensions.Nx/FileProviders/FromAbsolutePathProvider.cs index 207d8f9..d5c7a36 100644 --- a/src/Extensions/NexusMods.Paths.Extensions.Nx/FileProviders/FromAbsolutePathProvider.cs +++ b/src/Extensions/NexusMods.Paths.Extensions.Nx/FileProviders/FromAbsolutePathProvider.cs @@ -21,6 +21,7 @@ public IFileData GetFileData(ulong start, ulong length) // TODO: This could probably be better, as it's unoptimal for chunked files. // Ideally the file should be opened once in the provider and then calls in GetFileData // could work on slices of the larger MMF. + // This however requires a change in Nx itself, which should be done at some point. var fileSystem = FilePath.FileSystem; var handle = fileSystem.CreateMemoryMappedFile(FilePath, FileMode.Open, MemoryMappedFileAccess.Read, 0); return new PathsMemoryMappedFileData(handle, start, length); From b9b9deb3dcdc6b4c3008ccb4d6e4611198e420ea Mon Sep 17 00:00:00 2001 From: Sewer56 Date: Mon, 29 Jul 2024 18:35:22 +0100 Subject: [PATCH 3/6] Improved: Cleaned up Pin.cs --- src/NexusMods.Paths/Utilities/Pin.cs | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/NexusMods.Paths/Utilities/Pin.cs b/src/NexusMods.Paths/Utilities/Pin.cs index bb61976..21f6025 100644 --- a/src/NexusMods.Paths/Utilities/Pin.cs +++ b/src/NexusMods.Paths/Utilities/Pin.cs @@ -9,8 +9,7 @@ namespace NexusMods.Paths.Utilities; internal unsafe class Pin : IDisposable where T : unmanaged { /// - /// Pointer to the native value in question. - /// If the class was instantiated using an array, this is the pointer to the first element of the array. + /// Pointer to the first value of the pinned array. /// public T* Pointer { get; private set; } @@ -18,11 +17,6 @@ internal unsafe class Pin : IDisposable where T : unmanaged private GCHandle _handle; private bool _disposed; - /* Constructor/Destructor */ - - // Note: GCHandle.Alloc causes boxing(due to conversion to object), meaning our item is stored on the heap. - // This means that for value types, we do not need to store them explicitly. - /// /// Pins an array of values to the heap. /// @@ -37,11 +31,6 @@ public Pin(T[] value) Pointer = (T*)_handle.AddrOfPinnedObject(); } - - /// - /// Allows an object to try to free resources and perform other cleanup operations before it is reclaimed by - /// garbage collection. - /// ~Pin() => Dispose(); /// From f1a3201a1ee2f68eb3bd85d54b4e182be191cbe5 Mon Sep 17 00:00:00 2001 From: Sewer56 Date: Mon, 29 Jul 2024 23:25:31 +0100 Subject: [PATCH 4/6] Changed: Re-Sync tests with Meta --- .editorconfig | 87 +++++-------------- .globalconfig | 20 +++++ extern/meta | 2 +- tests/Directory.Build.targets | 9 ++ .../FileData/PathsMemoryMappedDataTests.cs | 2 + .../FromAbsolutePathProviderTests.cs | 2 + .../OutputAbsolutePathProviderTests.cs | 2 + ...NexusMods.Paths.Extensions.Nx.Tests.csproj | 9 -- tests/Extensions/SharedAssemblyAttributes.cs | 0 tests/Extensions/SharedUsings.cs | 0 .../NexusMods.Paths.Tests.csproj | 10 --- 11 files changed, 59 insertions(+), 84 deletions(-) create mode 100644 .globalconfig create mode 100644 tests/Extensions/SharedAssemblyAttributes.cs create mode 100644 tests/Extensions/SharedUsings.cs diff --git a/.editorconfig b/.editorconfig index f0d7709..272fc95 100644 --- a/.editorconfig +++ b/.editorconfig @@ -2,76 +2,35 @@ root = true [*] charset = utf-8 +end_of_line = lf insert_final_newline = true -trim_trailing_whitespace = true -max_line_length = 120 - -# Microsoft .NET properties -csharp_new_line_before_members_in_object_initializers = false -csharp_preferred_modifier_order = public, private, protected, internal, file, new, static, abstract, virtual, sealed, readonly, override, extern, unsafe, volatile, async, required:suggestion -csharp_style_prefer_utf8_string_literals = true:suggestion -csharp_style_var_elsewhere = true:warning -csharp_style_var_for_built_in_types = true:warning -csharp_style_var_when_type_is_apparent = true:warning -dotnet_naming_rule.unity_serialized_field_rule.import_to_resharper = True -dotnet_naming_rule.unity_serialized_field_rule.resharper_description = Unity serialized field -dotnet_naming_rule.unity_serialized_field_rule.resharper_guid = 5f0fdb63-c892-4d2c-9324-15c80b22a7ef -dotnet_naming_rule.unity_serialized_field_rule.severity = warning -dotnet_naming_rule.unity_serialized_field_rule.style = lower_camel_case_style -dotnet_naming_rule.unity_serialized_field_rule.symbols = unity_serialized_field_symbols -dotnet_naming_style.lower_camel_case_style.capitalization = camel_case -dotnet_naming_symbols.unity_serialized_field_symbols.applicable_accessibilities = * -dotnet_naming_symbols.unity_serialized_field_symbols.applicable_kinds = -dotnet_naming_symbols.unity_serialized_field_symbols.resharper_applicable_kinds = unity_serialised_field -dotnet_naming_symbols.unity_serialized_field_symbols.resharper_required_modifiers = instance -dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none -dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:none -dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none -dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion -dotnet_style_predefined_type_for_member_access = true:suggestion -dotnet_style_qualification_for_event = false:warning -dotnet_style_qualification_for_field = false:warning -dotnet_style_qualification_for_method = false:warning -dotnet_style_qualification_for_property = false:warning -dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion - -# ReSharper properties -resharper_apply_auto_detected_rules = false -resharper_autodetect_indent_settings = true -resharper_csharp_empty_block_style = together_same_line -resharper_csharp_stick_comment = false -resharper_outdent_statement_labels = true -resharper_show_autodetect_configure_formatting_tip = false -resharper_use_indent_from_vs = false -resharper_wrap_lines = true - -# ReSharper inspection severities -resharper_arrange_redundant_parentheses_highlighting = hint -resharper_arrange_type_member_modifiers_highlighting = hint -resharper_arrange_type_modifiers_highlighting = hint -resharper_built_in_type_reference_style_for_member_access_highlighting = hint -resharper_built_in_type_reference_style_highlighting = hint - -[*.cs] -indent_size = 4 indent_style = space -tab_width = 4 - -# CS4014: Task not awaited -dotnet_diagnostic.cs4014.severity = error - -# CS8509: Missing switch case for named enum value -dotnet_diagnostic.CS8509.severity = error - -# CS824: Missing switch case for unnamed enum value -dotnet_diagnostic.CS8524.severity = none +indent_size = 4 -# Enums should not have duplicate values -dotnet_diagnostic.CA1069.severity = error +[{*.har,*.jsb2,*.jsb3,*.json,*.jsonc,*.postman_collection,*.postman_collection.json,*.postman_environment,*.postman_environment.json,.babelrc,.eslintrc,.prettierrc,.stylelintrc,bowerrc,jest.config}] +indent_style = space +indent_size = 2 [{*.yaml,*.yml}] indent_style = space indent_size = 2 -[*.csproj] +[{*.bash,*.sh,*.zsh}] +indent_style = space +indent_size = 2 + +[*.{appxmanifest,asax,ascx,aspx,axaml,build,c,c++,c++m,cc,ccm,cginc,compute,cp,cpp,cppm,cs,cshtml,cu,cuh,cxx,cxxm,dtd,fs,fsi,fsscript,fsx,fx,fxh,h,hh,hlsl,hlsli,hlslinc,hpp,hxx,inc,inl,ino,ipp,ixx,master,ml,mli,mpp,mq4,mq5,mqh,mxx,nuspec,paml,razor,resw,resx,shader,skin,tpp,usf,ush,uxml,vb,xaml,xamlx,xoml,xsd}] +indent_style = space indent_size = 4 +tab_width = 4 + +# Verify settings +[*.{received,verified}.{txt,xml,json}] +charset = "utf-8-bom" +end_of_line = lf +indent_size = +indent_style = +insert_final_newline = false +tab_width = +trim_trailing_whitespace = false + diff --git a/.globalconfig b/.globalconfig new file mode 100644 index 0000000..acb99d7 --- /dev/null +++ b/.globalconfig @@ -0,0 +1,20 @@ +is_global = true + +# CS4014: Task not awaited +dotnet_diagnostic.cs4014.severity = error + +# CS8509: Missing switch case for named enum value +dotnet_diagnostic.CS8509.severity = error + +# CS824: Missing switch case for unnamed enum value +dotnet_diagnostic.CS8524.severity = none + +# Enums should not have duplicate values +dotnet_diagnostic.CA1069.severity = error + +# Non-constant fields should not be visible +dotnet_diagnostic.CA2211.severity = error + +# Don't call Enumerable.Cast or Enumerable.OfType with incompatible types +dotnet_diagnostic.CA2021.severity = error + diff --git a/extern/meta b/extern/meta index 0aee3da..75faebd 160000 --- a/extern/meta +++ b/extern/meta @@ -1 +1 @@ -Subproject commit 0aee3dafa39a3a03c1f2c2b62a46ba14ad8ac469 +Subproject commit 75faebd40fc091fcaf995c6c174867537caf2b05 diff --git a/tests/Directory.Build.targets b/tests/Directory.Build.targets index e591941..9a4d777 100644 --- a/tests/Directory.Build.targets +++ b/tests/Directory.Build.targets @@ -4,4 +4,13 @@ true + + + + SharedUsings.cs + + + SharedAssemblyAttributes.cs + + diff --git a/tests/Extensions/NexusMods.Paths.Extensions.Nx.Tests/FileProviders/FileData/PathsMemoryMappedDataTests.cs b/tests/Extensions/NexusMods.Paths.Extensions.Nx.Tests/FileProviders/FileData/PathsMemoryMappedDataTests.cs index 95b7999..d6d19ad 100644 --- a/tests/Extensions/NexusMods.Paths.Extensions.Nx.Tests/FileProviders/FileData/PathsMemoryMappedDataTests.cs +++ b/tests/Extensions/NexusMods.Paths.Extensions.Nx.Tests/FileProviders/FileData/PathsMemoryMappedDataTests.cs @@ -1,4 +1,6 @@ +using FluentAssertions; using NexusMods.Paths.Extensions.Nx.FileProviders.FileData; +using Xunit; namespace NexusMods.Paths.Extensions.Nx.Tests.FileProviders.FileData; public unsafe class PathsMemoryMappedDataTests diff --git a/tests/Extensions/NexusMods.Paths.Extensions.Nx.Tests/FileProviders/FromAbsolutePathProviderTests.cs b/tests/Extensions/NexusMods.Paths.Extensions.Nx.Tests/FileProviders/FromAbsolutePathProviderTests.cs index 11e0652..e076bdf 100644 --- a/tests/Extensions/NexusMods.Paths.Extensions.Nx.Tests/FileProviders/FromAbsolutePathProviderTests.cs +++ b/tests/Extensions/NexusMods.Paths.Extensions.Nx.Tests/FileProviders/FromAbsolutePathProviderTests.cs @@ -1,4 +1,6 @@ +using FluentAssertions; using NexusMods.Paths.Extensions.Nx.FileProviders; +using Xunit; namespace NexusMods.Paths.Extensions.Nx.Tests.FileProviders; public class FromAbsolutePathProviderTests diff --git a/tests/Extensions/NexusMods.Paths.Extensions.Nx.Tests/FileProviders/OutputAbsolutePathProviderTests.cs b/tests/Extensions/NexusMods.Paths.Extensions.Nx.Tests/FileProviders/OutputAbsolutePathProviderTests.cs index 52abfef..f404291 100644 --- a/tests/Extensions/NexusMods.Paths.Extensions.Nx.Tests/FileProviders/OutputAbsolutePathProviderTests.cs +++ b/tests/Extensions/NexusMods.Paths.Extensions.Nx.Tests/FileProviders/OutputAbsolutePathProviderTests.cs @@ -1,6 +1,8 @@ +using FluentAssertions; using NexusMods.Archives.Nx.FileProviders.FileData; using NexusMods.Archives.Nx.Headers.Managed; using NexusMods.Paths.Extensions.Nx.FileProviders; +using Xunit; namespace NexusMods.Paths.Extensions.Nx.Tests.FileProviders; public class OutputAbsolutePathProviderTests diff --git a/tests/Extensions/NexusMods.Paths.Extensions.Nx.Tests/NexusMods.Paths.Extensions.Nx.Tests.csproj b/tests/Extensions/NexusMods.Paths.Extensions.Nx.Tests/NexusMods.Paths.Extensions.Nx.Tests.csproj index 880a849..6036dec 100644 --- a/tests/Extensions/NexusMods.Paths.Extensions.Nx.Tests/NexusMods.Paths.Extensions.Nx.Tests.csproj +++ b/tests/Extensions/NexusMods.Paths.Extensions.Nx.Tests/NexusMods.Paths.Extensions.Nx.Tests.csproj @@ -10,13 +10,4 @@ - - - - SharedUsings.cs - - - SharedAssemblyAttributes.cs - - diff --git a/tests/Extensions/SharedAssemblyAttributes.cs b/tests/Extensions/SharedAssemblyAttributes.cs new file mode 100644 index 0000000..e69de29 diff --git a/tests/Extensions/SharedUsings.cs b/tests/Extensions/SharedUsings.cs new file mode 100644 index 0000000..e69de29 diff --git a/tests/NexusMods.Paths.Tests/NexusMods.Paths.Tests.csproj b/tests/NexusMods.Paths.Tests/NexusMods.Paths.Tests.csproj index e25c85d..bf4334a 100644 --- a/tests/NexusMods.Paths.Tests/NexusMods.Paths.Tests.csproj +++ b/tests/NexusMods.Paths.Tests/NexusMods.Paths.Tests.csproj @@ -14,14 +14,4 @@ Always - - - - - SharedUsings.cs - - - SharedAssemblyAttributes.cs - - From 9f9deacb5b8e965e40ccc99cc0d7aac7c0944bf7 Mon Sep 17 00:00:00 2001 From: Sewer56 Date: Mon, 29 Jul 2024 23:37:05 +0100 Subject: [PATCH 5/6] Improved: Dispose FileStream As Early as Possible if File Size is 0 on Open --- .../FileSystemAbstraction/RealFileSystem/FileSystem.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/NexusMods.Paths/FileSystemAbstraction/RealFileSystem/FileSystem.cs b/src/NexusMods.Paths/FileSystemAbstraction/RealFileSystem/FileSystem.cs index 0df8e84..e35c29a 100644 --- a/src/NexusMods.Paths/FileSystemAbstraction/RealFileSystem/FileSystem.cs +++ b/src/NexusMods.Paths/FileSystemAbstraction/RealFileSystem/FileSystem.cs @@ -250,7 +250,10 @@ protected override unsafe MemoryMappedFileHandle InternalCreateMemoryMappedFile( fileSize = (ulong)fs.Length; if (fileSize == 0) + { + fs.Dispose(); // Dispose early. return new MemoryMappedFileHandle((byte*)0, 0, null); + } mmf = MemoryMappedFile.CreateFromFile(fs, null, (long)fileSize, access, HandleInheritability.None, false); view = mmf.CreateViewAccessor(0, 0, access); From 078156120f65bc454075cbaafb53b6dd1522a767 Mon Sep 17 00:00:00 2001 From: Sewer56 Date: Tue, 30 Jul 2024 00:09:40 +0100 Subject: [PATCH 6/6] Improved: Early dispose of handles in tests on Windows --- .../FromAbsolutePathProviderTests.cs | 49 ++++++++++--------- .../OutputAbsolutePathProviderTests.cs | 33 +++++++------ .../FileSystem/FileSystemTests.cs | 34 +++++++++---- 3 files changed, 69 insertions(+), 47 deletions(-) diff --git a/tests/Extensions/NexusMods.Paths.Extensions.Nx.Tests/FileProviders/FromAbsolutePathProviderTests.cs b/tests/Extensions/NexusMods.Paths.Extensions.Nx.Tests/FileProviders/FromAbsolutePathProviderTests.cs index e076bdf..5433ba9 100644 --- a/tests/Extensions/NexusMods.Paths.Extensions.Nx.Tests/FileProviders/FromAbsolutePathProviderTests.cs +++ b/tests/Extensions/NexusMods.Paths.Extensions.Nx.Tests/FileProviders/FromAbsolutePathProviderTests.cs @@ -20,15 +20,16 @@ public async Task GetFileData_ReturnsCorrectData(IFileSystem fileSystem) var provider = new FromAbsolutePathProvider { FilePath = path }; // Act - var fileData = provider.GetFileData(2, 3); - - // Assert - fileData.DataLength.Should().Be(3ul); - unsafe + using (var fileData = provider.GetFileData(2, 3)) { - fileData.Data[0].Should().Be(2); - fileData.Data[1].Should().Be(3); - fileData.Data[2].Should().Be(4); + // Assert + fileData.DataLength.Should().Be(3ul); + unsafe + { + fileData.Data[0].Should().Be(2); + fileData.Data[1].Should().Be(3); + fileData.Data[2].Should().Be(4); + } } // Cleanup @@ -44,14 +45,15 @@ public async Task GetFileData_HandlesOverflow(IFileSystem fileSystem) var provider = new FromAbsolutePathProvider { FilePath = path }; // Act - var fileData = provider.GetFileData(8, 5); - - // Assert - fileData.DataLength.Should().Be(2ul); - unsafe + using (var fileData = provider.GetFileData(8, 5)) { - fileData.Data[0].Should().Be(8); - fileData.Data[1].Should().Be(9); + // Assert + fileData.DataLength.Should().Be(2ul); + unsafe + { + fileData.Data[0].Should().Be(8); + fileData.Data[1].Should().Be(9); + } } // Cleanup @@ -67,10 +69,11 @@ public async Task GetFileData_HandlesEmptyFile(IFileSystem fileSystem) var provider = new FromAbsolutePathProvider { FilePath = path }; // Act - var fileData = provider.GetFileData(0, 5); - - // Assert - fileData.DataLength.Should().Be(0ul); + using (var fileData = provider.GetFileData(0, 5)) + { + // Assert + fileData.DataLength.Should().Be(0ul); + } // Cleanup CleanupTestFile(fileSystem, path); @@ -85,10 +88,12 @@ public async Task GetFileData_HandlesOutOfBoundsOverflow(IFileSystem fileSystem) var provider = new FromAbsolutePathProvider { FilePath = path }; // Act - var fileData = provider.GetFileData(10, 5); + using (var fileData = provider.GetFileData(10, 5)) + { + // Assert + fileData.DataLength.Should().Be(0ul); + } - // Assert - fileData.DataLength.Should().Be(0ul); // Cleanup CleanupTestFile(fileSystem, path); diff --git a/tests/Extensions/NexusMods.Paths.Extensions.Nx.Tests/FileProviders/OutputAbsolutePathProviderTests.cs b/tests/Extensions/NexusMods.Paths.Extensions.Nx.Tests/FileProviders/OutputAbsolutePathProviderTests.cs index f404291..01b7394 100644 --- a/tests/Extensions/NexusMods.Paths.Extensions.Nx.Tests/FileProviders/OutputAbsolutePathProviderTests.cs +++ b/tests/Extensions/NexusMods.Paths.Extensions.Nx.Tests/FileProviders/OutputAbsolutePathProviderTests.cs @@ -20,13 +20,14 @@ public void GetFileData_ReturnsCorrectData(IFileSystem fileSystem) // Arrange var path = CreateTestPath(fileSystem); var entry = new FileEntry { DecompressedSize = 100 }; - var provider = new OutputAbsolutePathProvider(path, "relative/path.txt", entry); // Act - var fileData = provider.GetFileData(10, 5); - - // Assert - fileData.DataLength.Should().Be(5ul); + using (var provider = new OutputAbsolutePathProvider(path, "relative/path.txt", entry)) + using (var fileData = provider.GetFileData(10, 5)) + { + // Assert + fileData.DataLength.Should().Be(5ul); + } // Cleanup CleanupTestFile(fileSystem, path); @@ -39,14 +40,15 @@ public void GetFileData_HandlesEmptyFile(IFileSystem fileSystem) // Arrange var path = CreateTestPath(fileSystem); var entry = new FileEntry { DecompressedSize = 0 }; - var provider = new OutputAbsolutePathProvider(path, "relative/path.txt", entry); // Act - var fileData = provider.GetFileData(0, 5); - - // Assert - fileData.DataLength.Should().Be(0ul); - Assert.IsType(fileData); + using (var provider = new OutputAbsolutePathProvider(path, "relative/path.txt", entry)) + using (var fileData = provider.GetFileData(0, 5)) + { + // Assert + fileData.DataLength.Should().Be(0ul); + Assert.IsType(fileData); + } // Cleanup CleanupTestFile(fileSystem, path); @@ -61,10 +63,11 @@ public void Constructor_CreatesFile(IFileSystem fileSystem) var entry = new FileEntry { DecompressedSize = 100 }; // Act - _ = new OutputAbsolutePathProvider(path, "relative/path.txt", entry); - - // Assert - fileSystem.FileExists(path).Should().BeTrue(); + using (_ = new OutputAbsolutePathProvider(path, "relative/path.txt", entry)) + { + // Assert + fileSystem.FileExists(path).Should().BeTrue(); + } // Cleanup CleanupTestFile(fileSystem, path); diff --git a/tests/NexusMods.Paths.Tests/FileSystem/FileSystemTests.cs b/tests/NexusMods.Paths.Tests/FileSystem/FileSystemTests.cs index 63c103b..ceb0457 100644 --- a/tests/NexusMods.Paths.Tests/FileSystem/FileSystemTests.cs +++ b/tests/NexusMods.Paths.Tests/FileSystem/FileSystemTests.cs @@ -110,20 +110,34 @@ public async Task Test_CreateMemoryMappedFile_CanCreateAndWrite() var tempFile = fs.GetKnownPath(KnownPath.TempDirectory).Combine(Path.GetRandomFileName()); var contents = new byte[] { 1, 2, 3, 4, 5 }; + // Create a new MemoryMappedFile try { - // Create a new MemoryMappedFile - using var mmf = fs.CreateMemoryMappedFile(tempFile, FileMode.CreateNew, MemoryMappedFileAccess.ReadWrite, (ulong)contents.Length); - - unsafe + using (var mmf = fs.CreateMemoryMappedFile(tempFile, FileMode.CreateNew, MemoryMappedFileAccess.ReadWrite, + (ulong)contents.Length)) { - mmf.Should().NotBeNull(); - ((nuint)mmf.Pointer).Should().NotBe(0); - mmf.Length.Should().Be((nuint)contents.Length); - - // Write data to the MemoryMappedFile - contents.CopyTo(new Span(mmf.Pointer, contents.Length)); + unsafe + { + mmf.Should().NotBeNull(); + ((nuint)mmf.Pointer).Should().NotBe(0); + mmf.Length.Should().Be((nuint)contents.Length); + + // Write data to the MemoryMappedFile + contents.CopyTo(new Span(mmf.Pointer, contents.Length)); + } } + + /* + Note(sewer) + + There's something weird going on in the Memory Mapped File abstraction, + we open a FileStream with `FileShare.Read`, to create the MemoryMappedFile from under the hood, + and we propagate this when opening the MemoryMappedFile. + + Below in `ReadAllBytesAsync`, we are opening a second FileStream with `FileAccess.Read` and `FileShare.Read`. + It should technically work, but it does not. Chances are it's some not properly documented part + of .NET's MemoryMappedFile abstraction for Win32. + */ // Verify the data was written correctly var writtenData = await fs.ReadAllBytesAsync(tempFile);