diff --git a/PG.StarWarsGame.Files/PG.StarWarsGame.Files.Test/HolderTest_MegAndDisposableModel.cs b/PG.StarWarsGame.Files/PG.StarWarsGame.Files.Test/HolderTest_MegAndDisposableModel.cs index f54033202..6f70d60e3 100644 --- a/PG.StarWarsGame.Files/PG.StarWarsGame.Files.Test/HolderTest_MegAndDisposableModel.cs +++ b/PG.StarWarsGame.Files/PG.StarWarsGame.Files.Test/HolderTest_MegAndDisposableModel.cs @@ -3,21 +3,21 @@ namespace PG.StarWarsGame.Files.Test; -public class HolderTest_MegAndDisposableModel : PetroglyphFileHolderTest> +public class HolderTest_MegAndDisposableModel : PetroglyphFileHolderTest> { protected override DisposableModel CreateModel() { return new DisposableModel(); } - protected override MegTestParam CreateFileInfo(string path, bool inMeg = false) + protected override TestMegFileInfo CreateFileInfo(string path, bool inMeg = false) { - return new MegTestParam { FilePath = path, IsInsideMeg = inMeg}; + return new TestMegFileInfo { FilePath = path, IsInsideMeg = inMeg}; } - protected override TestFileHolder CreateFileHolder(DisposableModel model, MegTestParam fileInfo) + protected override TestFileHolder CreateFileHolder(DisposableModel model, TestMegFileInfo fileInfo) { - return new TestFileHolder(model, fileInfo, ServiceProvider); + return new TestFileHolder(model, fileInfo, ServiceProvider); } [Fact] @@ -25,9 +25,9 @@ public void Ctor_ThrowsArgumentNullException() { var model = CreateModel(); var fileInfo = CreateFileInfo(DefaultFileName); - Assert.Throws(() => new TestFileHolder(model, fileInfo, null!)); - Assert.Throws(() => new TestFileHolder(model, null!, ServiceProvider)); - Assert.Throws(() => new TestFileHolder(null!, fileInfo, ServiceProvider)); + Assert.Throws(() => new TestFileHolder(model, fileInfo, null!)); + Assert.Throws(() => new TestFileHolder(model, null!, ServiceProvider)); + Assert.Throws(() => new TestFileHolder(null!, fileInfo, ServiceProvider)); } [Fact] diff --git a/PG.StarWarsGame.Files/PG.StarWarsGame.Files.Test/HolderTest_NonMegNonDisposableModel.cs b/PG.StarWarsGame.Files/PG.StarWarsGame.Files.Test/HolderTest_NonMegNonDisposableModel.cs index 5aaded704..e44ae6f29 100644 --- a/PG.StarWarsGame.Files/PG.StarWarsGame.Files.Test/HolderTest_NonMegNonDisposableModel.cs +++ b/PG.StarWarsGame.Files/PG.StarWarsGame.Files.Test/HolderTest_NonMegNonDisposableModel.cs @@ -3,23 +3,23 @@ namespace PG.StarWarsGame.Files.Test; -public class HolderTest_NonMegNonDisposableModel : PetroglyphFileHolderTest> +public class HolderTest_NonMegNonDisposableModel : PetroglyphFileHolderTest> { protected override object CreateModel() { return new DisposableModel(); } - protected override TestParam CreateFileInfo(string path, bool inMeg = false) + protected override TestFileInfo CreateFileInfo(string path, bool inMeg = false) { if (inMeg) Assert.Fail(); - return new TestParam { FilePath = path }; + return new TestFileInfo { FilePath = path }; } - protected override TestFileHolder CreateFileHolder(object model, TestParam fileInfo) + protected override TestFileHolder CreateFileHolder(object model, TestFileInfo fileInfo) { - return new TestFileHolder(model, fileInfo, ServiceProvider); + return new TestFileHolder(model, fileInfo, ServiceProvider); } [Fact] @@ -27,8 +27,8 @@ public void Ctor_ThrowsArgumentNullException() { var model = CreateModel(); var fileInfo = CreateFileInfo(DefaultFileName); - Assert.Throws(() => new TestFileHolder(model, fileInfo, null!)); - Assert.Throws(() => new TestFileHolder(model, null!, ServiceProvider)); - Assert.Throws(() => new TestFileHolder(null!, fileInfo, ServiceProvider)); + Assert.Throws(() => new TestFileHolder(model, fileInfo, null!)); + Assert.Throws(() => new TestFileHolder(model, null!, ServiceProvider)); + Assert.Throws(() => new TestFileHolder(null!, fileInfo, ServiceProvider)); } } \ No newline at end of file diff --git a/PG.StarWarsGame.Files/PG.StarWarsGame.Files.Test/PetroglyphFileHolderTest.cs b/PG.StarWarsGame.Files/PG.StarWarsGame.Files.Test/PetroglyphFileHolderTest.cs index 6730df0ea..965dc0f40 100644 --- a/PG.StarWarsGame.Files/PG.StarWarsGame.Files.Test/PetroglyphFileHolderTest.cs +++ b/PG.StarWarsGame.Files/PG.StarWarsGame.Files.Test/PetroglyphFileHolderTest.cs @@ -1,6 +1,7 @@ -using PG.Testing; +using PG.Testing; using System; using System.IO; +using System.Runtime.InteropServices; using AnakinRaW.CommonUtilities.Testing.Attributes; using AnakinRaW.CommonUtilities.Testing.Extensions; using Testably.Abstractions.Testing; @@ -21,72 +22,81 @@ public abstract class PetroglyphFileHolderTest : PGT protected abstract THolder CreateFileHolder(TModel model, TFileInfo fileInfo); - - [Fact] - public void Ctor_SetupProperties() + [Theory] + [InlineData("test.txt")] + [InlineData("./test")] + [InlineData("a/../test")] + [InlineData("üöä")] + [InlineData("a/b")] + public void Ctor_LocalFile_SetupProperties(string filePath) { var model = CreateModel(); - - FileSystem.Initialize().WithFile(DefaultFileName); - var param = CreateFileInfo(DefaultFileName); + FileSystem.Initialize().WithFile(filePath); + var param = CreateFileInfo(filePath); var holder = CreateFileHolder(model, param); + var expectedFilePath = FileSystem.Path.GetFullPath(filePath); + Assert.Same(model, holder.Content); Assert.Same(model, ((IPetroglyphFileHolder)holder).Content); + Assert.Same(ServiceProvider, holder.Services); + Assert.NotNull(holder.Logger); + + Assert.Equal(FileSystem.Path.GetFileName(expectedFilePath), holder.FileName); + Assert.Equal(expectedFilePath, holder.FilePath); + Assert.Equal(FileSystem.Path.GetDirectoryName(expectedFilePath), holder.Directory); + + Assert.Equal(expectedFilePath, holder.FileInformation.FilePath); Assert.NotSame(param, holder.FileInformation); Assert.NotSame(param, ((IPetroglyphFileHolder)holder).FileInformation); - Assert.Equal(FileSystem.Path.GetFullPath(param.FilePath), FileSystem.Path.GetFullPath(holder.FileInformation.FilePath)); - Assert.Equal(FileSystem.Path.GetFullPath(param.FilePath), FileSystem.Path.GetFullPath(((IPetroglyphFileHolder)holder).FileInformation.FilePath)); + } - Assert.Equal(FileSystem.Path.GetFullPath(DefaultFileName), holder.FilePath); - Assert.Equal(FileSystem.Path.GetDirectoryName(FileSystem.Path.GetFullPath(DefaultFileName)), holder.Directory); - Assert.Same(ServiceProvider, holder.Services); - Assert.NotNull(holder.Logger); + [PlatformSpecificTheory(TestPlatformIdentifier.Linux)] + [InlineData("FOO\\BAR.XML")] + [InlineData("DATA\\XML\\FOO.XML")] + public void Ctor_LocalFile_BackslashIsLiteralFilenameChar_Linux(string filePath) + { + var model = CreateModel(); + FileSystem.Initialize().WithFile(filePath); + var holder = CreateFileHolder(model, CreateFileInfo(filePath)); + + Assert.Equal(filePath, holder.FileName); + Assert.Equal("/", holder.Directory); + Assert.Equal("/" + filePath, holder.FilePath); } [Theory] - [InlineData("test", true)] - [InlineData("path/test", true)] - [InlineData("test", false)] - [InlineData("path/test", false)] - public void Ctor_SetupProperties_MegSupport(string path, bool inMeg) + [InlineData("foo.xml")] + [InlineData("FOO.XML")] + [InlineData("DATA\\FOO.XML")] + [InlineData("data/foo.xml")] + [InlineData("DATA\\XML\\FOO.XML")] + [InlineData("DATA/SUB\\FOO.XML")] + [InlineData("DATA\\SUB/FOO.XML")] + [InlineData("./DATA\\SUB/FOO.XML")] + [InlineData(".\\DATA\\SUB/FOO.XML")] + public void Ctor_InMeg_SetupProperties(string filePath) { - if (!typeof(TFileInfo).IsAssignableFrom(typeof(PetroglyphMegPackableFileInformation))) + if (!typeof(PetroglyphMegPackableFileInformation).IsAssignableFrom(typeof(TFileInfo))) return; var model = CreateModel(); - - if (!inMeg) - FileSystem.Initialize().WithFile(path); - - var param = CreateFileInfo(path, inMeg); - Assert.Equal(inMeg, (param as PetroglyphMegPackableFileInformation)!.IsInsideMeg); + var param = CreateFileInfo(filePath, inMeg: true); var holder = CreateFileHolder(model, param); - Assert.Same(model, holder.Content); - Assert.Same(model, ((IPetroglyphFileHolder)holder).Content); - Assert.NotSame(holder.FileInformation, param); - Assert.NotSame(((IPetroglyphFileHolder)holder).FileInformation, param); - - if (inMeg) - { - Assert.Equal(param.FilePath, holder.FilePath); - Assert.Equal(FileSystem.Path.GetDirectoryName(path), holder.Directory); - Assert.NotNull(holder.Directory); + var expectedFilePath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? filePath + : filePath.Replace('\\', '/'); - Assert.Equal(holder.FileInformation, param); - } - else - { - Assert.Equal(FileSystem.Path.GetFullPath(path), holder.FilePath); - Assert.Equal(FileSystem.Path.GetDirectoryName(FileSystem.Path.GetFullPath(path)), holder.Directory); - - Assert.NotEqual(holder.FileInformation, param); - } + Assert.Equal(FileSystem.Path.GetFileName(expectedFilePath), holder.FileName); + Assert.Equal(expectedFilePath, holder.FilePath); + Assert.Equal(FileSystem.Path.GetDirectoryName(expectedFilePath), holder.Directory); - Assert.Same(ServiceProvider, holder.Services); + Assert.Equal(filePath, holder.FileInformation.FilePath); + Assert.NotSame(param, holder.FileInformation); + Assert.Equal(param, holder.FileInformation); } [PlatformSpecificTheory(TestPlatformIdentifier.Linux)] @@ -107,52 +117,6 @@ public void PassingFileNames_Whitespace_Linux(string filePath, string? expectedF } } - [PlatformSpecificTheory(TestPlatformIdentifier.Windows)] - [InlineData("test.txt", "test.txt", "C:\\", "C:\\test.txt")] - [InlineData("./test", "test", "C:\\", "C:\\test")] - [InlineData("a/../test", "test", "C:\\", "C:\\test")] - [InlineData("üöä", "üöä", "C:\\", "C:\\üöä")] - [InlineData("a/b", "b", "C:\\a", "C:\\a\\b")] -#if NET - [InlineData("test/\u00A0", "\u00A0", "C:\\test", "C:\\test\\\u00A0")] -#endif - //[InlineData("\u00A0", "\u00A0", "C:\\\u00A0", "C:\\u00A0")] // Currently not possible due to https://github.com/TestableIO/System.IO.Abstractions/issues/1070 - public void PassingFileNames_Windows(string filePath, string? expectedFileName, string expectedDirectory, string expectedFilePath) - { - var model = CreateModel(); - FileSystem.Initialize().WithFile(filePath); - var holder = CreateFileHolder(model, CreateFileInfo(filePath)); - - if (expectedFileName is not null) - { - Assert.Equal(expectedFileName, holder.FileName); - Assert.Equal(expectedDirectory, holder.Directory); - Assert.Equal(expectedFilePath, holder.FilePath); - } - } - - [PlatformSpecificTheory(TestPlatformIdentifier.Linux)] - [InlineData("test.txt", "test.txt", "/", "/test.txt")] - [InlineData("./test", "test", "/", "/test")] - [InlineData("a/../test", "test", "/", "/test")] - [InlineData("üöä", "üöä", "/", "/üöä")] - [InlineData("a/b", "b", "/a", "/a/b")] - [InlineData("test/\u00A0", "\u00A0", "/test", "/test/\u00A0")] - // [InlineData("\u00A0", "\u00A0", "/\u00A0", "/\u00A0")] // Currently not possible due to https://github.com/TestableIO/System.IO.Abstractions/issues/1070 - public void PassingFileNames_Linux(string filePath, string? expectedFileName, string expectedDirectory, string expectedFilePath) - { - var model = CreateModel(); - FileSystem.Initialize().WithFile(filePath); - var holder = CreateFileHolder(model, CreateFileInfo(filePath)); - - if (expectedFileName is not null) - { - Assert.Equal(expectedFileName, holder.FileName); - Assert.Equal(expectedDirectory, holder.Directory); - Assert.Equal(expectedFilePath, holder.FilePath); - } - } - [PlatformSpecificTheory(TestPlatformIdentifier.Windows)] [InlineData(" ", typeof(ArgumentException))] public void Ctor_InvalidPath_Whitespace_Windows_Throws(string path, Type type) @@ -180,7 +144,7 @@ public void Ctor_FileNotFound_Throws() Assert.Throws(() => CreateFileHolder(model, CreateFileInfo("notFound"))); - if (!typeof(TFileInfo).IsAssignableFrom(typeof(PetroglyphMegPackableFileInformation))) + if (!typeof(PetroglyphMegPackableFileInformation).IsAssignableFrom(typeof(TFileInfo))) return; Assert.DoesNotThrow(() => CreateFileHolder(model, CreateFileInfo("notFound", true))); @@ -208,11 +172,11 @@ public void FileInformation_ReturnsCopy() FileSystem.Initialize().WithFile(DefaultFileName); var disposableParam = CreateFileInfo(DefaultFileName); var holder = CreateFileHolder(model, disposableParam); - + var a = holder.FileInformation; var b = holder.FileInformation; Assert.NotSame(a, b); Assert.Equal(a, b); } -} \ No newline at end of file +} diff --git a/PG.StarWarsGame.Files/PG.StarWarsGame.Files.Test/PetroglyphFileInformationTest.cs b/PG.StarWarsGame.Files/PG.StarWarsGame.Files.Test/PetroglyphFileInformationTest.cs index 419374cf1..ae8db1ecd 100644 --- a/PG.StarWarsGame.Files/PG.StarWarsGame.Files.Test/PetroglyphFileInformationTest.cs +++ b/PG.StarWarsGame.Files/PG.StarWarsGame.Files.Test/PetroglyphFileInformationTest.cs @@ -10,16 +10,39 @@ public class PetroglyphFileInformationTest [InlineData("")] public void EmptyPath_Throws(string? path) { - Assert.ThrowsAny(() => _ = new MegTestParam + Assert.ThrowsAny(() => _ = new TestMegFileInfo { FilePath = path! }); } + [Theory] + [InlineData("file.txt")] + [InlineData("FILE.TXT")] + [InlineData("path/file.txt")] + [InlineData("PATH\\FILE.TXT")] + public void FilePath_ContainsRawData(string path) + { + Assert.Equal(path, new TestFileInfo + { + FilePath = path, + }.FilePath); + Assert.Equal(path, new TestMegFileInfo + { + FilePath = path, + IsInsideMeg = true + }.FilePath); + Assert.Equal(path, new TestMegFileInfo + { + FilePath = path, + IsInsideMeg = false + }.FilePath); + } + [Fact] public void Dispose() { - var info = new MegTestParam + var info = new TestMegFileInfo { FilePath = "somePath" }; diff --git a/PG.StarWarsGame.Files/PG.StarWarsGame.Files.Test/TestFileInfo.cs b/PG.StarWarsGame.Files/PG.StarWarsGame.Files.Test/TestFileInfo.cs new file mode 100644 index 000000000..76f55e316 --- /dev/null +++ b/PG.StarWarsGame.Files/PG.StarWarsGame.Files.Test/TestFileInfo.cs @@ -0,0 +1,3 @@ +namespace PG.StarWarsGame.Files.Test; + +public record TestFileInfo : PetroglyphFileInformation; \ No newline at end of file diff --git a/PG.StarWarsGame.Files/PG.StarWarsGame.Files.Test/MegTestParam.cs b/PG.StarWarsGame.Files/PG.StarWarsGame.Files.Test/TestMegFileInfo.cs similarity index 76% rename from PG.StarWarsGame.Files/PG.StarWarsGame.Files.Test/MegTestParam.cs rename to PG.StarWarsGame.Files/PG.StarWarsGame.Files.Test/TestMegFileInfo.cs index f3792af83..ba7f66adc 100644 --- a/PG.StarWarsGame.Files/PG.StarWarsGame.Files.Test/MegTestParam.cs +++ b/PG.StarWarsGame.Files/PG.StarWarsGame.Files.Test/TestMegFileInfo.cs @@ -1,6 +1,6 @@ namespace PG.StarWarsGame.Files.Test; -public record MegTestParam : PetroglyphMegPackableFileInformation +public record TestMegFileInfo : PetroglyphMegPackableFileInformation { public bool IsDisposed { get; private set; } diff --git a/PG.StarWarsGame.Files/PG.StarWarsGame.Files.Test/TestParam.cs b/PG.StarWarsGame.Files/PG.StarWarsGame.Files.Test/TestParam.cs deleted file mode 100644 index a31e0ba2e..000000000 --- a/PG.StarWarsGame.Files/PG.StarWarsGame.Files.Test/TestParam.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace PG.StarWarsGame.Files.Test; - -public record TestParam : PetroglyphFileInformation; \ No newline at end of file diff --git a/PG.StarWarsGame.Files/PG.StarWarsGame.Files/IPetroglyphFileHolder.cs b/PG.StarWarsGame.Files/PG.StarWarsGame.Files/IPetroglyphFileHolder.cs index 15cd089df..313b3b394 100644 --- a/PG.StarWarsGame.Files/PG.StarWarsGame.Files/IPetroglyphFileHolder.cs +++ b/PG.StarWarsGame.Files/PG.StarWarsGame.Files/IPetroglyphFileHolder.cs @@ -28,7 +28,16 @@ public interface IPetroglyphFileHolder : IDisposable /// Gets the relative or absolute file path e.g, "c:/my/path/myfile.txt". /// /// - /// Relative paths are only allowed if the file is packed inside a MEG archive. + /// + /// If originates from a MEG file, + /// the returned path is normalized to the current operating system. + /// This means the Windows directory separator ("\") + /// is treated as directory separator on linux systems too. + /// To retrieve the original file path for MEG files, use of . + /// + /// + /// Relative paths are only returned if the file is packed inside a MEG archive. + /// /// string FilePath { get; } @@ -37,13 +46,29 @@ public interface IPetroglyphFileHolder : IDisposable /// or if the file does not contain directory information. /// /// + /// + /// If originates from a MEG file, + /// the returned directory path is normalized to the current operating system. + /// This means the Windows directory separator ("\") + /// is treated as directory separator on linux systems too. + /// To retrieve the original file path for MEG files, use of . + /// + /// /// Relative or empty paths are only allowed if the file is packed inside a MEG archive. + /// /// string Directory { get; } /// /// Gets the file name with extension, e.g, "myfile.txt". /// + /// + /// If originates from a MEG file, + /// the returned file name is normalized to the current operating system. + /// This means the Windows directory separator ("\") + /// is treated as directory separator on linux systems too. + /// To retrieve the original file path for MEG files, use of . + /// string FileName { get; } } diff --git a/PG.StarWarsGame.Files/PG.StarWarsGame.Files/PathHelper.cs b/PG.StarWarsGame.Files/PG.StarWarsGame.Files/PathHelper.cs new file mode 100644 index 000000000..32ddedbce --- /dev/null +++ b/PG.StarWarsGame.Files/PG.StarWarsGame.Files/PathHelper.cs @@ -0,0 +1,13 @@ +using AnakinRaW.CommonUtilities.FileSystem.Normalization; + +namespace PG.StarWarsGame.Files; + +internal static class PathHelper +{ + internal static readonly PathNormalizeOptions UnixMegPathNormalizationOptions = new() + { + UnifyDirectorySeparators = true, + TreatBackslashAsSeparator = true, + UnifySeparatorKind = DirectorySeparatorKind.System + }; +} \ No newline at end of file diff --git a/PG.StarWarsGame.Files/PG.StarWarsGame.Files/PetroglyphFileHolder.cs b/PG.StarWarsGame.Files/PG.StarWarsGame.Files/PetroglyphFileHolder.cs index 32f02c33a..f3f40b2e3 100644 --- a/PG.StarWarsGame.Files/PG.StarWarsGame.Files/PetroglyphFileHolder.cs +++ b/PG.StarWarsGame.Files/PG.StarWarsGame.Files/PetroglyphFileHolder.cs @@ -4,7 +4,9 @@ using System; using System.IO; using System.IO.Abstractions; +using System.Runtime.InteropServices; using AnakinRaW.CommonUtilities; +using AnakinRaW.CommonUtilities.FileSystem.Normalization; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -72,6 +74,7 @@ public TFileInfo FileInformation /// The data model of this holder. /// The file information for this holder. /// The for this instance. + /// contains an invalid path or is not a file. /// or or is . /// The underlying local file of the does not exist. protected PetroglyphFileHolder(TModel model, TFileInfo fileInformation, IServiceProvider serviceProvider) @@ -86,33 +89,38 @@ protected PetroglyphFileHolder(TModel model, TFileInfo fileInformation, IService FileSystem = serviceProvider.GetRequiredService(); Logger = serviceProvider.GetService()?.CreateLogger(GetType()) ?? NullLogger.Instance; - var fileInfo = FileSystem.FileInfo.New(fileInformation.FilePath); - - // We got a path with trailing path separator which is treated as a directory path. - if (string.IsNullOrEmpty(fileInfo.Name)) - throw new ArgumentException($"The specified path '{fileInfo.FullName}' is not a valid file path."); - - var fileName = fileInfo.Name; - ThrowHelper.ThrowIfNullOrEmpty(fileName); - - FileName = fileInfo.Name; - if (fileInformation is PetroglyphMegPackableFileInformation { IsInsideMeg: true }) { - FilePath = fileInformation.FilePath; - Directory = FileSystem.Path.GetDirectoryName(fileInformation.FilePath) ?? + var path = fileInformation.FilePath; + + // NB: This is necessary, because for EaW/FoC file paths inside MEG files + // may use Windows-style separators even on Unix-based systems. + // Therefore, the paths of files originating from MEG files + // are sanitized to treat backslashes as directory separators. + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + path = PathNormalizer.Normalize(path, PathHelper.UnixMegPathNormalizationOptions); + + FilePath = path; + FileName = GetFileNameOrThrow(path); + Directory = FileSystem.Path.GetDirectoryName(path) ?? throw new InvalidOperationException($"No directory found for file '{FilePath}'"); + // Create a defensive copy of the file information to ensure that the original + // instance can be safely disposed without affecting this holder. _internalFileInformation = fileInformation with { }; } else { + var fileInfo = FileSystem.FileInfo.New(fileInformation.FilePath); + + // Use absolute paths for local files. + FilePath = fileInfo.FullName; + FileName = GetFileNameOrThrow(fileInfo.FullName); + // We can only check whether the file exists if it is a local file. if (!fileInfo.Exists) throw new FileNotFoundException($"File '{fileInfo.FullName}' not found.", fileInfo.FullName); - // Use absolute paths for local files. - FilePath = fileInfo.FullName; Directory = fileInfo.DirectoryName ?? throw new InvalidOperationException($"No directory found for file '{FilePath}'"); @@ -120,6 +128,14 @@ protected PetroglyphFileHolder(TModel model, TFileInfo fileInformation, IService _internalFileInformation = fileInformation with { FilePath = FilePath }; } } + + private string GetFileNameOrThrow(string path) + { + var name = FileSystem.Path.GetFileName(path); + return string.IsNullOrEmpty(name) + ? throw new ArgumentException($"The specified path '{path}' is not a file.") + : name; + } /// protected override void DisposeResources() diff --git a/PG.StarWarsGame.Files/PG.StarWarsGame.Files/PetroglyphFileInformation.cs b/PG.StarWarsGame.Files/PG.StarWarsGame.Files/PetroglyphFileInformation.cs index 555ae52ac..0127db3a2 100644 --- a/PG.StarWarsGame.Files/PG.StarWarsGame.Files/PetroglyphFileInformation.cs +++ b/PG.StarWarsGame.Files/PG.StarWarsGame.Files/PetroglyphFileInformation.cs @@ -15,11 +15,12 @@ public abstract record PetroglyphFileInformation : IDisposable private readonly string _filePath; /// - /// Gets or sets the file path e.g, "c:/my/path/myfile.txt" + /// Gets or sets the path of the file e.g, "c:/my/path/myfile.txt" /// /// - /// The path may be relative. + /// The path is taken as-is and may be relative. /// + /// is empty. /// is . public required string FilePath {